Python编程使用DRF实现一次性验证码OTP
作者:somenzz 发布时间:2021-07-30 00:25:26
一次性验证码,英文是 One Time Password,简写为 OTP,又称动态密码或单次有效密码,是指计算机系统或其他数字设备上只能使用一次的密码,有效期为只有一次登录会话或很短如 1 分钟。OTP 避免了一些静态密码认证相关系的缺点,不容易受到重放攻击,比如常见的注册场景,用户的邮箱或短信会收到一条一次性的激活链接,或者收到一次随机的验证码(只能使用一次),从而验证了邮箱或手机号的有效性。
要实现的功能就是:
1、验证码是 6 位的数字和小写字母的组合。
2、有效期为 5 分钟,第二次发送验证码的必须在 1 分钟之后。
3、如果该邮箱/手机号已经注册,则不能发送注册验证码。
具体的实现逻辑就是:
1、先生成满足条件的验证码。
2、发送前验证,是否上次发送的验证码在 1 分钟之内?是否邮箱已经注册?,如果是,拒绝发送,并提示用户,如果否,发送验证码。
3、验证,是否是 5 分钟之内的验证码,是否正确,如果是,则放行。否则提示用户。
为了验证验证码及其时效,我们需要把发送验证码的时间和对应的邮箱记录下来,那么就需要设计一张表来存储。
class VerifyCode(models.Model):
mobile = models.CharField(max_length=11, verbose_name="手机号", blank=True)
email = models.EmailField(verbose_name="email", blank=True)
code = models.CharField(max_length=8, verbose_name="验证码")
add_time = models.DateTimeField(verbose_name='生成时间', auto_now_add=True)
1、生成验证码
第一个逻辑非常简单,可以直接写出代码:
from random import choice
def generate_code(self):
"""
生成 6 位数验证码,防止破解
:return:
"""
seeds = "1234567890abcdefghijklmnopqrstuvwxyz"
random_str = []
for i in range(6):
random_str.append(choice(seeds))
return "".join(random_str)
2、发送前验证
Django REST framework 框架的 Serializer 可以对 Models 里的每一个字段进行验证,我们直接在里面做填空题即可:
# serializers.py
class VerifyCodeSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
def validate_email(self, email):
"""
验证邮箱是否合法
"""
# 邮箱是否注册
if User.objects.filter(email = email).count():
raise serializers.ValidationError('该邮箱已经注册')
# 验证邮箱号码合法
if not re.match(EMAIL_REGEX, email):
raise serializers.ValidationError('邮箱格式错误')
# 验证码发送频率
one_minute_age = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
if VerifyCode.objects.filter(add_time__gt=one_minute_age, email=email).count():
raise serializers.ValidationError('请一分钟后再次发送')
return email
3、发送验证码
发送验证码,其实就是生成验证码并保存的过程,借助于 Django REST framework 框架的 GenericViewSet 和 CreateModelMixin 即可实现 view 类,代码都有详细的注释,你很容易就看明白:
from rest_framework.response import Response
from rest_framework.views import status
from rest_framework import mixins, viewsets
class VerifyCodeViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin):
"""
发送验证码
"""
permission_classes = [AllowAny] #允许所有人注册
serializer_class = VerifyCodeSerializer #相关的发送前验证逻辑
def generate_code(self):
"""
生成6位数验证码 防止破解
:return:
"""
seeds = "1234567890abcdefghijklmnopqrstuvwxyz"
random_str = []
for i in range(6):
random_str.append(choice(seeds))
return "".join(random_str)
def create(self, request, *args, **kwargs):
# 自定义的 create() 的内容
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) #这一步相当于发送前验证
# 从 validated_data 中获取 mobile
email = serializer.validated_data["email"]
# 随机生成code
code = self.generate_code()
# 发送短信或邮件验证码
sms_status = SendVerifyCode.send_email_code(code=code, to_email_adress=email)
if sms_status == 0:
# 记录日志
return Response({"msg": "邮件发送失败"}, status=status.HTTP_400_BAD_REQUEST)
else:
code_record = VerifyCode(code=code, email=email)
# 保存验证码
code_record.save()
return Response(
{"msg": f"验证码已经向 {email} 发送完成"}, status=status.HTTP_201_CREATED
)
SendVerifyCode.send_email_code 的实现如下:
#encoding=utf-8
from django.core.mail import send_mail
class SendVerifyCode(object):
@staticmethod
def send_email_code(code,to_email_adress):
try:
success_num = send_mail(subject='xxx 系统验码', message=f'您的验证码是【[code]】。如非本人操作,请忽略。',from_email='xxxx@163.com',recipient_list = [to_email_adress], fail_silently=False)
return success_num
except:
return 0
4、注册时验证
用户注册对于数据库来讲就是 User 类插入一条记录,也就是 User 的 view 类的 create 操作来实现注册。
from .serializers import UserRegisterSerializer, UserSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
def get_serializer_class(self):
if self.action == "create":
# 如果是创建用户,那么用 UserRegisterSerializer
serializer_class = UserRegisterSerializer
else:
serializer_class = UserSerializer
return serializer_class
这个骨架好了以后,我们现在来编写 UserRegisterSerializer 类,实现注册时验证:
# serializers.py
class UserRegisterSerializer(serializers.ModelSerializer):
# error_message:自定义错误消息提示的格式
code = serializers.CharField(required=True, allow_blank=False, min_length=6, max_length=6, help_text='验证码',
error_messages={
'blank': '请输入验证码',
'required': '请输入验证码',
'min_length': '验证码格式错误',
'max_length': '验证码格式错误',
}, write_only=True)
# 利用drf中的validators验证username是否唯一
username = serializers.CharField(required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message='用户已经存在')])
email = serializers.EmailField(required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message='邮箱已被注册')])
# 对code字段单独验证(validate_+字段名)
def validate_code(self, code):
verify_records = VerifyCode.objects.filter(email=self.initial_data['email']).order_by('-add_time')
if verify_records:
last_record = verify_records[0]
# 判断验证码是否过期
five_minutes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) # 获取5分钟之前的时间
if last_record.add_time < five_minutes_ago:
raise serializers.ValidationError('验证码过期')
# 判断验证码是否正确
if last_record.code != code:
raise serializers.ValidationError('验证码错误')
# 不用将code返回到数据库中,只是做验证
# return code
else:
raise serializers.ValidationError('验证码不存在')
# attrs:每个字段validate之后总的dict
def validate(self, attrs):
# attrs['mobile'] = attrs['username']
# 从attrs中删除code字段
del attrs['code']
return attrs
class Meta:
model = User
fields = ('username', 'email', 'password', 'code')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
至此发送验证码的后端编码已经结束。
最后的话
一次性验证码(OTP)的逻辑简单,需要思考的是如何在 DRF 的框架中填空,填在哪里?这其实需要了解 DRF 的 ModelSerializer 类和 ViewSet 类之前的关系,在调用关系上,ViewSet 类调用 ModelSerializer 来实现字段的验证和数据保存及序列化,Serializers 类不是必须的,你可以完全自己实现验证和数据保存及序列化,只不过这样会导致 View 类特别臃肿,不够优雅,不易维护。
参考资料
[1]
Django REST framework: https://www.django-rest-framework.org
来源:https://blog.csdn.net/somenzz/article/details/120072996


猜你喜欢
- 引用是什么在 PHP 中引用意味着用不同的名字访问同一个变量内容。这并不像 C 的指针,替代的是,引用是符号表别名。注意在 PHP 中,变量
- 本文实例讲述了python实现比较两段文本不同之处的方法。分享给大家供大家参考。具体实现方法如下:# find the difference
- mysql误删数据使用delete语句误删数据行使用drop table或者truncate table误删数据表使用drop databa
- 前言SQL模式影响MySQL支持的SQL语法和执行的数据验证检查。MySQL服务器可以在不同的SQL模式下运行,并且可以针对不同的客户端以不
- 前言:array.map() 是一个非常有用的映射函数:它接收一个数组和一个映射函数,然后返回一个新的映射数组。然而,有一个替代 array
- 一、Mysql锁是什么?锁有哪些类别?锁定义: 同一时间同一资源只能被一个线程访问  
- 内容摘要:网页设计师制作网页最常用的设计软件应该就算adobe的产品Photoshop了,当然Photoshop不仅可以设计网页,不过作为网
- PDO::beginTransactionPDO::beginTransaction 启动一个事务(PHP 5 >= 5.1.0, P
- 偶然从pytorch讨论论坛中看到的一个问题,KL divergence different results from tf,kl dive
- 写在前面额、、、最近开始学习机器学习嘛,网上找到一本关于机器学习的书籍,名字叫做《机器学习实战》。很巧的是,这本书里的算法是用Python语
- 目录urllib库作用Urllib 库下的几种模块的基本使用一、urllib.request模块1.功能2.常用方法参数说明:总结urlli
- 创建df:>>> df = pd.DataFrame(np.arange(16).reshape(4, 4), colum
- 写下这篇博客,起源于Tornado邮件群组的这个问题how to use outer variable in inner method,这里
- JavaScript 中的 this 指向问题有很多文章在解释,仍然有很多人问。上周我们的开发团队连续两个人遇到相关问题,所以我不得不将关于
- 当使用桌面应用程序的时候,有没有那么一瞬间,想学习一下桌面应用程序开发?建议此次课程大家稍作了解不要浪费太多时间,因为没有哪家公司会招聘以为
- 应用场景这段代码可以用于修改Excel文件的元数据,例如作者、主题、描述等,通过使用Python和Openpyxl模块,以及wxPython
- 做了一个网站,放到线上,用微信打开,点击分享,可是分享后发给朋友的链接卡片是微信默认自带的,如下: 这标题,描述以及图片是默认自带
- 在使用python做大数据和机器学习处理过程中,首先需要读取hdfs数据,对于常用格式数据一般比较容易读取,parquet略微特殊。从hdf
- MySQL分区方便了我们的使用,但是MySQL分区究竟能做些什么,MySQL分区有没有什么限制呢?阅读下文,您就能找到答案。MySQL分区能
- DATE_FORMA T(date, format) 根据格式串format 格式化日期或日期和时间值date,返回结果串。可用DATE_F