10086
10086
3月前 · 11 人阅读

Vue+Django REST framework实战

搭建一个前后端分离的生鲜超市网站
Django rtf 完成 手机注册和用户登录(下)

user serializer和validator验证(注册功能)

注册页面需要我们输入手机号码 验证码 和密码

django的form 和 model form 是用来验证用户提交的字段的合法性的。

restful api 中实际是对资源的操作。我们的注册对应的资源就是用户。

因为用户注册必定是会crate model操作的,所以继承CreateModelMixin

先为viewset准备一个配套的Serializers

users/serializers.py(继承modelSerializer,因为字段都是必填项,都有的,虽然相比较用户model多了一个code字段)

通过一些小技巧,既能享受model Serializer带来的好处,又能突破它的限制

class UserRegSerializer(serializers.ModelSerializer):
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4,error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 },
                                 help_text="验证码")

自定义我们每种验证的错误信息。

验证username是否存在:

    username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])
mark

mark

drf为我们提供的唯一性字段验证。together是联合字段验证,也就是比如收藏这种。
关系唯一。

UniqueValidator的参数:

  • queryset required - This is the queryset against which uniqueness should be enforced.
  • message - The error message that should be used when validation fails.
  • lookup - The lookup used to find an existing instance with the value being validated. Defaults to 'exact'.

lookup执行哪些操作。

    username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

eclipse快捷键下,pycharm格式化快捷键为ctrl+shift+f

为mobile model添加可以为空的选项。

    mobile = models.CharField(null=True, blank=True, max_length=11, verbose_name="电话")

因为前端只有手机号码这一个值。而这个值还存在username中。所以需要置空,保证验证成功。

在Serializer中添加code字段,这个code是多余字段不会被保存到数据库中

 def validate_code(self, code):

        # get与filter的区别: get有两种异常,一个是有多个,一个是一个都没有。
        # try:
        #     verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
        # except VerifyCode.DoesNotExist as e:
        #     pass
        # except VerifyCode.MultipleObjectsReturned as e:
        #     pass

        # 验证码在数据库中是否存在,用户从前端post过来的值都会放入initial_data里面,排序(最新一条)。
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            # 获取到最新一条
            last_record = verify_records[0]

            # 有效期为五分钟。
            five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError("验证码过期")

            if last_record.code != code:
                raise serializers.ValidationError("验证码错误")

        else:
            raise serializers.ValidationError("验证码错误")

进行验证码的验证,可能出现的错误有过期,验证码错误。或记录根本不存在。

验证完之后将code这个字段删除掉

    # 不加字段名的验证器作用于所有字段之上。attrs是字段 validate之后返回的总的dict
    def validate(self, attrs):
        attrs["mobile"] = attrs["username"]
        del attrs["code"]
        return attrs

views中的用户viewset中实例化Serializer

class UserViewset(CreateModelMixin, viewsets.GenericViewSet):
    """
    用户
    """
    serializer_class = UserRegSerializer

为该viewset配置路由

# 配置users的url
router.register(r'users', UserViewset, base_name="users")

drf验证默认的返回格式:

HTTP 400 Bad Request
Allow: POST, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "username": [
        "用户已经存在"
    ],
    "code": [
        "验证码错误"
    ]
}
  • 单个字段出错: 字段 + 数组
  • 联合字段出错: non_fields_error

哪个出错哪个高亮。

一般我们的传统开发都会喜欢自定义消息

{
    "status": 0,
    "msg": 验证码出错
}

不好的地方:

前端自己要去做解析知道哪种对应着失败。
想要做到单个字段的提示,消息要做成这种格式

{
    "status": 0,
    "msg":
    {
        moblie: [""],
        code: [""]  
    }
}

status如果只是用来判定用户的状态正确或失败,那跟httpcode的区别就几乎没有了
rest api 的设计模式。

拉勾网: 请求过多page, 已经发生异常显示的不正常的页面,但是200状态。
影响seo。对于Google

基于http code进行开发,可以让大家对于错误正确的状态判断保持一致

完善用户注册,django信号量实现用户密码修改。

用户注册逻辑编码。

后台添加一条用户短信验证码数据之后进行验证。

class UserViewset(CreateModelMixin, viewsets.GenericViewSet):
    """
    用户
    """
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

http://www.django-rest-framework.org/api-guide/fields/

报错信息:

Got AttributeError when attempting to get a value for field `code` on serializer `UserRegSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `UserProfile` instance.
Original exception text was: 'UserProfile' object has no attribute 'code'.

这是因为我们配置的

        fields = ("username", "code", "mobile", "password")

有四个字段,而我们在Serializer的处理验证过程中已经删除了其中的code。

解决方案(重要参数):

code字段添加write only=true。就不会将此字段进行序列化返回给前端。

上面的错误查看源码中CreateModelMixin的部分代码。可以看到它在验证了是否有效之后执行了save。这些都是不会有问题的,但是当它return Response时,它会return S rializer的data(会依照我们在fields中的配置)。这时候因为data中的字段已经和model中的不再一致。

如果是一个正常的Serializer会将我们post过去的数据序列化之后返回回来。

password也被返回了回来。这是不合理的,为password也添加write only =True参数

密码会被明文存储。

    def create(self, validated_data):
        user = super(UserRegSerializer, self).create(validated_data=validated_data)
        user.set_password(validated_data["password"])
        user.save()
        return user

重载Serializer的create方法。可以实现。

虽然重载代码量已经很少了,但是可能比较难理解,所以我们选择其他解决方案

附录:

label是如下图form中左侧字样,helptext是input框下面的。

django信号量机制。

django post_save()方法

我们的model对象进行操作的时候,会发出全局的信号。捕捉之后做出我们自己的操作。

https://docs.djangoproject.com/en/2.0/ref/signals/

django的信号量如request_started和scrapy中的是一致的。

删除之前做一些事情,就可以接收pre_delete的信号

post_save

http://www.django-rest-framework.org/api-guide/authentication/

Generating Tokens
By using signals
If you want every user to have an automatically generated Token, you can simply catch the User's post_save signal.

from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

新建文件users/signals.py

# encoding: utf-8
__author__ = 'mtianyan'
__date__ = '2018/3/9 0009 09:29'

from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

from django.contrib.auth import get_user_model
User = get_user_model()


# 参数一接收哪种信号,参数二是接收哪个model的信号
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    # 是否新建,因为update的时候也会进行post_save
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()

做完刚才这些操作,还要重载一个配置

否则会导致虽然没有任何报错信息,但是密码并没有被加密

apps.py中。

    def ready(self):
        """
        Override this method in subclasses to run code when Django starts.
        """

这是AppConfig中我们可以在子类中自定义的函数,它将会在django启动时被运行。

    def ready(self):
        import users.signals

我们修改的mobile的字段可为空,只在代码中修改是没有用的,还需要我们进行migrations

这样我们才可以在后台中不需要mobile字段直接添加用户。

mark

mark

可以看到成功的添加了密码并且为我们进行加密。

vue和注册功能联调

  • 查看vue的项目结构,找到register.vue
                    <input class="btn btn-green" id="jsMobileRegBtn" @click="isRegister" type="button" value="注册并登录">

点击注册并登录会调用isRegister 函数,

isRegister(){
            var that = this;
            register({
                password:that.password,
                username:that.mobile ,
                code:that.code,
            }).then((response)=> {
              cookie.setCookie('name',response.data.username,7);
              cookie.setCookie('token',response.data.token,7)
              //存储在store
              // 更新store数据
              that.$store.dispatch('setInfo');
              //跳转到首页页面
              this.$router.push({ name: 'index'})

          })

调用了一个register的函数接口。

export const register = parmas => { return axios.post(`${host}/users/`, parmas) }

实际上就是指向我们的user这个url。获取参数。将host 改为localhost进行联调

register传递的参数有password,username,code。这些都是前端页面post过来的值

常见的注册登录有注册完了你自己登录,以及注册完成帮你自动登录。

如果是让用户自己去登录,那么就将cookie.setcookie这两行注释掉。直接让它跳转到首页。

但是如果是自动登录,那么我们此时就没有给前台返回jwt 的token。

重载createmodelmixin里面的create函数。

框架原本实现代码:

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

这里的perform中的save是save了当前的model(user)。但是并没有返回该model。我们要想获取到user model 就必须重写让它返回model

然后在执行perform_create之后插入我们自己的逻辑。

分析jwt的源码实现,找到它哪部分才是生成token的。

  • 从url入口。点进obtain_jwt_token
    # jwt的token认证
    path('login/', obtain_jwt_token )

实现代码:

obtain_jwt_token = ObtainJSONWebToken.as_view()
class ObtainJSONWebToken(JSONWebTokenAPIView):
    """
    API View that receives a POST with a user's username and password.

    Returns a JSON Web Token that can be used for authenticated requests.
    """
    serializer_class = JSONWebTokenSerializer

此时我们就可以去查看继承的父类: JSONWebTokenAPIView

该类中用户在post数据过来之后。

 def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

关键在于token是直接从Serializer中获取的,那么token的生成应该是在Serializer中实现的。

            token = serializer.object.get('token')

寻找我们的Serializer

serializer = self.get_serializer(data=request.data)

    def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()
        return self.serializer_class

Serializer位于rest_framework_jwt/views.py的ObtainJSONWebToken类中

    serializer_class = JSONWebTokenSerializer

该类中token的生成相关代码

                payload = jwt_payload_handler(user)

                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }

调用了jwt_encode_handler, 而该handler是api_setting中设置的

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

rest_framework_jwt/settings.py

DEFAULTS = {
    'JWT_ENCODE_HANDLER':
    'rest_framework_jwt.utils.jwt_encode_handler',
      'JWT_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_payload_handler',
    }

所以我们已经找到了生成token的两个重要步骤,一payload,二encode

实现代码:

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        re_dict["token"] = jwt_encode_handler(payload)
        re_dict["name"] = user.name if user.name else user.username

         headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

其中的token要和前端保持一致。注意将原本返回的Serializer.data进行加工之后返回。

退出功能

退出功能就变得更好做了,因为jwt的token并不是保存在服务器端的。

src/views/head/shophead.vue

实现代码:

                <a @click="loginOut">退出</a>

退出按钮调用的是loginout函数

实现代码:

 loginOut(){
        cookie.delCookie('token');
        cookie.delCookie('name');
        //重新触发store
        //更新store数据
        this.$store.dispatch('setInfo');
        //跳转到登录
        this.$router.push({name: 'login'})
      },

清空token给axios发一个通知。跳转到登录页面。

注册页面测试,将codes与前端保持一致,以及修改localhost

收藏 0
关键词: serializer token user code dat
评论