欢迎访问我的博客专题
源码可访问 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.pngHTTP 400 Bad Request
,传递过来的参数有问题,drf自动设置好状态码了。
{
"mobile": [
"手机号码不正确"
]
}
返回格式和Django Form中是一样的,如果哪个字段验证错误,就会在里面提示。前部分是字段名称,后部分是一个数字,告诉这个字段有哪些错误。
image.png如果输入一个符合格式的手机号,那么就进入发送验证码逻辑。同时在数据库中也会保存该验证码的内容。
image.png用户serializer和validator验证登录字段
image.pngRestful API实际上是对资源操作,这儿的资源就是用户,实际上就是将用户的数据POST到用户数据库。首先就要写一个ViewSet
在注册页面,需要提供手机号码、验证码和密码。而在Django中username
字段为必填字段,所以可以将手机号作为username
,DRF序列化只需要验证username
满足即可,mobile
作为可为空字段,然后作为username
的手机号验证通过后,将手机号填入mobile
中
现在将 UserProfile
中mobile
字段设置可为空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
中,修改完成后执行数据库同步:makemigrations
、migrate
创建用户测试序列化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': '验证码格式不正确',
})
# 。。。。。。
当用户输入验证码长度不正确就会提示验证码格式不正确
但当验证码没输入时却提示的是"该字段不能为空。"
和预期的是不一样的,再进行如下修改,增加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 增加UserRegisterSerializer
中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
:用于查找正在验证值的现有实例。默认为“精确”。
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
这个字段,做序列化的时候就报错
没有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
是拿到这个字段直接保存,并未对其进行加密。真正的密码在保存的过程中需要有一个加密过程。
网友评论