【15.0】登陆注册功能

Dream-Z / 2023-08-19 / 原文

【一】多方式登陆

【1】思路分析

(1)接口设计

  • 接口描述

    • 用户登录接口
  • 请求URL

    • /api/v1/user/userinfo/mul_login/
  • 请求方式

    • POST
  • Body请求参数(application/json)

参数名 必选 类型 说明
username string 用户名(支持用户名/邮箱/手机号)
password string 密码
  • 返回示例
    • 500认证成功
    • 401 认证失败
{
    "code": 100,
    "msg": "请求成功",
    "username": "admin",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjkxNTY0MTE1LCJlbWFpbCI6IiJ9.RYxs3D6EjOfzLKGgF-QAav6k0sC_tCPWUd6tskGdlhI",
    "icon": "http://127.0.0.1:8000/media/icon/default.png"
}
  • 要求
    • 校验参数是否为空
    • 校验账号密码是否正确
    • 登陆之后返回token
    • 支持用户名多字段登陆,用户名可以使用手机号/邮箱/用户名登陆

【2】代码实现

  • 视图层
    • luffyCity\luffyCity\apps\user\views.py
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework.decorators import action
from luffyCity.apps.user.models import User
from luffyCity.apps.user.serializers.mul_login_serializer import MulLoginSerializer
from luffyCity.utils.common_response import CommonResponse
from rest_framework.exceptions import APIException


# Create your views here.
class UserView(GenericViewSet):
    '''
    验证手机号接口
    get请求
    与数据库交互但不需要序列化
    继承 ViewSet 自动生成路由
    '''

    # 序列化类
    serializer_class = MulLoginSerializer

    # 校验手机号
    @action(methods=['GET'], detail=False)
    def check_mobile(self, request, *args, **kwargs):
        '''
        get 请求 携带在地址参数
        :param request:
        :param args:
        :param kwargs:
        :return:
        '''
        try:
            mobile = request.query_params.get('mobile', None)
            User.objects.get(mobile=mobile)  # 有且只有一条才不会报错
            return CommonResponse(msg="手机号存在")
        except Exception as e:
            raise APIException("手机号不存在")

    # 多方式登陆 --- 序列化类校验数据
    @action(methods=['POST'], detail=False)
    def mul_login(self, request, *args, **kwargs):
        # 校验逻辑 --- 序列化类
        ser = self.get_serializer(data=request.data)
        # raise_exception:如果有错误,主动抛出异常,被全局异常捕获
        # is_valid : 触发字段的校验规则,局部钩子/全局钩子(全局钩子中写验证逻辑,签发token)
        ser.is_valid(raise_exception=True)
        username = ser.context.get('username')
        token = ser.context.get('token')
        icon = ser.context.get('icon')
        return CommonResponse(username=username, token=token, icon=icon)
  • 序列化类
    • luffyCity\luffyCity\apps\user\serializers\mul_login_serializer.py

  • luffyCity\luffyCity\utils\common_settings.py
BACKEND_URL = 'http://127.0.0.1:8000'
  • 路由
    • luffyCity\luffyCity\apps\user\urls.py
from django.urls import path
from .views import UserView
from rest_framework.routers import SimpleRouter

router = SimpleRouter()
router.register('userinfo', UserView, 'userinfo')
urlpatterns = [

]
urlpatterns += router.urls

【二】验证码接口封装成包

【1】验证码发送主函数

  • luffyCity\luffyCity\libs\SMS_TencentCloud_Sender\SMS_Ten_Send.py

【2】验证码发送配置文件

  • luffyCity\luffyCity\libs\SMS_TencentCloud_Sender\settings.py

【3】验证码包的初始化文件

  • luffyCity\luffyCity\libs\SMS_TencentCloud_Sender\__init__.py

【4】验证码发送接口的风险问题

验证码发送接口的风险问题主要是安全性和频率限制两个方面。

  • 首先,短信验证码的安全性是一个重要的问题。
    • 因为验证码通常用于身份验证或重要操作的确认,如果验证码泄露或被劫持,可能导致用户账号被盗或遭受其他安全威胁。

(1)提高安全性

数据加密:

  • 在验证码发送接口中,可以引入加密机制
  • 例如携带一个特定的加密串,使用对称或非对称加密算法将验证码数据进行加密,以防止数据被未授权人员获取并使用。

防止重放攻击:

  • 为了防止恶意用户重复使用已经获取到的验证码进行验证,可以在验证码数据中加入一些唯一标识符或时间戳,并在验证过程中验证其有效性。
  • 这样可以有效防止验证码的重放攻击。

(2)频率限制

  • 其次,频率限制也是必要的。通过限制发送验证码的频率,可以降低恶意用户滥用接口或对系统造成过大负载的风险。

IP限制:

  • 可以通过限制同一个IP地址在一段时间内发送验证码的次数,来限制恶意用户的行为。对于大流量代理服务器问题,可以考虑使用代理池技术,即在后台维护一个可用的代理IP池,通过轮询使用不同的IP地址发送验证码短信,以平均负载和降低单个IP的频率。

手机号限制:

  • 同一个手机号在一定时间内发送验证码的次数也需要进行限制,以防止恶意用户通过滥用接口对用户账号进行猜测或攻击。可以设置一个合理的时间间隔,在此时间间隔内只允许发送有限次数的验证码。

【三】短信验证登录

【1】主视图函数

from django.shortcuts import render, HttpResponse
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework.decorators import action
from luffyCity.apps.user.models import User
from luffyCity.apps.user.serializers.mul_login_serializer import MulLoginSerializer, SmsLoginSerializer
from luffyCity.utils.common_response import CommonResponse
from rest_framework.exceptions import APIException
from luffyCity.libs.SMS_TencentCloud_Sender import get_verify_code, tencent_sms_main
from django.core.cache import cache


# Create your views here.
class UserView(GenericViewSet):
    '''
    验证手机号接口
    get请求
    与数据库交互但不需要序列化
    继承 ViewSet 自动生成路由
    '''

    # 序列化类
    serializer_class = MulLoginSerializer

    # 校验手机号
    @action(methods=['GET'], detail=False)
    def check_mobile(self, request, *args, **kwargs):
        '''
        get 请求 携带在地址参数
        :param request:
        :param args:
        :param kwargs:
        :return:
        '''
        try:
            mobile = request.query_params.get('mobile', None)
            User.objects.get(mobile=mobile)  # 有且只有一条才不会报错
            return CommonResponse(msg="手机号存在")
        except Exception as e:
            raise APIException("手机号不存在")

    def _common_login(self, request, *args, **kwargs):
        # 校验逻辑 --- 序列化类
        ser = self.get_serializer(data=request.data)
        # raise_exception:如果有错误,主动抛出异常,被全局异常捕获
        # is_valid : 触发字段的校验规则,局部钩子/全局钩子(全局钩子中写验证逻辑,签发token)
        ser.is_valid(raise_exception=True)
        username = ser.context.get('username')
        token = ser.context.get('token')
        icon = ser.context.get('icon')
        return CommonResponse(username=username, token=token, icon=icon)

    # 多方式登陆 --- 序列化类校验数据
    @action(methods=['POST'], detail=False)
    def mul_login(self, request, *args, **kwargs):

        return self._common_login(request, *args, **kwargs)

    @action(methods=['GET'], detail=False)
    def send_sms(self, request, *args, **kwargs):

        # 前端把需要发送验证码的手机号传入,携带在地址栏中
        mobile = request.query_params.get('mobile', None)
        code = get_verify_code(4)  # 存储验证码,放到缓存内
        cache.set(f'sms_code_{mobile}', code)
        if mobile and tencent_sms_main(verify_code=code, tag_phone=mobile):
            return CommonResponse(msg="发送验证码成功")
        raise APIException("请输入手机号")

    def get_serializer_class(self):
        if self.action == 'sms_login':
            return SmsLoginSerializer
        else:
            return self.serializer_class

    @action(methods=['POST'], detail=False)
    def sms_login(self, request, *args, **kwargs):
        return self._common_login(request, *args, **kwargs)

【短信发送功能优化】异步处理

原来的发送短信,是同步

  • 前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》调用腾讯发送短信---》腾讯去发短信---》发完后----》回复给我们后端发送成功---》我们后端收到发送成功---》给我们前端返回发送成功

把腾讯发送短信的过程,变成异步

  • 前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》开启线程,去调用腾讯短信发送(异步)---》我们后端继续往后走----》直接返回给前端,告诉前端短信已发送
    -另一条发短信线程线程会去发送短信,至于是否成功,我们不管了
@action(methods=['GET'], detail=False)
def send_sms(self, request, *args, **kwargs):

    # 前端把需要发送验证码的手机号传入,携带在地址栏中
    mobile = request.query_params.get('mobile', None)
    code = get_verify_code(4)  # 存储验证码,放到缓存内
    cache.set(f'sms_code_{mobile}', code)
    if mobile:
        # 开启线程处理短信
        # tencent_sms_main(verify_code=code, tag_phone=mobile)
        t = Thread(target=tencent_sms_main, args=(code, mobile,))
        t.start()
        return CommonResponse(msg="验证码已发送")
    raise APIException("请输入手机号")

【2】序列化校验


【四】注册功能

【2】功能实现

  • 视图函数
    • luffyCity\luffyCity\apps\user\views.py
from threading import Thread

from django.shortcuts import render, HttpResponse
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework.mixins import CreateModelMixin
from rest_framework.decorators import action
from luffyCity.apps.user.models import User
from luffyCity.apps.user.serializers.mul_login_serializer import MulLoginSerializer, SmsLoginSerializer, \
    UserRegisterSerializer
from luffyCity.utils.common_response import CommonResponse
from rest_framework.exceptions import APIException
from luffyCity.libs.SMS_TencentCloud_Sender import get_verify_code, tencent_sms_main
from django.core.cache import cache


# Create your views here.
class UserView(GenericViewSet, CreateModelMixin):
    '''
    验证手机号接口
    get请求
    与数据库交互但不需要序列化
    继承 ViewSet 自动生成路由
    '''

    # 序列化类
    serializer_class = MulLoginSerializer

    # 校验手机号
    @action(methods=['GET'], detail=False)
    def check_mobile(self, request, *args, **kwargs):
        '''
        get 请求 携带在地址参数
        :param request:
        :param args:
        :param kwargs:
        :return:
        '''
        try:
            mobile = request.query_params.get('mobile', None)
            User.objects.get(mobile=mobile)  # 有且只有一条才不会报错
            return CommonResponse(msg="手机号存在")
        except Exception as e:
            raise APIException("手机号不存在")

    def _common_login(self, request, *args, **kwargs):
        # 校验逻辑 --- 序列化类
        ser = self.get_serializer(data=request.data)
        # raise_exception:如果有错误,主动抛出异常,被全局异常捕获
        # is_valid : 触发字段的校验规则,局部钩子/全局钩子(全局钩子中写验证逻辑,签发token)
        ser.is_valid(raise_exception=True)
        username = ser.context.get('username')
        token = ser.context.get('token')
        icon = ser.context.get('icon')
        return CommonResponse(username=username, token=token, icon=icon)

    # 多方式登陆 --- 序列化类校验数据
    @action(methods=['POST'], detail=False)
    def mul_login(self, request, *args, **kwargs):

        return self._common_login(request, *args, **kwargs)

    # @action(methods=['GET'], detail=False)
    # def send_sms(self, request, *args, **kwargs):
    #
    #     # 前端把需要发送验证码的手机号传入,携带在地址栏中
    #     mobile = request.query_params.get('mobile', None)
    #     code = get_verify_code(4)  # 存储验证码,放到缓存内
    #     cache.set(f'sms_code_{mobile}', code)
    #     if mobile and tencent_sms_main(verify_code=code, tag_phone=mobile):
    #         return CommonResponse(msg="发送验证码成功")
    #     raise APIException("请输入手机号")

    @action(methods=['GET'], detail=False)
    def send_sms(self, request, *args, **kwargs):

        # 前端把需要发送验证码的手机号传入,携带在地址栏中
        mobile = request.query_params.get('mobile', None)
        code = get_verify_code(4)  # 存储验证码,放到缓存内
        cache.set(f'sms_code_{mobile}', code)
        if mobile:
            # 开启线程处理短信
            # tencent_sms_main(verify_code=code, tag_phone=mobile)
            t = Thread(target=tencent_sms_main, args=(code, mobile,))
            t.start()
            return CommonResponse(msg="验证码已发送")
        raise APIException("请输入手机号")

    def get_serializer_class(self):
        if self.action == 'sms_login':
            return SmsLoginSerializer
        elif self.action == 'register' or self.action == 'create':
            return UserRegisterSerializer
        else:
            return self.serializer_class

    @action(methods=['POST'], detail=False)
    def sms_login(self, request, *args, **kwargs):
        return self._common_login(request, *args, **kwargs)

    # 自己写的  访问:127.0.0.1:8000/api/v1/user/userinfo/register/   --->post请求即可
    @action(methods=['POST'], detail=False)
    def register(self, request, *args, **kwargs):
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)
        ser.save()
        # super().create(request, *args, **kwargs)  # 只要这样写,又会走序列化
        return CommonResponse(msg='注册成功')

    # 不自己写了,只要继承CreateModelMixin,访问:127.0.0.1:8000/api/v1/user/userinfo   --->post请求即可
    # 这个我们不用写,它有  只要post请求过来,就会执行create
    # def create(self, request, *args, **kwargs):
    #     serializer = self.get_serializer(data=request.data) # 第一个错误  UserRegisterSerializer
    #     serializer.is_valid(raise_exception=True) #执行三个校验:字段自己,局部钩子,全局
    #     self.perform_create(serializer)
    #     # 序列化要调用它,只要调用serializer.data ,就会走序列化,只要走序列化,会把create返回的user对象 来使用UserRegisterSerializer类做序列化
    #     return CommonResponse(msg='注册成功') #不走序列化了,序列类中得write_only 也就不用了
  • 序列化类
    • luffyCity\luffyCity\apps\user\serializers\mul_login_serializer.py

# 注册:1 校验数据  2 保存  3 序列化用不要?存疑
class UserRegisterSerializer(serializers.ModelSerializer):
    code = serializers.CharField(max_length=4, min_length=4, write_only=True)

    class Meta:
        model = User
        fields = ['mobile', 'password', 'code']  # code 不是数据库的字段,需要重写

    # 如果要限制密码强度,需要写个局部钩子
    def _check_code(self, attrs):
        mobile = attrs.get('mobile')
        code = attrs.get('code')
        old_code = cache.get('send_sms_code_%s' % mobile)
        if not (code == old_code or (settings.DEBUG and code == '9999')):  # 第二个错误:debug忘了设为True
            raise APIException("验证码错误")

    def _pre_save(self, attrs):  # {mobile:122222,code:8888,password:123}
        attrs.pop('code')
        attrs['username'] = attrs.get('mobile')  # 默认用户名就是手机号  可以随机生成用户名  随机生成有意义的名字( Faker)

    def validate(self, attrs):
        # 写逻辑
        # 1 校验验证码是否正确
        self._check_code(attrs)
        # 2 入口前准备 ---》code不是数据库字段,不能入库,username是数据库字段必填,这里没有,写成默认
        self._pre_save(attrs)
        return attrs

    def create(self, validated_data):  # {mobile:122222,password:123,username:名字}
        # 为什么要重写create? 因为密码人家是加密的,如果不写用的是
        # User.objects.create(**validated_data)  # 密码是铭文,必须重写
        user = User.objects.create_user(**validated_data)  # 保存成功,密码就是加密的
        return user