专栏首页Python七号用 Django REST framework 来实现一次性验证码(OTP)

用 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 分钟之内的验证码,是否正确,如果是,则放行。否则提示用户。

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

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

本文分享自微信公众号 - Python七号(PythonSeven),作者:somenzz

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-09-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 基于Django的双因子认证实现

    双因子简介 对于网络信息系统来说,能否识别使用者的身份,是能否确保安全的基础和关键。在实际应用中,许多网络信息系统都会要求使用者在使用系统之前,提供一些相关信息...

    FB客服
  • 一、二、开发准备

    Freshman
  • Django REST framework+Vue 打造生鲜超市(一)

    一、项目介绍 1.1.掌握的技术 Vue + Django Rest Framework 前后端分离技术 彻底玩转restful api 开发流程 Django...

    zhang_derek
  • Django-rest-framework 是个什么鬼?

    其实,基于 django-rest-framework 的 RESTful API 的开发,过程是完全类似的:

    HelloGitHub
  • Django API开发: 使用Python和Django构建web APIs

    中文翻译版:Django API开发: 使用Python和Django构建web APIs

    宇宙之一粟
  • Django+Vue开发生鲜电商平台之1.项目介绍

    本项目旨在使用Django、Vue和REST Framework等技术开发一个前后端分离的生鲜电商平台。

    cutercorley
  • FastAPI框架诞生的缘由(上)

    原文:https://fastapi.tiangolo.com/alternatives/

    somenzz
  • 第 3 篇:实现博客首页文章列表 API

    此前在讨论基于模板引擎的开发方式和 django-rest-framework 开发的异同时说过,django-rest-framework 开发和传统的开发方...

    HelloGitHub
  • Python周刊:第 2 期

    TalkPython
  • DRF系列总结一:DRF是啥?为啥子要用?

    目前组内很多项目都在用Django REST framework(简称DRF)来构建Restful WebApi,并推广到了许多实习生同学和外部服...

    高木工
  • 在学习django-rest-framework时收集的学习资料推荐

    由于我平时开发的 django 项目都比较小,所以一直以来都是使用 django 模板引擎渲染 html 页面这种比较原始的方式在开发。最近发起了一个 Djan...

    追梦人物
  • 在学习django-rest-framework时收集的学习资料推荐

    由于我平时开发的 django 项目都比较小,所以一直以来都是使用 django 模板引擎渲染 html 页面这种比较原始的方式在开发。最近发起了一个 Djan...

    追梦人物
  • django_restframework模块学习

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

    菲宇
  • 说说web应用程序中的用户认证

    后端只能收到前端发送的请求头,请求参数,及资源定位符(url)。在没有用户认证的情况下,无论前端是谁,只要发送的请求一样,后端返回的数据也是一样的,前端人人平等...

    somenzz
  • python测试开发django-62.基于类的视图(APIView和View)

    django中编辑视图views.py有两种方式,一种是基于类的实现,另外一种是函数式的实现方式,两种方法都可以用。 REST框架提供了一个APIView类,它...

    上海-悠悠
  • Django Rest Framework 限流(下)

    向认证一样Django进阶篇 Rest framework (五),在 utils 包中定义限流组件。

    小团子
  • django-rest-framework配置json web token进行接口的认证

    使用django-rest-framework开发api并使用json web token进行身份验证 在这里使用django-rest-framework-j...

    earthchen
  • Django REST Framework

    若尘_
  • Django进阶篇 Rest framework (五)

    使用 pycharm 作为集成开发工具,创建 django 项目查看 Python 和第三方库源码很方便,使用 pycharm 创建一个 django 项目,然...

    小团子

扫码关注云+社区

领取腾讯云代金券