您现在的位置是: 网站首页 >Django >Vue+Django REST framework前后端分离生鲜电商 Django

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

admin2019年5月4日 22:16 Django | Vue 1817人已围观

Vue+Django REST framework前后端分离生鲜电商简介 Vue+Django REST framework 打造前后端分离的生鲜电商项目(慕课网视频)。 Github地址:https://github.com/xyliurui/DjangoOnlineFreshSupermarket ; Django版本:2.2、djangorestframework:3.9.2。 前端Vue模板可以直接联系我拿。

### 发送短信验证码 ![BLOG_20190530_143256_14](/media/blog/images/2019/05/BLOG_20190530_143256_14.png "博客图集BLOG_20190530_143256_14.png") 需要发送短信验证码,后端给一个发送短信的接口才能使用。使用第三方短信发送服务,参考 https://www.yunpian.com/doc/zh_CN/introduction/demos/python.html 。 #### 发送验证码函数 在项目下创建 `utils` 包,然后创建 user_op.py 文件,用于放置发送短信的方法模拟(没实际使用) ```python # 这个用于模拟短信发送,直接在后端输出显示验证码内容 def send_sms(mobile, code): """ 调用短信服务商API发送短信逻辑 :param mobile: :param code: :return: """ print('\n\n【生鲜电商】你的验证码为:{}\n\n'.format(code)) ``` #### 序列化类VerifyCodeSerializer验证手机号 在 users应用下创建 serializers.py 文件,创建验证码序列化类,这个和Django的Form几乎是一样的用法。 ```python 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 增加发送验证码视图 ```python 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/ ![BLOG_20190530_143242_89](/media/blog/images/2019/05/BLOG_20190530_143242_89.png "博客图集BLOG_20190530_143242_89.png") 假如输入一个不正确的手机号码 ![BLOG_20190530_143232_14](/media/blog/images/2019/05/BLOG_20190530_143232_14.png "博客图集BLOG_20190530_143232_14.png") `HTTP 400 Bad Request`,传递过来的参数有问题,drf自动设置好状态码了。 ```json { "mobile": [ "手机号码不正确" ] } ``` 返回格式和Django Form中是一样的,如果哪个字段验证错误,就会在里面提示。前部分是字段名称,后部分是一个数字,告诉这个字段有哪些错误。 ![BLOG_20190530_143221_11](/media/blog/images/2019/05/BLOG_20190530_143221_11.png "博客图集BLOG_20190530_143221_11.png") 如果输入一个符合格式的手机号,那么就进入发送验证码逻辑。同时在数据库中也会保存该验证码的内容。 ![BLOG_20190530_143214_39](/media/blog/images/2019/05/BLOG_20190530_143214_39.png "博客图集BLOG_20190530_143214_39.png") ### 用户serializer和validator验证登录字段 ![BLOG_20190530_143207_26](/media/blog/images/2019/05/BLOG_20190530_143207_26.png "博客图集BLOG_20190530_143207_26.png") Restful API实际上是对资源操作,这儿的资源就是用户,实际上就是将用户的数据POST到用户数据库。首先就要写一个ViewSet 在注册页面,需要提供手机号码、验证码和密码。而在Django中`username`字段为必填字段,所以可以将手机号作为`username`,DRF序列化只需要验证`username`满足即可,`mobile`作为可为空字段,然后作为`username`的手机号验证通过后,将手机号填入`mobile`中 现在将 `UserProfile` 中`mobile`字段设置可为空`blank=True, null=True`,如果不这样设置,后端做验证的时候就会提示`mobile`这个字段必填。 ```python 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`中,修改完成后执行数据库同步:`makemigrations`、`migrate` #### 创建用户测试序列化UserSerializer 在 users/serializers.py 中增加 ```python class UserSerializer(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 中增加下面类 ```python from .serializers import VerifyCodeSerializer, UserSerializer class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ 创建用户 """ serializer_class = UserSerializer ``` #### 增加用户注册URL 编辑主 urls.py 增加注册的url ```python from users.views import SendSmsCodeViewSet, UserViewSet router.register(r'users', UserViewSet, base_name='users') # 用户注册 ``` #### API测试,验证code 现在,可以访问 http://127.0.0.1:8000/users/ 测试 ![BLOG_20190530_143152_83](/media/blog/images/2019/05/BLOG_20190530_143152_83.png "博客图集BLOG_20190530_143152_83.png") 当用户输入一个错误的验证码时,就会出现以上错误,默认提供的验证,例如`"请确保这个字段至少包含 4 个字符。"`,如果自定义这些错误? 现在修改`UserSerializer` ```python class UserSerializer(serializers.ModelSerializer): code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码', error_messages={ 'required': '该字段必填项', 'min_length': '验证码格式不正确', 'max_length': '验证码格式不正确', }) # 。。。。。。 ``` 当用户输入验证码长度不正确就会提示`验证码格式不正确` 但当验证码没输入时却提示的是`"该字段不能为空。"` ![BLOG_20190530_143143_10](/media/blog/images/2019/05/BLOG_20190530_143143_10.png "博客图集BLOG_20190530_143143_10.png") 和预期的是不一样的,再进行如下修改,增加`blank`验证 ```python class UserSerializer(serializers.ModelSerializer): code = serializers.CharField(required=True, min_length=4, max_length=4, help_text='验证码', label='验证码', error_messages={ 'blank': '请输入验证码', 'required': '该字段必填项', 'min_length': '验证码格式不正确', 'max_length': '验证码格式不正确', }) ``` ![BLOG_20190530_143133_29](/media/blog/images/2019/05/BLOG_20190530_143133_29.png "博客图集BLOG_20190530_143133_29.png") 现在当验证码没有输入时,就会提示`"请输入验证码"` #### 注册时验证username字段 修改 users/serializers.py 增加`UserSerializer`中`username`验证 可以访问 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`:用于查找正在验证值的现有实例。默认为“精确”。 ```python from rest_framework.validators import UniqueValidator class UserSerializer(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 在用户注册的视图中 ```python class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ 创建用户 """ serializer_class = UserSerializer ``` `serializer`是使用的`ModelSerializer`,这里面不用加功能了,只需要加上`queryset`即可 ```python class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ 创建用户 """ serializer_class = UserSerializer queryset = User.objects.all() # 实际测试好像不加也可以完成注册,可以测试下 ``` 这样用户注册的功能就可以完成了。 #### API测试,验证username ![BLOG_20190530_143117_76](/media/blog/images/2019/05/BLOG_20190530_143117_76.png "博客图集BLOG_20190530_143117_76.png") 输入一个已存在的用户名,则会提示`用户已存在`。 使用一个符合规则的手机号做用户名,符合规则的验证码填入提交 ![BLOG_20190530_143110_81](/media/blog/images/2019/05/BLOG_20190530_143110_81.png "博客图集BLOG_20190530_143110_81.png") #### 保存用户信息是序列化code报错解决 ```error AttributeError: 'UserProfile' object has no attribute 'code' # ... AttributeError: Got AttributeError when attempting to get a value for field `code` on serializer `UserSerializer`. 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`这个字段,做序列化的时候就报错 ![BLOG_20190530_143100_98](/media/blog/images/2019/05/BLOG_20190530_143100_98.png "博客图集BLOG_20190530_143100_98.png") 没有`password`字段,将其添加到序列化类中 ```python class UserSerializer(serializers.ModelSerializer): # ......省略 class Meta: model = User fields = ('username', 'mobile', 'code', 'password') # username是Django自带的字段,与mobile的值保持一致 ``` 是实际上与`password`这和字段无关 原因分析: `UserViewSet`继承了`mixins.CreateModelMixin`,也就是下面的代码 ```python class CreateModelMixin(object): """ Create a model instance. """ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) # 获取在users中配置的serializers的UserSerializer,类似于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就会将数据按照UserSerializer中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 中的`UserSerializer`,为`code`字段添加`write_only=True`参数。 ```pytohn class UserSerializer(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/users/ 页面上就增加了一个密码字段 ![BLOG_20190530_143050_56](/media/blog/images/2019/05/BLOG_20190530_143050_56.png "博客图集BLOG_20190530_143050_56.png") 但是这个秘密是明文显示的。这儿就需要用到一个`style`字段。 #### 增加password字段style参数隐藏密码显示 参考 https://www.django-rest-framework.org/api-guide/fields/#style 一个键值对字典,可用于控制呈现器应如何呈现字段。例如这些的密码想要不显示,则进行如下配置,在`UserSerializer`中增加`password`字段,并配置它的`style`。 ```python class UserSerializer(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/users/ 可以看到密码已经隐藏了。 ![BLOG_20190530_143038_13](/media/blog/images/2019/05/BLOG_20190530_143038_13.png "博客图集BLOG_20190530_143038_13.png") 再来测试添加一个验证码后注册用户 ![BLOG_20190530_143029_27](/media/blog/images/2019/05/BLOG_20190530_143029_27.png "博客图集BLOG_20190530_143029_27.png") 输入用户名、验证码和密码后POST,就可以在上方看到返回的信息 ![BLOG_20190530_143022_54](/media/blog/images/2019/05/BLOG_20190530_143022_54.png "博客图集BLOG_20190530_143022_54.png") 状态码`HTTP 201 Created` #### 序列化password字段增加write_only参数 但是上方`password`字段也被显示出来了,这显然是不合理的,所以也需要将`password`添加 ```python class UserSerializer(serializers.ModelSerializer): # ...省略其他代码 password = serializers.CharField(required=True, help_text='密码', label='密码', write_only=True, style={'input_type': 'password'}) # ...省略其他代码 ``` 这样就不会返回该字段了,也就是序列化时不包含该字段。 查看数据库刚添加的用户。 ![BLOG_20190530_143014_56](/media/blog/images/2019/05/BLOG_20190530_143014_56.png "博客图集BLOG_20190530_143014_56.png") 密码是明文,而不是密码(无法反解的),这样是不正确的。因为`UserSerializer`是拿到这个字段直接保存,并未对其进行加密。真正的密码在保存的过程中需要有一个加密过程。

很赞哦! (1)

文章交流

  • emoji
0人参与,0条评论

当前用户

未登录,点击   登录

站点信息

  • 建站时间:网站已运行2285天
  • 系统信息:Linux
  • 后台程序:Python: 3.8.10
  • 网站框架:Django: 3.2.6
  • 文章统计:257 篇
  • 文章评论:63 条
  • 腾讯分析网站概况-腾讯分析
  • 百度统计网站概况-百度统计
  • 公众号:微信扫描二维码,关注我们
  • QQ群:QQ加群,下载网站的学习源码
返回
顶部
标题 换行 登录
网站