首页 > 技术文章 > (生鲜项目)18. 用户手机号注册 (code验证 + 信号量 + 和前端Vue联调)

jiangzongyou 2019-12-28 16:07 原文

第一步: 前期分析

 

 

可见, 前端会POST过来三个字段, 所以这里我们应该使用 mixins.CreateModelMixin

由于我们自定义的Userprofile中只有name是必填字段(注意:django自带的user中username是必填字段), 所以我们可以在serializers.py中使用 serializers.ModelSerializer

code的验证需要单独的validate_code()方法

  • 并没有向手机号发送验证码, 用户随便输入的   
  • 有发送过验证码, 但是超过了5分钟过期限制
  • 有发送过验证码, 但是用户输错了
  • 如果发送了多条验证码, 应该只核对用户收到的最新的那个验证码

但是有一个问题, 传过来的code是我们Userprofile中没有的字段, 怎么使用ModelSerializer时将code验证并过滤掉?  答:先增加一个code字段, 然后使用全局钩子删掉code这个字段

注意: 上面这个页面, Vue前端post过来的手机号码放在username属性里的,  所以我们在取手机号码的时候, 应该用来self.initial_data["username"]取, 然后我们自己将其复制到mobile属性中去

这里我们还需要学会自定义错误提示信息 error_messages

虽然在发送验证码阶段, 已经验证了手机号码是否存在,但在这里由于前端post的是用户名,所以也要对用户名进行去重验证 UniqueValidator,校验是否已经存在

 

 

第二步: 写代码

1.首先写UserSerializer, 专门来处理code的验证, 和Username去重的验证

from rest_framework import serializers
from django.contrib.auth import get_user_model
from rest_framework.validators import UniqueValidator  # 给username字段做查重用

User = get_user_model()  # 可以获取数据库的userprofile表
# 验证码校验
class UserRegSerializer(serializers.ModelSerializer):
    # 由于userprofile并没有code字段,所以先加进来
    code = serializers.CharField(required=True, max_length=4, min_length=4, label="短信验证码",
                                 # 自定义的错误提示信息
                                 error_messages={
                                     "blank":"你的验证码哪儿去了",  # blank针对的是有字段名,但没有字段值
                                     "required":"请输入验证码",  # required针对的是连字段名都没有
                                     "max_length":"验证码长度错误",
                                     "min_length":"验证码长度错误",
                                 },
                                 help_text="验证码")

    # 虽然在发送验证码阶段, 已经验证了手机号码是否存在,但在这里由于前端post的是用户名,所以也要对用户名进行验证,校验是否已经存在
    username = serializers.CharField(required=True,allow_blank=False, label="用户名",
                                     validators=[UniqueValidator(queryset=User.objects.all(),message="用户已经存在")])

    # style可以将密码改成密文格式显示
    password = serializers.CharField(
        style={'input_type': 'password'},
        label="密码"
    )

    # 局部钩子, 只对code进行校验
    def validate_code(self, code):
        # POST过来的数据封装在self.initial_data里, 且传递过来手机号的key是username
        # 一定要按时间排序,因为我们只会取最后一条code来验证, order_by默认是升序
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")

        # 如果验证码存在
        if verify_records:
            # 取最新的那一条验证码
            last_records = verify_records[0]

            # 验证码有效期为5min
            five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            if five_minute_ago > last_records.add_time:
                raise serializers.ValidationError("验证码过期")

            # 验证码核对
            if last_records.code != code:
                raise serializers.ValidationError("验证码错误")

        # 如果验证码不存在, 则直接报错
        else:
            raise serializers.ValidationError("验证码错误")

    # 全局钩子, 目的是过滤掉code字段(code其实只用于验证,而不用存在userprofile表中)
    # attrs是局部钩子清洗后的所有字段的dict
    def validate(self, attrs):
        attrs["mobile"] = attrs["username"]
        del attrs["code"]
        return attrs

    class Meta:
        model = User
        fields = ("username", "code", "mobile","password")

然后是views.UserViewset

# 用户注册逻辑
class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
    serializer_class=UserRegSerializer

别忘记配置url

# 用户注册接口(包含code验证,username去重验证)
router.register(r'users', UserViewset, basename="users")

 

 2. 最后去浏览器验证

 

  

 

第三步: 'UserProfile' object has no attribute 'code' 异常处理

1.  现在我们人为往数据库中添加一个手机号和验证码(省钱), 然后尝试着去POST, 最后的结果就是提示userprofile表没有code字段, 前面我们明明已经做的那么好了, 甚至还刻意删掉了code字段, 为什么还是报错???

  

 

2. 主要问题出在REST自带的 CreateModelMixin 上, 见下图

 

 3. 解决办法就是给code加一个字段属性, write_only=True,       补充:关于REST的serializers fields  https://www.django-rest-framework.org/api-guide/fields/

code = serializers.CharField(...
                                 write_only=True,  # 有了这个就解决了'UserProfile' object has no attribute 'code' 的问题
                                                   # 原理就是执行serializer.data时,该字段不会被序列化
...)

4.先去数据库把18572355522这个用户删掉, 再把验证码时间改成现在, 再次测试是否能成功注册, POST页面如下

 

如果后台逻辑没有问题, 那么你提交的什么字段, 就应该返回什么字段, 见下图, 说明成功了

  

5. 但这个时候又有2个新的问题, 一就是我们不想让密码返回, 二是数据库中居然也存的是明文的密码解决办法见下

    password = serializers.CharField(
     ...
        write_only=True,  # 让密码不返回
    )

    # 让密码在保存的时候加密 (拦截create方法, 在其屁股后面加上密码加密)
    def create(self, validated_data):
        user = super(UserRegSerializer, self).create(validated_data=validated_data)
        user.set_password(validated_data["password"])
        user.save()
        return user

 问题完美解决了!

 

6. 提问: 难道只有重写create才能达到这个目的吗??? No, 这里就引出django信号量的问题    详见: https://yiyibooks.cn/xx/Django_1.11.6/topics/signals.html

 最常用的是 django.db.models.signals 即在执行数据库操作的时候,Model在实例化前后, 保存前后, 删除前后, 会向全局发送信号, 此时, 我们可以利用这个信号实现功能的补充, 这样做的优点是可移植性非常高.

 该项目中, 只会用到 post_save(), REST中的  Authentication 模块有讲解这个信号量怎么用

除了django内置的信号函数, 我们也可以自定义信号函数,不过一定要记得要将信号发送出去, (内置的信号会自动发送)

 新建signals.py

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()  # 可以获取数据库的userprofile表


@receiver(post_save, sender=User)  # sender即我们接收哪个Model传递过来的
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:  # created即只有当检测到是新建用户时, 才执行以下逻辑
        password = instance.password
        instance.set_password(password)
        instance.save()
        # Token.objects.create(user=instance) # 我们已经使用了JWT模式, 就无须再创建Token

千万别忘记还有一个app的设置users.apps.py

from django.apps import AppConfig


class UsersConfig(AppConfig):
    name = 'users'
    verbose_name = "用户管理"

    def ready(self):
        import users.signals

 最后去浏览器POST测试, 信号检测成功, 密码也加密成功

 

 

 

第四步: 前端Vue的注册功能联调

1.  现在分析前端的源码, 前端把手机号码以username形式传过来的, 这和我们前面说的一致, 此外可以看见前端一旦得到username和token就会立即以登录的状态跳转到首页去

 

2. 要想在后端生成token, 唯一的办法就是重载这个ViewSet的create函数, 但这里我们首要的任务是分析JWT是怎么样产生token的

通过下面的源码可以看到, 生成token的两个函数都封装在jwt框架中的util中, 所以我们只需要把user传给他们, 再接收他们的返回值就可以了

 

 

3.重写create, 和 perform_create 

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

# 用户注册逻辑
class UserViewset(CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = UserRegSerializer

    # 重写create函数,使得前端注册成功后,后端可以返回该用户的token,好让前端直接处于一个登陆的状态
    # 同时,要明白后端是如何根据user生成token的
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)

        # 给serializer.data封装上token字段,返给前端用的
        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)

    # 源码中的该函数并没有返回值,而这里我们需要其返回user
    def perform_create(self, serializer):
        return serializer.save() # 知识点: serializer.save()返回的就是它对应的实例,这里即user

 

4. 前端测试, 可见注册接口没有问题了

 

 5. 疑问,我们知道JWT插件允许的格式是 "JWT ###.xxx.$$$", 前端只拿到token是怎么封装的? 见下图, api.index.js 已经把每一个请求都封装上了JWT认证需要的头

 

 

 

 

 

 

 

 

---  君子处其实,不处其华;治其内,不治其外   张居正  ----

推荐阅读