路飞项目登录注册功能,短信封装,短信验证码功能,短信登录接口,短信注册接口

路飞项目登录注册功能

项目分析

# 接口分析
	1 校验手机号是否存在的接口
	2 多方式登录接口:用户名/手机号/邮箱 +密码都可以登录
	3 发送手机验证码接口   (借助于第三方短信平台)
    4 短信登录接口
    5 注册接口

手机号是否存在接口

view

from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from .models import User
from utils.common_response import APIResponse



class UserView(GenericViewSet):
    # 验证手机号是否存在接口
    @action(methods=['GET'],detail=False)
    def check_mobile(self,request,*args,**kwargs):
        try:
            # 拿到地址栏中的手机号中括号取不到直接报错['mobile']
            mobile = request.query_params['mobile']
            # 表操作get()不到直接报错,字典的get取不到是None
            User.objects.get(mobile=mobile)
        except Exception as e:
            raise e
        # 传参,不是直接返回到前端
        return APIResponse(msg='手机号存在')

urls

from .views import UserView
from django.urls import path
from rest_framework.routers import SimpleRouter
router = SimpleRouter()

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

]
urlpatterns += router.urls

用户登录接口

views

from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from .models import User
from utils.common_response import APIResponse
from .serializer import UserLoginSerializer


class UserView(GenericViewSet):
    # 指定序列化类
    serializer_class = UserLoginSerializer
    # 拿到用户数据 并过滤是否活动用户
    queryset = User.objects.all().filter(is_active=True)


    @action(methods=['POST'], detail=False)
    def login_mul(self, request, *args, **kwargs):
        # 拿到序列化对象
        ser = self.get_serializer(data=request.data)
        # 校验数据,会走字段自己的校验,局部钩子,全局钩子 raise_exception 没通过直接抛异常
        ser.is_valid(raise_exception=True)
        # 拿到token
        token = ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token, username=username)

序列化类

from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import APIException
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserLoginSerializer(serializers.ModelSerializer):
    # 因auth的user表的username字段有唯一校验,用户登录肯定会校验用户名,如果用户存在校验就不符合唯一,我们需要重写校验规则
    username = serializers.CharField()

    class Meta:
        model = User
        fields = ['username', 'password']

    # 全局钩子校验
    def validate(self, attrs):
        '''
           把这个逻辑放在序列化类中
           1 取出前端传入的用户名和密码
           2 通过用户名和密码去数据库查询用户
           3 如果能查到,签发token
           4 返回给前端登录成功
           '''

        # 写一个函数拿到用户对象
        user = self._get_user(attrs)
        # 写一个函数拿到token 把用户传过去
        token = self._get_token(user)
        #把用户名,和token放到ser的 context中与视图类沟通
        self.context['username'] = user.username
        self.context['token'] = token
        return attrs

    def _get_user(self, attrs):
        # attrs 是前端传入的数据,经过字段自己和局部钩子的校验后的数据(但是我们没写)
        username = attrs.get('username')
        password = attrs.get('password')
        # 手机号验证 此时的username 是前端传的手机号 username:13213245
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = User.objects.filter(mobile=username).first()
        # 邮箱验证 此时的username 是前端传的邮箱号 username:2123@qq.com
        elif re.match(r'^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$', username):
            user = User.objects.filter(email=username).first()
        else:
            # 都不是就按用户名拿
            user = User.objects.filter(username=username).first()
        # 校验用户是否有值 and 加密后密码校验通过
        if user and user.check_password(password):
            return user
        # 校验没通过抛异常
        else:
            raise APIException('用户名或密码错误')

    def _get_token(self, user):
        # jwt自带了签发token 导入使用
        # 用用户拿到payload
        payload=jwt_payload_handler(user)
        # 用payload生成token
        token = jwt_encode_handler(payload)
        return token

腾讯云短信

用户注册或登录需要发送验证码到用户的手机上,就需要用到一个短信平台。腾讯云就是其中之一。

使用腾讯云的SDK

安装SDK

# 方式一
pip install tencentcloud-sdk-python
# 方式二源码下载
下载源码		
pycharm切换到源码目录下
执行 python steup.py install

封装发送短信

在lib目录下创建一个包 send_sms_v3

创建py文件sms

写一个生成验证码函数

# 生成验证码函数
import random
def get_code():
    code = ''
    for i in range(4):
        code += str(random.randint(0, 9))
    return code
print(get_code())

腾讯云短信SDK函数,需要用到一些参数不写死的情况下需要写在配置文件内

创建一个这个包的settings

SECRET_ID = 'AKIDjU9sWK8qSuDNOFuNdjdLrAGAwZ0KZcT9'
SECRET_KEY = 'd6LM9j9SejeMp362mHXZLtBF4e2Ghwwq'
APP_ID = '1400799874'
SIGN_NAME = '李阿鸡的公众号'
TEMPLATE_ID = '1720400'

再写一个腾讯云短信SDK函数,调用的时候需要传验证码与手机号,并在函数内部添加返回给视图函数 短信是否发送成功。True或False

# 导入短信SDK相关模块
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sms.v20210111 import sms_client, models
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
# 导入配置文件
from . import settings
import json

# 写发送短信函数调用时需要传参 验证码与手机号

def send_sms_sdk(code,mobile):
    try:
        # 必要步骤:
        # 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
        # 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
        # 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
        # 以免泄露密钥对危及你的财产安全。
        # SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi
        cred = credential.Credential(settings.SECRET_ID, settings.SECRET_KEY)
        # cred = credential.Credential(
        #     os.environ.get(""),
        #     os.environ.get("")
        # )

        # 实例化一个http选项,可选的,没有特殊需求可以跳过。
        httpProfile = HttpProfile()
        # 如果需要指定proxy访问接口,可以按照如下方式初始化hp(无需要直接忽略)
        # httpProfile = HttpProfile(proxy="http://用户名:密码@代理IP:代理端口")
        httpProfile.reqMethod = "POST"  # post请求(默认为post请求)
        httpProfile.reqTimeout = 30  # 请求超时时间,单位为秒(默认60秒)
        httpProfile.endpoint = "sms.tencentcloudapi.com"  # 指定接入地域域名(默认就近接入)

        # 非必要步骤:
        # 实例化一个客户端配置对象,可以指定超时时间等配置
        clientProfile = ClientProfile()
        clientProfile.signMethod = "TC3-HMAC-SHA256"  # 指定签名算法
        clientProfile.language = "en-US"
        clientProfile.httpProfile = httpProfile

        # 实例化要请求产品(以sms为例)的client对象
        # 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
        client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)

        # 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
        # 你可以直接查询SDK源码确定SendSmsRequest有哪些属性可以设置
        # 属性可能是基本类型,也可能引用了另一个数据结构
        # 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明
        req = models.SendSmsRequest()

        # 基本类型的设置:
        # SDK采用的是指针风格指定参数,即使对于基本类型你也需要用指针来对参数赋值。
        # SDK提供对基本类型的指针引用封装函数
        # 帮助链接:
        # 短信控制台: https://console.cloud.tencent.com/smsv2
        # 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81

        # 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666
        # 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
        req.SmsSdkAppId = settings.APP_ID
        # 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名
        # 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
        req.SignName = settings.SIGN_NAME
        # 模板 ID: 必须填写已审核通过的模板 ID
        # 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
        req.TemplateId = settings.TEMPLATE_ID
        # 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空
        req.TemplateParamSet = [code]    # 生成的验证码
        # 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
        # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
        req.PhoneNumberSet = ["+86" + mobile]  # 前端携带的手机号
        # 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回
        req.SessionContext = ""
        # 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手]
        req.ExtendCode = ""
        # 国际/港澳台短信 senderid(无需要可忽略): 国内短信填空,默认未开通,如需开通请联系 [腾讯云短信小助手]
        req.SenderId = ""

        resp = client.SendSms(req)

        # 输出json格式的字符串回包, 给接口返回发送结果
        res=json.loads(resp.to_json_string(indent=2))
        if res.get('SendStatusSet')[0].get('Code') == "Ok":
            return True
        else:
            return False
        # 当出现以下错误码时,快速解决方案参考
        # - 更多错误,可咨询[腾讯云助手](https://tccc.qcloud.com/web/im/index.html#/chat?webAppId=8fa15978f85cb41f7e2ea36920cb3ae1&title=Sms)

    except TencentCloudSDKException as err:
        print(err)
        return False

短信验证码接口

刚刚封装好的发送短信,我们在短信验证码接口中调用并接受返回值判断是否发送成功,需要导入生成验证码与发送短信函数

# 下列导入过长
# from libs.send_sms_v3.sms import  send_sms_sdk,get_code
# 可以在lib的init里加入 from .sms import send_sms_sdk,get_code 注册
from libs.send_sms_v3 import get_code, send_sms_sdk
from rest_framework.viewsets import GenericViewSet
from django.core.cache import cache
class UserView(GenericViewSet):
    @action(methods=['POST'], detail=False)
    def send_sms(self, request):
        try:
            # 拿到手机号,[]拿不到报错
            mobile = request.data['mobile']
            # 调用生成验证码函数生成验证码
            code = get_code()
            # 存放验证码在缓存里
            # 验证码存放到缓存里,k值需要唯一不然会被覆盖
        	  cache.set('sms_code_%s'%mobile,code)
            # 调用发送短信函数并传验证码与手机号
            res = send_sms_sdk(code,mobile)
            # 判断短信发送返回值
            if res:
                return APIResponse(msg='发送成功')
            else:
                return APIResponse(msg='发送失败')
        except Exception as e:
            raise e

            
# 异步操作优化            
from threading import Thread
def send_sms(self, request):
    try:
        mobile = request.data['mobile']
        code = get_code()
        # 验证码存放到缓存里,k值需要唯一不然会被覆盖
        cache.set('sms_code_%s'%mobile,code)
        Thread(target=send_sms_sdk,args=['code,mobile'])
        t.start()
        # 线程的结果并不知道只能返回短信已发送,不能确切的知道发送成功与否
        return APIResponse(msg='短信已发送')
    except Exception as e:
    	raise e

短信登录接口

功能分析

取出手机号和验证码
校验验证码是否正确(发送验证码接口,存储验证码)
验证码往哪存
	 -session:根本不用
    -全局变量:不好,可能会取不到,集群环境中
    -用缓存
		 -from django.core.cache import cache
       	cache.set() 存
       	cache.get() 拿
根据手机号查询用户,如果能查到     
签发token
返回给前端

视图类

from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from utils.common_response import APIResponse
from .serializer import UserLoginSerializer, UserMobileLoginSerializer
from django.core.cache import cache

"""
用户登录接口的功能与短信登录接口功能代码重复,可以把他提取出来写一个方法 _login 在那两个接口直接调用
两个接口用的序列化类不一样 需要重写get_serializer_class方法
"""

class UserView(GenericViewSet):
    # 指定序列化类
    serializer_class = UserLoginSerializer
    # 拿到用户数据 并过滤是否活动用户
    queryset = User.objects.all().filter(is_active=True)


    def get_serializer_class(self):
        if self.action == 'login_sms':
            return UserMobileLoginSerializer
        else:
            # 如果不是短信登录接口还用之前的序列化类
            return super().get_serializer_class()

    @action(methods=['POST'], detail=False)
    def login_sms(self, request, *args, **kwargs):
        # 需要和登录接口用不一样的序列化类在外面重写 get_serializer_class方法
        "发现与登录接口代码重复,抽出来封装成方法"
        return self._login(request, *args, **kwargs)

    def _login(self, request, *args, **kwargs):
        # 拿到序列化对象
        ser = self.get_serializer(data=request.data)
        # 校验数据,会走字段自己的校验,局部钩子,全局钩子 raise_exception 没通过直接抛异常
        ser.is_valid(raise_exception=True)
        # 拿到token
        token = ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token, username=username)

序列化类

"""
因全局钩子,签发token,获取用户 函数功能与用户登录功能一样,只是在获取用户上逻辑代码有些区别,我们可以 封装一个类 包含这三种函数,只需要重写_get_user 函数即可。
"""
from rest_framework import serializers
from .models import User
from django.core.cache import cache
from utils.common_serializer import BaseSerializer

# 短信登录序列化类
class UserMobileLoginSerializer(BaseSerializer, serializers.ModelSerializer):
    # code 不是自己的字段重写
    code = serializers.CharField()
    # mobile 有唯一约束重写
    mobile = serializers.CharField()

    class Meta:
        model = User
        fields = ['code', 'mobile']

    def _get_user(self, attrs):
        # 取出前端输入的验证码与手机号
        code = attrs.get('code')
        mobile = attrs.get('mobile')
        # 从缓冲中拿出我们存取的验证码比对
        old_code = cache.get('sms_code_%s' % mobile)
        if old_code == code:
            user = User.objects.filter(mobile=mobile).first()
            if user:
                return user
            else:
                raise APIException('用户不存在')
        else:
            raise APIException('验证码校验失败')

# #  短信登录序列化类第二种写法
# class UserMobileLoginSerializer(BaseSerializer,serializers.Serializer):
#     # code 不是自己的字段重写
#     code = serializers.CharField()
#     # mobile 有唯一约束重写
#     mobile = serializers.CharField()
# 
#     def _get_user(self, attrs):
#         # 取出前端输入的验证码与手机号
#         code = attrs.get('code')
#         mobile = attrs.get('mobile')
#         # 从缓冲中拿出我们存取的验证码比对
#         old_code = cache.get('sms_code_%s' % mobile)
#         if old_code == code:
#             user = User.objects.filter(mobile=mobile).first()
#             if user:
#                 return user
#             else:
#                 raise APIException('用户不存在')
#         else:
#             raise APIException('验证码校验失败')

封装全局钩子,获取用户,签发token类

from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


# 因两个序列化都用到了全局钩子与token和获取用户,封装一个类用于继承
class BaseSerializer:
    # 全局钩子校验
    def validate(self, attrs):
        '''
           把这个逻辑放在序列化类中
           1 取出前端传入的用户名和密码
           2 通过用户名和密码去数据库查询用户
           3 如果能查到,签发token
           4 返回给前端登录成功
           '''

        # 写一个函数拿到用户对象
        user = self._get_user(attrs)
        # 写一个函数拿到token 把用户传过去
        token = self._get_token(user)
        # 把用户名,和token放到ser的 context中与视图类沟通
        self.context['username'] = user.username
        self.context['token'] = token
        return attrs

    # 必须重写_get_user方法
    def _get_user(self, attrs):
        raise Exception('必须重写_get_user方法')

    def _get_token(self, user):
        # jwt自带了签发token 导入使用
        # 用用户拿到payload
        payload = jwt_payload_handler(user)
        # 用payload生成token
        token = jwt_encode_handler(payload)
        return token

短信注册接口

功能分析

需要前端发送注册的 手机号,验证码,登录密码

写一个注册路由

router.register('register',RegisterUserView,'register')

视图类

我们需要统一返回格式所以自己封装了CreateModelMixin方法

from .serializer import RegisterSerializer
from utils.common_createmodelmixin import CommonCreateModelMixin


class RegisterUserView(GenericViewSet, CommonCreateModelMixin):
    queryset = User.objects.all()
    serializer_class = RegisterSerializer


封装的CommonCreateModelMixin

from rest_framework.mixins import CreateModelMixin
from utils.common_response import APIResponse


class CommonCreateModelMixin(CreateModelMixin):
    def create(self, request, *args, **kwargs):
        res = super().create(request, *args, **kwargs)
        return APIResponse(data=res.data)
另一种方法 这样序列化类里code字段就不用写write_only 了
	    def create(self, request, *args, **kwargs):
        # 使用父类的,会触发序列化,一定要让code只写
        super().create(request, *args, **kwargs)
        # 另一种写法,不用序列化
        # serializer = self.get_serializer(data=request.data)
        # serializer.is_valid(raise_exception=True)
        # self.perform_create(serializer)
        return APIResponse(msg='注册成功')

序列化类

from rest_framework import serializers
class RegisterSerializer(serializers.ModelSerializer):
    # code 不是数据库字段重写,执行is_valid就会执行字段的校验,这个是和走进来code是有值的,但是我们在全局钩子里弹出去了,create里执行了我们自己的create反回了一个用户后,执行ser.data就会再次执行序列化,但是这个时候已经被我们的全局钩子弹出去了,如果不写write_only 就会继续序列化这个字段,就会得到None
    code = serializers.CharField(max_length=4,write_only=True)
    # mobile 有唯一性校验 重写
    mobile = serializers.CharField()

    class Meta:
        model = User
        fields =['code','mobile','password']
        extra_kwargs = {
            'password':{'write_only':True}
        }
    # 全局钩子
    def validate(self, attrs):
        # 取出前端传入的code校验code是否正确
        mobile = attrs.get('mobile')
        code = attrs.get('code')
        # 从缓冲中拿出我们存取的验证码比对,这里需要手机号所以上面获取了手机号
        old_code = cache.get('sms_code_%s' % mobile)
        if code and old_code == code:
            # 因短信注册用户没有输入用户名,使用的手机号,我们把手机号当做用户名
            attrs['username'] = mobile
            # 因code不是表中字段
            attrs.pop('code')
        else:
            raise APIException('验证码校验失败')
        return attrs

    # 存储用户
    def create(self,validated_data):
        user = User.objects.create_user(**validated_data)
        return user

posted @ 2023-03-06 21:09  李阿鸡  阅读(28)  评论(0编辑  收藏  举报
Title