
一次性验证码,用D验证英文是实现 One Time Password,简写为 OTP,次性又称动态密码或单次有效密码,用D验证是实现指计算机系统或其他数字设备上只能使用一次的密码,有效期为只有一次登录会话或很短如 1 分钟。次性OTP 避免了一些静态密码认证相关系的用D验证缺点,不容易受到重放攻击,实现比如常见的次性注册场景,用户的用D验证邮箱或短信会收到一条一次性的激活链接,或者收到一次随机的实现验证码(只能使用一次),从而验证了邮箱或手机号的次性有效性。
今天讲一下如何用 Django REST framework[1](DRF) 来实现 OTP,用D验证阅读本文需要一定的实现 DRF 的基础知识。
要实现的次性功能就是:
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 类特别臃肿,不够优雅,不易维护。
本文转载自微信公众号「Python七号」,可以通过以下二维码关注。转载本文请联系Python七号公众号。
