Vue移动端

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

2019-05-04  本文已影响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约束。它接受一个必需的参数和一个可选的消息参数:

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是拿到这个字段直接保存,并未对其进行加密。真正的密码在保存的过程中需要有一个加密过程。

上一篇下一篇

猜你喜欢

热点阅读