美文网首页Vue移动端
【Vue+DRF生鲜电商】14.用户注册发送短信验证码、登录字段

【Vue+DRF生鲜电商】14.用户注册发送短信验证码、登录字段

作者: 吾星喵 | 来源:发表于2019-05-04 22:22 被阅读23次

    欢迎访问我的博客专题

    源码可访问 Github 查看

    发送短信验证码

    image.png

    需要发送短信验证码,后端给一个发送短信的接口才能使用。使用第三方短信发送服务,参考 https://www.yunpian.com/doc/zh_CN/introduction/demos/python.html

    发送验证码函数

    在项目下创建 utils 包,然后创建 user_op.py 文件,用于放置发送短信的方法模拟(没实际使用)

    # 这个用于模拟短信发送,直接在后端输出显示验证码内容
    
    
    def send_sms(mobile, code):
        """
        调用短信服务商API发送短信逻辑
        :param mobile:
        :param code:
        :return:
        """
        print('\n\n【生鲜电商】你的验证码为:{}\n\n'.format(code))
    

    序列化类VerifyCodeSerializer验证手机号

    在 users应用下创建 serializers.py 文件,创建验证码序列化类,这个和Django的Form几乎是一样的用法。

    import re
    from django.utils.timezone import now
    from datetime import timedelta
    from django.contrib.auth import get_user_model
    from rest_framework import serializers
    from users.models import VerifyCode
    
    User = get_user_model()
    
    
    class VerifyCodeSerializer(serializers.Serializer):
        """"
        不用ModelSerializer原因:发送验证码只需要提交手机号码
        """
        mobile = serializers.CharField(max_length=11, help_text='手机号码', label='手机号码')
    
        def validate_mobile(self, mobile):
            """
            验证手机号码
            :param mobile:
            :return:
            """
            # 是否已注册
            if User.objects.filter(mobile=mobile):
                raise serializers.ValidationError('用户已存在')
    
            # 正则验证手机号码
            regexp = "^[1][3,4,5,7,8][0-9]{9}$"
            if not re.match(regexp, mobile):
                raise serializers.ValidationError('手机号码不正确')
    
            # 验证发送频率
            one_minute_ago = now() - timedelta(hours=0, minutes=1, seconds=0)  # 获取一分钟以前的时间
            # print(one_minute_ago)
            if VerifyCode.objects.filter(add_time__gt=one_minute_ago, mobile=mobile):
                # 如果添加时间大于一分钟以前的时间,则在这一分钟内已经发过短信,不允许再次发送
                raise serializers.ValidationError('距离上次发送未超过60s')
    
            return mobile
    
    

    新增视图生成验证码发送

    修改 users/views.py 增加发送验证码视图

    from django.contrib.auth import get_user_model
    from django.db.models import Q
    from random import choice
    from django.contrib.auth.backends import ModelBackend
    from rest_framework import mixins, viewsets, status
    from rest_framework.response import Response
    from .serializers import VerifyCodeSerializer
    from utils.user_op import send_sms
    from .models import VerifyCode
    
    User = get_user_model()
    
    
    
    class SendSmsCodeViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
        """
        发送短信验证码
        """
        serializer_class = VerifyCodeSerializer
    
        def generate_code(self):
            # 定义一个种子,从这里面随机拿出一个值,可以是字母
            seeds = "1234567890"
            # 定义一个空列表,每次循环,将拿到的值,加入列表
            random_str = []
            # choice函数:每次从seeds拿一个值,加入列表
            for i in range(4):
                # 将列表里的值,变成四位字符串
                random_str.append(choice(seeds))
            return ''.join(random_str)
    
        # 直接复制CreateModelMixin中的create方法进行重写
        def create(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            # raise_exception=True表示is_valid验证失败,就直接抛出异常,被drf捕捉到,直接会返回400错误,不会往下执行
    
            mobile = serializer.validated_data['mobile']  # 直接取mobile,上方无异常,那么mobile字段肯定是有的
    
            # 生成验证码
            code = self.generate_code()
            sendsms = send_sms(mobile=mobile, code=code)  # 模拟发送短信
    
            if sendsms.get('status_code') != 0:
                return Response({
                    'mobile': sendsms['msg']
                }, status=status.HTTP_400_BAD_REQUEST)
            else:
                # 在短信发送成功之后保存验证码
                code_record = VerifyCode(mobile=mobile, code=code)
                code_record.save()
    
                return Response({
                    'mobile': mobile
                }, status=status.HTTP_201_CREATED)  # 可以创建成功代码为201
    
            # 以下就不需要了
            # self.perform_create(serializer)
            # headers = self.get_success_headers(serializer.data)
            # return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
    

    测试发送验证码接口

    在网页上访问 http://127.0.0.1:8000/code/

    image.png

    假如输入一个不正确的手机号码

    image.png

    HTTP 400 Bad Request,传递过来的参数有问题,drf自动设置好状态码了。

    {
        "mobile": [
            "手机号码不正确"
        ]
    }
    

    返回格式和Django Form中是一样的,如果哪个字段验证错误,就会在里面提示。前部分是字段名称,后部分是一个数字,告诉这个字段有哪些错误。

    image.png

    如果输入一个符合格式的手机号,那么就进入发送验证码逻辑。同时在数据库中也会保存该验证码的内容。

    image.png

    用户serializer和validator验证登录字段

    image.png

    Restful API实际上是对资源操作,这儿的资源就是用户,实际上就是将用户的数据POST到用户数据库。首先就要写一个ViewSet

    在注册页面,需要提供手机号码、验证码和密码。而在Django中username字段为必填字段,所以可以将手机号作为username,DRF序列化只需要验证username满足即可,mobile作为可为空字段,然后作为username的手机号验证通过后,将手机号填入mobile

    现在将 UserProfilemobile字段设置可为空blank=True, null=True,如果不这样设置,后端做验证的时候就会提示mobile这个字段必填。

    class UserProfile(AbstractUser):
        """
        扩展用户,需要在settings设置认证model
        """
        name = models.CharField(max_length=30, blank=True, null=True, verbose_name='姓名', help_text='姓名')
        birthday = models.DateField(null=True, blank=True, verbose_name='出生年月', help_text='出生年月')
        mobile = models.CharField(max_length=11, blank=True, null=True, verbose_name='电话', help_text='电话')
        gender = models.CharField(max_length=6, choices=(('male', '男'), ('female', '女')), default='male', verbose_name='性别', help_text='性别')
    

    这样可以自定义添加,将传入的username字段的内容直接填充到mobile中,修改完成后执行数据库同步:makemigrationsmigrate

    创建用户测试序列化UserRegisterSerializer

    在 users/serializers.py 中增加

    class UserRegisterSerializer(serializers.ModelSerializer):
        code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码')
    
        def validate_code(self, code):
            # self.initial_data 为用户前端传过来的所有值
            verify_codes = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time')
            if verify_codes:
                last_record = verify_codes[0]
    
                # 发送验证码如果超过某个时间就提示过期
                three_minute_ago = now() - timedelta(hours=0, minutes=3, seconds=0)  # 获取三分钟以前的时间
                if last_record.add_time < three_minute_ago:
                    #            3ago             now
                    #      add1          add2            add1就过期
                    raise serializers.ValidationError('验证码已过期')
    
                # 比较传入的验证码
                if last_record.code != code:
                    raise serializers.ValidationError('验证码输入错误')
                # return code
                # 这没必要return,因为code这个字段只是用来验证的,不是用来保存到数据库中的
    
            else:
                # 没有查到该手机号对应的验证码
                raise serializers.ValidationError('验证码错误')
    
        def validate(self, attrs):
            """
            code 这个字段是不需要保存数据库的,不需要改字段
            validate这个函数作用于所有的字段之上
            :param attrs: 每个字段validate之后返回的一个总的dict
            :return:
            """
            attrs['mobile'] = attrs['username']  # mobile不需要前端传过来,就直接后台取username中的值填充
            del attrs['code']  # 删除不需要的code字段
            return attrs
    
        class Meta:
            model = User
            fields = ('username', 'mobile', 'code')  # username是Django自带的字段,与mobile的值保持一致
    

    这个Serializer中直接继承ModelSerializer,并添加code这个字段,用于验证验证码是否正确,如果正确,则在validate(self, attrs)函数中删除该字段的键值,并把username赋值给mobile

    创建用户注册视图

    在 users/views.py 中增加下面类

    from .serializers import VerifyCodeSerializer, UserRegisterSerializer
    
    
    class UserRegisterViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
        """
        创建用户
        """
        serializer_class = UserRegisterSerializer
    

    增加用户注册URL

    编辑主 urls.py 增加注册的url

    from users.views import SendSmsCodeViewSet, UserRegisterViewSet
    
    
    router.register(r'register', UserRegisterViewSet, base_name='register')  # 用户注册
    

    API测试,验证code

    现在,可以访问 http://127.0.0.1:8000/register/ 测试

    image.png

    当用户输入一个错误的验证码时,就会出现以上错误,默认提供的验证,例如"请确保这个字段至少包含 4 个字符。",如果自定义这些错误?

    现在修改UserRegisterSerializer

    class UserRegisterSerializer(serializers.ModelSerializer):
        code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码',
                                     error_messages={
                                         'required': '该字段必填项',
                                         'min_length': '验证码格式不正确',
                                         'max_length': '验证码格式不正确',
                                     })
        # 。。。。。。
    

    当用户输入验证码长度不正确就会提示验证码格式不正确

    但当验证码没输入时却提示的是"该字段不能为空。"

    image.png

    和预期的是不一样的,再进行如下修改,增加blank验证

    class UserRegisterSerializer(serializers.ModelSerializer):
        code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码',
                                     error_messages={
                                         'blank': '请输入验证码',
                                         'required': '该字段必填项',
                                         'min_length': '验证码格式不正确',
                                         'max_length': '验证码格式不正确',
                                     })
    
    image.png

    现在当验证码没有输入时,就会提示"请输入验证码"

    注册时验证username字段

    修改 users/serializers.py 增加UserRegisterSerializerusername验证

    可以访问 https://www.django-rest-framework.org/api-guide/validators/ 查看DRF验证机制

    大多数时候,在REST框架中处理验证时,只需要依赖默认字段验证,或者在序列化器或字段类上编写显式验证方法。

    在这验证username唯一性,可参考 https://www.django-rest-framework.org/api-guide/validators/#uniquevalidator 进行

    此验证器可用于对模型字段强制unique=True约束。它接受一个必需的参数和一个可选的消息参数:

    • queryset:必须的,这是应该强制惟一性的queryset
    • message:验证失败时应该使用的错误消息。
    • lookup:用于查找正在验证值的现有实例。默认为“精确”。
    from rest_framework.validators import UniqueValidator
    
    
    class UserRegisterSerializer(serializers.ModelSerializer):
        code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码',
                                     error_messages={
                                         'blank': '请输入验证码',
                                         'required': '该字段必填项',
                                         'min_length': '验证码格式不正确',
                                         'max_length': '验证码格式不正确',
                                     })
        username = serializers.CharField(required=True, allow_blank=False,
                                         help_text='用户名',
                                         label='用户名',
                                         validators=[UniqueValidator(queryset=User.objects.all(), message='用户已存在')])
    
        def validate_code(self, code):
            # 验证code
            # self.initial_data 为用户前端传过来的所有值
            verify_codes = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time')
            if verify_codes:
                last_record = verify_codes[0]
    
                # 发送验证码如果超过某个时间就提示过期
                three_minute_ago = now() - timedelta(hours=0, minutes=3, seconds=0)  # 获取三分钟以前的时间
                if last_record.add_time < three_minute_ago:
                    #            3ago             now
                    #      add1          add2            add1就过期
                    raise serializers.ValidationError('验证码已过期')
    
                # 比较传入的验证码
                if last_record.code != code:
                    raise serializers.ValidationError('验证码输入错误')
                # return code
                # 这没必要return,因为code这个字段只是用来验证的,不是用来保存到数据库中的
    
            else:
                # 没有查到该手机号对应的验证码
                raise serializers.ValidationError('验证码错误')
    
        def validate(self, attrs):
            """
            code 这个字段是不需要保存数据库的,不需要改字段
            validate这个函数作用于所有的字段之上
            :param attrs: 每个字段validate之后返回的一个总的dict
            :return:
            """
            attrs['mobile'] = attrs['username']  # mobile不需要前端传过来,就直接后台取username中的值填充
            del attrs['code']  # 删除不需要的code字段
            return attrs
    
        class Meta:
            model = User
            fields = ('username', 'mobile', 'code')  # username是Django自带的字段,与mobile的值保持一致
    

    validators=[UniqueValidator(queryset=User.objects.all(), message='用户已存在')]中该字段进行添加时,从User.objects.all()验证唯一性,如果已存在,则提示message中的内容。

    ViewSets中添加queryset

    在用户注册的视图中

    class UserRegisterViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
        """
        创建用户
        """
        serializer_class = UserRegisterSerializer
    

    serializer是使用的ModelSerializer,这里面不用加功能了,只需要加上queryset即可

    class UserRegisterViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
        """
        创建用户
        """
        serializer_class = UserRegisterSerializer
        queryset = User.objects.all()  # 实际测试好像不加也可以完成注册,可以测试下
    

    这样用户注册的功能就可以完成了。

    API测试,验证username

    image.png

    输入一个已存在的用户名,则会提示用户已存在

    使用一个符合规则的手机号做用户名,符合规则的验证码填入提交

    image.png

    保存用户信息是序列化code报错解决

    AttributeError: 'UserProfile' object has no attribute 'code'
    
    # ...
    
    AttributeError: Got AttributeError when attempting to get a value for field `code` on serializer `UserRegisterSerializer`.
    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'.
    

    意思是UserProfile中没有code这个字段,做序列化的时候就报错

    image.png

    没有password字段,将其添加到序列化类中

    class UserRegisterSerializer(serializers.ModelSerializer):
        # ......省略
    
        class Meta:
            model = User
            fields = ('username', 'mobile', 'code', 'password')  # username是Django自带的字段,与mobile的值保持一致
    

    是实际上与password这和字段无关

    原因分析:
    UserRegisterViewSet继承了mixins.CreateModelMixin,也就是下面的代码

    class CreateModelMixin(object):
        """
        Create a model instance.
        """
        def create(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)  # 获取在users中配置的serializers的UserRegisterSerializer,类似于Django的Form
            serializer.is_valid(raise_exception=True)  # 数据做验证
            self.perform_create(serializer)  # 调用Models的Serializer,保存数据库,以上这些不收都是不会报错的
            headers = self.get_success_headers(serializer.data)
            # 下面返回的时候调用了serializer.data,这个serializer.data就会将数据按照UserRegisterSerializer中Meta配置的fields做一个序列化,其中已包含code,但在validate()函数中已经被del掉了,也就是没有这个键,那么就会抛异常
            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
    
        def perform_create(self, serializer):
            serializer.save()
    
        def get_success_headers(self, data):
            try:
                return {'Location': str(data[api_settings.URL_FIELD_NAME])}
            except (TypeError, KeyError):
                return {}
    

    序列化code字段增加write_only参数

    解决上面步骤的问题,访问 https://www.django-rest-framework.org/api-guide/fields/#core-arguments 可以看到他有很多其他的参数。

    其中有个参数叫write_only,这个字段的意思是:将此设置为True,以确保在更新或创建实例时可以使用该字段,但在序列化表示时不包括该字段,默认为False

    修改 users/serializers.py 中的UserRegisterSerializer,为code字段添加write_only=True参数。

    class UserRegisterSerializer(serializers.ModelSerializer):
        code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码',
                                     write_only=True,  # 更新或创建实例时可以使用该字段,但序列化时不包含该字段
                                     error_messages={
                                         'blank': '请输入验证码',
                                         'required': '该字段必填项',
                                         'min_length': '验证码格式不正确',
                                         'max_length': '验证码格式不正确',
                                     })
    
        # ...省略其他代码
    

    将刚才创建的用户删除,再重新测试。

    再次访问 http://127.0.0.1:8000/register/ 页面上就增加了一个密码字段

    image.png

    但是这个秘密是明文显示的。这儿就需要用到一个style字段。

    增加password字段style参数隐藏密码显示

    参考 https://www.django-rest-framework.org/api-guide/fields/#style

    一个键值对字典,可用于控制呈现器应如何呈现字段。例如这些的密码想要不显示,则进行如下配置,在UserRegisterSerializer中增加password字段,并配置它的style

    class UserRegisterSerializer(serializers.ModelSerializer):
        code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码',
                                     write_only=True,  # 更新或创建实例时可以使用该字段,但序列化时不包含该字段
                                     error_messages={
                                         'blank': '请输入验证码',
                                         'required': '该字段必填项',
                                         'min_length': '验证码格式不正确',
                                         'max_length': '验证码格式不正确',
                                     })
        username = serializers.CharField(required=True, allow_blank=False,
                                         help_text='用户名',
                                         label='用户名',
                                         validators=[UniqueValidator(queryset=User.objects.all(), message='用户已存在')])
        password = serializers.CharField(required=True, help_text='密码', label='密码', style={'input_type': 'password'})
    
        # ...省略其他代码
    

    现在访问 http://127.0.0.1:8000/register/ 可以看到密码已经隐藏了。

    image.png

    再来测试添加一个验证码后注册用户

    image.png

    输入用户名、验证码和密码后POST,就可以在上方看到返回的信息

    image.png

    状态码HTTP 201 Created

    序列化password字段增加write_only参数

    但是上方password字段也被显示出来了,这显然是不合理的,所以也需要将password添加

    class UserRegisterSerializer(serializers.ModelSerializer):
        # ...省略其他代码
        password = serializers.CharField(required=True, help_text='密码', label='密码', write_only=True, style={'input_type': 'password'})
        # ...省略其他代码
    

    这样就不会返回该字段了,也就是序列化时不包含该字段。

    查看数据库刚添加的用户。

    image.png

    密码是明文,而不是密码(无法反解的),这样是不正确的。因为UserRegisterSerializer是拿到这个字段直接保存,并未对其进行加密。真正的密码在保存的过程中需要有一个加密过程。

    相关文章

      网友评论

        本文标题:【Vue+DRF生鲜电商】14.用户注册发送短信验证码、登录字段

        本文链接:https://www.haomeiwen.com/subject/tdxqoqtx.html