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

标签:Python,DRF,OTP
0
投稿

猜你喜欢

  • 解决phpmyadmin中缺少mysqli扩展问题的方法

    2023-07-13 02:30:12
  • [译]“我心中的ebay”

    2008-06-04 12:09:00
  • python中format的用法实例详解

    2023-08-03 12:27:28
  • python3.9和pycharm的安装教程并创建简单项目的步骤

    2021-06-18 05:34:16
  • Python math库 ln(x)运算的实现及原理

    2023-09-11 18:12:28
  • python math模块使用方法介绍

    2021-06-20 23:44:01
  • JavaScript实现年历效果

    2023-09-10 10:53:26
  • 详解MySQL数据库中Show命令的用法

    2008-11-27 16:04:00
  • mysql int范围与最大值分析

    2012-03-26 18:24:00
  • 如何列出SQL数据库中的存储过程?

    2010-01-12 19:58:00
  • 零基础学python应该从哪里入手

    2023-04-27 20:44:56
  • 用Python生成会跳舞的美女

    2023-04-04 11:52:09
  • javascript彩虹圈效果

    2011-08-05 19:10:45
  • 30秒学会30个超实用Python代码片段【收藏版】

    2021-08-04 17:13:32
  • SQL Server字符串切割函数

    2012-08-21 10:18:43
  • urllib和BeautifulSoup爬取维基百科的词条简单实例

    2023-10-25 21:46:59
  • JS实现标签滚动切换效果

    2023-08-24 00:35:37
  • ASP实现防止网站被采集代码

    2011-03-25 10:40:00
  • javascript this 关键字小提示

    2009-02-03 13:22:00
  • 使用ACCESS做网络版程序的四种解决方案

    2009-01-14 16:22:00
  • asp之家 网络编程 m.aspxhome.com