一次性验证码,英文是OneTimePassword,简称OTP,也称为动态密码或一次性有效密码,指的是密码计算机系统或其他数字设备一次性密码仅在一次登录会话或最短1分钟内有效。OTP避免了与静态密码认证相关的一些缺点,并且不易受到重放攻击。例如,在常见的注册场景中,用户的邮箱或短信会收到一个一次性激活链接,或收到一个随机验证码(只能使用一次),从而验证邮箱或手机号的有效性。今天我就来说说如何使用DjangoRESTframework[1](DRF)来实现OTP。阅读本文需要对DRF有一定的基础知识。实现的功能有:1、验证码为6位数字和小写字母的组合。2、有效期为5分钟,1分钟后必须发送二次验证码。3.如果邮箱/手机号已经注册过,则无法发送注册验证码。具体实现逻辑为:1.生成满足条件的验证码。2.发送前验证。上次发送的验证码是否在1分钟内?电子邮件地址是否已注册?如果是,则拒绝发送并提示用户,如果不是,则发送验证码。3、验证,是否是5分钟内的验证码,是否正确,是则放行。否则提示用户。为了验证验证码及其时效性,我们需要记录验证码发送的时间和对应的邮箱,所以需要设计一个表来存储。classVerifyCode(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="verificationcode")add_time=models.DateTimeField(verbose_name='generatedtime',auto_now_add=True)1.第一个生成验证码的逻辑很简单,直接写代码即可:fromrandomimportchoicedefgenerate_code(self):"""生成6位验证码,防止破解:return:"""seeds="1234567890abcdefghijklmnopqrstuvwxyz"random_str=[]foriinrange(6):random_str.append(choice(seeds))return""。join(random_str)2.发送前验证。DjangoREST框架的Serializer可以验证Models中的每个字段。我们可以直接填其中的空格:#serializers.pyclassVerifyCodeSerializer(serializers.Serializer):email=serializers.EmailField(required=True)defvalidate_email(self,email):"""验证邮箱地址是否有效"""#邮箱地址是否注册ifUser.objects.filter(email=email).count():raiseserializers.ValidationError('Thisemailaddresshasalreadybeenregistered')#验证邮箱号码是否合法ifnotre.match(EMAIL_REGEX,email):raiseserializers.ValidationError('邮箱格式错误')#验证码发送频率one_minute_age=datetime.now()-timedelta(hours=0,minutes=1,seconds=0)ifVerifyCode.objects.filter(add_time__gt=one_minute_age,email=email).count():raiseserializers.ValidationError('请过一分钟再发送')returnemail3.发送验证码发送验证码其实就是生成验证码并保存的过程,借助DjangoREST框架框架的GenericViewSet和CreateModelMixin可以实现视图类。代码有详细的注释,大家可以轻松看懂:fromrest_framework.responseimportResponsefromrest_framework.viewsimportstatusfromrest_frameworkimportmixins,viewsetsclassVerifyCodeViewSet(viewsets.GenericViewSet,mixins.CreateModelMixin):"""发送验证码"""permission_classes=[AllowAny]#Alloweveryonetoregisterserializer_class=VerifyCodeSerializer#发送前的相关验证逻辑defgenerate_code(self):"""生成6位验证码防止破解:return:"""seeds="1234567890abcdefghijklmnopqrstuvwxyz"random_str=[]foriinrange(6):random_str.append(choice(seeds))return"".join(random_str)defcreate(self,request,*args,**kwargs):#Customizedcreate()contentserializer=self.get_serializer(data=request.data)serializer.is_valid(raise_exception=True)#这一步相当于发送前验证#从validated_data获取mobileemail=serializer.validated_data["email"]#随机生成codecode=self.generate_code()#发送短信或邮件验证码sms_status=SendVerifyCode.send_email_code(code=code,to_email_adress=email)ifsms_status==0:#RecordLogreturnResponse({"msg":"邮件发送失败"},status=status.HTTP_400_BAD_REQUEST)else:code_record=VerifyCode(code=code,email=email)#保存验证码code_record.save()returnResponse({"msg":f"验证码已发送至{email}"},status=status.HTTP_201_CREATED)SendVerifyCode.send_email_code的实现如下:#encoding=utf-8fromdjango.core.mailimportsend_mailclassSendVerifyCode(object):@staticmethoddefsend_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)returnssuccess_numexcept:return04。注册时验证用户注册。对于数据库来说,User类插入一条记录,就是User视图类的create操作实现注册。from.serializersimportUserRegisterSerializer,UserSerializerclassUserViewSet(viewsets.ModelViewSet):"""APIendpointthatallowsuserstobeviewedoredited."""serializer_class=UserSerializerdefget_serializer_class(self):ifself.action=="create":#如果是创建用户,那么用UserRegisterSerializerserializer_class=UserRegisterSerializerelse:serializer_class=UserSerializerreturnserializer_class骨架准备好后,我们来编写UserRegisterSerializer类来实现注册时的校验:#serializers.pyclassUserRegisterSerializer(serializers.ModelSerializer):#error_message:自定义错误信息提示码的格式=serializers.CharField(required=True,allow_blank=False,min_length=6,max_length=6,help_text='验证码',error_messages={'blank':'请输入验证码','required':'请输入验证码','min_length':'验证码格式error','max_length':'验证码格式错误',},write_only=True)#在drf中使用validators验证userna是否存在我是独一无二的.EmailField(required=True,allow_blank=False,validators=[UniqueValidator(queryset=User.objects.all(),message='Emailhasbeenregistered')])#单独验证code字段(validate_+字段名)defvalidate_code(self,code):verify_records=VerifyCode.objects.filter(email=self.initial_data['email']).order_by('-add_time')ifverify_records:last_record=verify_records[0]#判断验证码是否过期five_minutes_ago=datetime.now()-timedelta(hours=0,minutes=5,seconds=0)#获取5分钟之前的时间iflast_record.add_time
