用 Django REST Framework 来实现一次性验证码(OTP)

时间:2022-09-04 07:58:19

用 Django REST Framework 来实现一次性验证码(OTP)

一次性验证码,英文是 One Time Password,简写为 OTP,又称动态密码或单次有效密码,是指计算机系统或其他数字设备上只能使用一次的密码,有效期为只有一次登录会话或很短如 1 分钟。OTP 避免了一些静态密码认证相关系的缺点,不容易受到重放攻击,比如常见的注册场景,用户的邮箱或短信会收到一条一次性的激活链接,或者收到一次随机的验证码(只能使用一次),从而验证了邮箱或手机号的有效性。

今天讲一下如何用 Django REST framework[1](DRF) 来实现 OTP,阅读本文需要一定的 DRF 的基础知识。

要实现的功能就是:

1、验证码是 6 位的数字和小写字母的组合。

2、有效期为 5 分钟,第二次发送验证码的必须在 1 分钟之后。

3、如果该邮箱/手机号已经注册,则不能发送注册验证码。

具体的实现逻辑就是:

1、先生成满足条件的验证码。

2、发送前验证,是否上次发送的验证码在 1 分钟之内?是否邮箱已经注册?,如果是,拒绝发送,并提示用户,如果否,发送验证码。

3、验证,是否是 5 分钟之内的验证码,是否正确,如果是,则放行。否则提示用户。

为了验证验证码及其时效,我们需要把发送验证码的时间和对应的邮箱记录下来,那么就需要设计一张表来存储。

  1. class VerifyCode(models.Model): 
  2.     mobile = models.CharField(max_length=11, verbose_name="手机号", blank=True
  3.     email = models.EmailField(verbose_name="email", blank=True
  4.     code = models.CharField(max_length=8, verbose_name="验证码"
  5.     add_time = models.DateTimeField(verbose_name='生成时间', auto_now_add=True

1、生成验证码

第一个逻辑非常简单,可以直接写出代码:

  1. from random import choice 
  2.  
  3. def generate_code(self): 
  4.  ""
  5.  生成 6 位数验证码,防止破解 
  6.  :return
  7.  ""
  8.  seeds = "1234567890abcdefghijklmnopqrstuvwxyz" 
  9.  random_str = [] 
  10.  for i in range(6): 
  11.   random_str.append(choice(seeds)) 
  12.  return "".join(random_str) 

2、发送前验证

Django REST framework 框架的 Serializer 可以对 Models 里的每一个字段进行验证,我们直接在里面做填空题即可:

  1. # serializers.py 
  2.  
  3. class VerifyCodeSerializer(serializers.Serializer): 
  4.     email = serializers.EmailField(required=True
  5.  
  6.     def validate_email(self, email): 
  7.         ""
  8.         验证邮箱是否合法 
  9.         ""
  10.         # 邮箱是否注册 
  11.         if User.objects.filter(email = email).count(): 
  12.             raise serializers.ValidationError('该邮箱已经注册'
  13.  
  14.         # 验证邮箱号码合法 
  15.         if not re.match(EMAIL_REGEX, email): 
  16.             raise serializers.ValidationError('邮箱格式错误'
  17.  
  18.         # 验证码发送频率 
  19.         one_minute_age = datetime.now() - timedelta(hours=0, minutes=1, seconds=0) 
  20.         if VerifyCode.objects.filter(add_time__gt=one_minute_age, email=email).count(): 
  21.             raise serializers.ValidationError('请一分钟后再次发送'
  22.         return email 

3、发送验证码

发送验证码,其实就是生成验证码并保存的过程,借助于 Django REST framework 框架的 GenericViewSet 和 CreateModelMixin 即可实现 view 类,代码都有详细的注释,你很容易就看明白:

  1. from rest_framework.response import Response 
  2. from rest_framework.views import status 
  3. from rest_framework import mixins, viewsets 
  4.  
  5.  
  6. class VerifyCodeViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin): 
  7.     ""
  8.     发送验证码 
  9.     ""
  10.     permission_classes = [AllowAny] #允许所有人注册 
  11.     serializer_class = VerifyCodeSerializer #相关的发送前验证逻辑 
  12.  
  13.     def generate_code(self): 
  14.         ""
  15.         生成6位数验证码 防止破解 
  16.         :return
  17.         ""
  18.         seeds = "1234567890abcdefghijklmnopqrstuvwxyz" 
  19.         random_str = [] 
  20.         for i in range(6): 
  21.             random_str.append(choice(seeds)) 
  22.         return "".join(random_str) 
  23.  
  24.     def create(self, request, *args, **kwargs): 
  25.   # 自定义的 create() 的内容 
  26.  
  27.         serializer = self.get_serializer(data=request.data) 
  28.         serializer.is_valid(raise_exception=True) #这一步相当于发送前验证 
  29.  
  30.          
  31.         # 从 validated_data 中获取 mobile 
  32.         email = serializer.validated_data["email"
  33.         # 随机生成code 
  34.         code = self.generate_code() 
  35.  
  36.         # 发送短信或邮件验证码 
  37.         sms_status = SendVerifyCode.send_email_code(code=code, to_email_adress=email) 
  38.  
  39.         if sms_status == 0:             
  40.    # 记录日志 
  41.  
  42.             return Response({"msg""邮件发送失败"}, status=status.HTTP_400_BAD_REQUEST) 
  43.         else
  44.             code_record = VerifyCode(code=code, email=email) 
  45.             # 保存验证码 
  46.             code_record.save()    
  47.             return Response( 
  48.                 {"msg": f"验证码已经向 {email} 发送完成"}, status=status.HTTP_201_CREATED 
  49.             ) 

SendVerifyCode.send_email_code 的实现如下:

  1. #encoding=utf-8 
  2.  
  3. from django.core.mail import send_mail 
  4.  
  5. class SendVerifyCode(object): 
  6.     @staticmethod 
  7.     def send_email_code(code,to_email_adress): 
  8.         try: 
  9.             success_num = send_mail(subject='xxx 系统验码', message=f'您的验证码是【{code}】。如非本人操作,请忽略。',from_email='xxxx@163.com',recipient_list = [to_email_adress], fail_silently=False
  10.             return success_num 
  11.         except
  12.             return 0 

4、注册时验证

用户注册对于数据库来讲就是 User 类插入一条记录,也就是 User 的 view 类的 create 操作来实现注册。

  1. from .serializers import UserRegisterSerializer, UserSerializer 
  2. class UserViewSet(viewsets.ModelViewSet): 
  3.     ""
  4.     API endpoint that allows users to be viewed or edited. 
  5.     ""
  6.     serializer_class = UserSerializer 
  7.  
  8.     def get_serializer_class(self): 
  9.         if self.action == "create"
  10.             # 如果是创建用户,那么用 UserRegisterSerializer 
  11.             serializer_class = UserRegisterSerializer 
  12.         else
  13.             serializer_class = UserSerializer 
  14.         return serializer_class 

这个骨架好了以后,我们现在来编写 UserRegisterSerializer 类,实现注册时验证:

  1. # serializers.py 
  2. class UserRegisterSerializer(serializers.ModelSerializer): 
  3.     # error_message:自定义错误消息提示的格式 
  4.     code = serializers.CharField(required=True, allow_blank=False, min_length=6, max_length=6, help_text='验证码'
  5.                                  error_messages={ 
  6.                                      'blank''请输入验证码'
  7.                                      'required''请输入验证码'
  8.                                      'min_length''验证码格式错误'
  9.                                      'max_length''验证码格式错误'
  10.                                  }, write_only=True
  11.  
  12.     # 利用drf中的validators验证username是否唯一 
  13.     username = serializers.CharField(required=True, allow_blank=False
  14.                                      validators=[UniqueValidator(queryset=User.objects.all(), message='用户已经存在')]) 
  15.  
  16.     email = serializers.EmailField(required=True, allow_blank=False
  17.                                    validators=[UniqueValidator(queryset=User.objects.all(), message='邮箱已被注册')]) 
  18.  
  19.     # 对code字段单独验证(validate_+字段名) 
  20.     def validate_code(self, code): 
  21.         verify_records = VerifyCode.objects.filter(email=self.initial_data['email']).order_by('-add_time'
  22.         if verify_records: 
  23.             last_record = verify_records[0] 
  24.             # 判断验证码是否过期 
  25.             five_minutes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)  # 获取5分钟之前的时间 
  26.             if last_record.add_time < five_minutes_ago: 
  27.                 raise serializers.ValidationError('验证码过期'
  28.             # 判断验证码是否正确 
  29.             if last_record.code != code: 
  30.                 raise serializers.ValidationError('验证码错误'
  31.             # 不用将code返回到数据库中,只是做验证 
  32.             # return code 
  33.         else
  34.             raise serializers.ValidationError('验证码不存在'
  35.  
  36.     # attrs:每个字段validate之后总的dict 
  37.     def validate(self, attrs): 
  38.         # attrs['mobile'] = attrs['username'
  39.         # 从attrs中删除code字段 
  40.         del attrs['code'
  41.         return attrs 
  42.  
  43.     class Meta: 
  44.         model = User 
  45.         fields = ('username''email''password''code'
  46.         extra_kwargs = {'password': {'write_only'True}} 
  47.  
  48.     def create(self, validated_data): 
  49.         user = User
  50.             email=validated_data['email'], 
  51.             username=validated_data['username'
  52.         ) 
  53.         user.set_password(validated_data['password']) 
  54.         user.save() 
  55.         return user 

至此发送验证码的后端编码已经结束。

最后的话

一次性验证码(OTP)的逻辑简单,需要思考的是如何在 DRF 的框架中填空,填在哪里?这其实需要了解 DRF 的 ModelSerializer 类和 ViewSet 类之前的关系,在调用关系上,ViewSet 类调用 ModelSerializer 来实现字段的验证和数据保存及序列化,Serializers 类不是必须的,你可以完全自己实现验证和数据保存及序列化,只不过这样会导致 View 类特别臃肿,不够优雅,不易维护。

原文链接:https://mp.weixin.qq.com/s/vr3jOdZ7R6S92vA86n6DmQ