登录注册

登录、注册功能分析

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

校验手机号是否存在接口

视图类

视图类初级版

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

class UserView(GenericViewSet):
    @action(methods=['post'],detail=False)
    def check_mobile(self,request,*args,**kwargs):
        mobile = request.query_params.get('mobile')
        if mobile:
            user=User.objects.filter(mobile=mobile).first()
            if user:
                return APIResponse(code=100,msg='手机号存在',exist=True)
            else:
                return APIResponse(code=100, msg='手机号不存在',exist=False)
        else:
            return APIResponse(code=999,msg='手机号必填')
            

视图类代码优化方案一

因为已经做了全局异常处理,主动抛异常也可以,抛异常不是return,而是raise

from .models import User
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from utils.common_response import APIResponse
from rest_framework.exceptions import APIException
from .serilizer import UserLoginSerializer

class UserView(GenericViewSet):
    serializer_class = UserLoginSerializer
    queryset = User.objects.filter(is_active=True).all()

    @action(methods=['get'],detail=False)
    def check_mobile(self,request,*args,**kwargs):
        try:
            mobile = request.GET.get('mobile')
            if mobile:
                # 用get方法来查询一个数据库里不存在的记录,程序会报错,我们做了全局异常处理,这里会被捕获
                User.objects.get(mobile=mobile)
            else:
                raise APIException('手机号必填')
        except Exception as e:
            return APIResponse(code=777,msg=str(e))
        return APIResponse(code=100, msg='手机号存在')

视图类代码优化方案二

class UserView(GenericViewSet):
    serializer_class = UserLoginSerializer
    queryset = User.objects.filter(is_active=True).all()

    @action(methods=['get'], detail=False)
    def check_mobile(self, request, *args, **kwargs):
        try:
            mobile = request.GET.get('mobile')
            User.objects.get(mobile=mobile)
        except Exception as e:
            return APIResponse(code=777, msg=str(e))
        return APIResponse(code=100, msg='手机号存在')

路由

总路由

from django.urls import path,include
path('api/v1/user/',include('home.urls'))

子路由

from rest_framework.routers import SimpleRouter
from . import views

router = SimpleRouter()
# 访问 http://127.0.0.1:8000/api/v1/user/userinfo/check_mobile/
router.register('userinfo', views.UserView, 'userinfo')

urlpatterns = [

]

urlpatterns += router.urls

多方式登录接口

逻辑分析

1.取出前端传入的用户名和密码
2.通过用户名和密码去数据库查询用户
3.如果查询到,签发token
4.返回给前端登录成功

序列化类

import re
from .models import User
from rest_framework import serializers
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):
    # 重写username字段,把原来的校验规则去掉
    username = serializers.CharField()
    class Meta:
        model = User
        # username映射过来,是唯一的,字段自己的校验就过不了
        fields = ['username','password']

    def validate(self, attrs):
        """
        1.取出前端传入的用户名和密码
        2.通过用户名和密码去数据库查询用户
        3.如果查询到,签发token
        4.返回给前端登录成功
        """
        # attrs是前端传入的数据,经过字段自己校验和局部钩子校验过后的数据
        user=self._get_user(attrs)
        token=self._get_token(user)
        # 把用户名和token放到ser的context中
        self.context['token']=token
        self.context['username']=user.username
        return attrs


    def _get_user(self,attrs):
        username=attrs.get('username')
        password=attrs.get('password')
        if re.match(r'^1[3-9][0-9]{9}$',username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$',username):
            user=User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            return user
        else:
            # 用户不存在或密码错误   这里的代码,还是在全局钩子中执行,全局钩子校验失败,要抛异常,所以在这抛异常
            raise APIException('用户不存在或密码错误')


    def _get_token(self,user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token

视图类

class UserView(GenericViewSet):
    queryset = User.objects.filter(is_active=True).all()
    serializer_class = UserLoginSerializer
    
    @action(methods=['POST'],detail=False)
    def login_mul(self,request):
        # 序列化类对象时,可以传入context字典
        # context是视图类和序列化类沟通的桥梁
        # 有了序列化类对象,通过对象.context就可以拿到值
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)  # 执行这句话,会执行字段自己校验,局部钩子、全局钩子
        token=ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token,username=username) 

封装发送短信

基于腾讯云短信Python SDK

# sdk:https://cloud.tencent.com/document/product/382/43196#
# 使用步骤:
    -下载模块:pip3 install tencentcloud-sdk-python

Demo代码

# -*- coding: utf-8 -*-
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
# 导入对应产品模块的client models。
from tencentcloud.sms.v20210111 import sms_client, models

# 导入可选配置类
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile

try:
    cred = credential.Credential("AKIDTXcdXG6u5C9ycAd7WyFex9ED5VwPpBXp", "LZgaKxTOI0VowVs22qTvDKDtLcfWCiqm")
    httpProfile = HttpProfile()
    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
    client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
    req = models.SendSmsRequest()
    req.SmsSdkAppId = "1400763090" # 腾讯短信创建app把app的id号复制过来https://console.cloud.tencent.com/smsv2/app-manage
    # 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名
    # 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
    req.SignName = "关于金鹏公众号"
    # 模板 ID: 必须填写已审核通过的模板 ID
    # 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
    req.TemplateId = "1603526"
    # 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空
    req.TemplateParamSet = ["8888",'100']
    # 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
    # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
    req.PhoneNumberSet = ["+8615386800417"]
    # 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回
    req.SessionContext = ""
    req.ExtendCode = ""
    req.SenderId = ""

    resp = client.SendSms(req)

    # 输出json格式的字符串回包
    print(resp.to_json_string(indent=2))

except TencentCloudSDKException as err:
    print(err)

把发送短信封装成包

新建目录及文件

# libs目录下建一个包send_sms_v3,在此包内建setttings.py和sms.py文件

-libs下:
    send_sms_v3文件夹
        __init__.py
        settings.py
        sms.py

__init__.py

from .sms import get_code,send_sms

settings.py

SECRET_ID = 'AKIDfDnFkBVinhQ0PiwyoV6WjldKXbT6Yryu'
SECRET_KEY = 'veCZigT5X8e1EHXcR30nNjvwnVDvbIsI'
APP_ID = '1400799929'
SIGN_NAME='新注册公众号'
TEMPLATE_ID='1720550'

sms.py

# -*- coding: utf-8 -*-
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
# 导入对应产品模块的client models。
from tencentcloud.sms.v20210111 import sms_client, models

# 导入可选配置类
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
import json

# 生成n位验证码的函数
import random

from libs.send_sms_v3 import settings

def get_code(number=4):
    code=''
    for i in range(number):
        code+=str(random.randint(0,9))
    return code


def send_sms(code,mobile):
    try:
        # 必要步骤:
        # 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
        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 = 60  # 请求超时时间,单位为秒(默认60秒)
        httpProfile.endpoint = "sms.tencentcloudapi.com"  # 指定接入地域域名(默认就近接入)

        # 非必要步骤:
        # 实例化一个客户端配置对象,可以指定超时时间等配置
        clientProfile = ClientProfile()
        clientProfile.signMethod = "TC3-HMAC-SHA256"  # 指定签名算法
        clientProfile.language = "en-US"
        clientProfile.httpProfile = httpProfile
        client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
        req = models.SendSmsRequest()
        req.SmsSdkAppId = settings.APP_ID
        req.SignName = settings.SIGN_NAME
        req.TemplateId = settings.TEMPLATE_ID
        req.TemplateParamSet = [code, '1']
        req.PhoneNumberSet = ["+86176XXXX7381"]
        req.SessionContext = ""
        req.ExtendCode = "" 
        req.SenderId = ""
        resp = client.SendSms(req)

        # 输出json格式的字符串回包
        # print(resp.to_json_string(indent=2))
        
        res=json.loads(resp.to_json_string(indent=2))
        # 从json格式的字符串回包中获取Code信息,返回
        if res.get('SendStatusSet')[0].get('Code')=='Ok':
            return True

    except TencentCloudSDKException as err:
        print(err)

短信验证接口

视图类

from .models import User
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from utils.common_response import APIResponse
from rest_framework.exceptions import APIException
from .serilizer import UserLoginSerializer
from libs.send_sms_v3 import get_code,send_sms

class UserView(GenericViewSet):
    queryset = User.objects.filter(is_active=True).all()
    serializer_class = UserLoginSerializer
    
    @action(methods=['POST'],detail=False)
    def send_msg(self,request):
        try:
            mobile = request.data['mobile']
            # 生成验证码
            code=get_code()
            # 使用异步发送短信
            t = Thread(target=send_sms,args=[code,mobile])
            t.start()
            return APIResponse(msg='短信已发送')
            
            # 同步发送短信
            # res = send_sms(code,mobile)
            # if res:
            #     return APIResponse(msg='发送成功')
            # else:
            #     return APIResponse(code=101,msg='发送失败')
        except Exception as e:
            raise APIException(str(e))  

路由

from rest_framework.routers import SimpleRouter
from . import views

router = SimpleRouter()
# 访问 http://127.0.0.1:8000/api/v1/user/userinfo/send_msg/
router.register('userinfo', views.UserView, 'userinfo')

urlpatterns = [

]

urlpatterns += router.urls

短信登录接口

视图类的方法中的逻辑

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

视图类

视图类初级代码

# views.py
class UserView(GenericViewSet):
    queryset = User.objects.filter(is_active=True).all()
    serializer_class = UserLoginSerializer

    @action(methods=['POST'],detail=False)
    def login_mul(self,request,*args,**kwargs):
        # 可以把login_mul与login_smg中相同的代码抽取出来,放到_login方法中,直接调用即可
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)  # 执行这句话,会执行字段自己校验,局部钩子、全局钩子
        token=ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token,username=username)

    @action(methods=['POST'],detail=False)
    def send_msg(self,request):
        try:
            mobile = request.data['mobile']
            # 生成验证码
            code=get_code()
            # 一定要找个地方存一下,存到缓存中,key值唯一
            # 目前没学redis,暂时先存在内存中【项目部署会有坑】,后期要使用redis作为缓存
            cache.set('sms_code_%s'%mobile,code)

            # 使用异步发送短信
            t = Thread(target=send_sms,args=[code,mobile])
            t.start()
            return APIResponse(msg='短信已发送')
        
        except Exception as e:
            raise APIException(str(e))


    @action(methods=['POST'], detail=False)
    def login_smg(self, request,*args,**kwargs):
        # 此处代码后面进行封装
        ser = UserMobileSerializer(data=request.data)
        ser.is_valid(raise_exception=True)  # 执行这句话,会执行字段自己校验,局部钩子、全局钩子
        token=ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token,username=username)


    def _login(self,request,*args,**kwargs):
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)
        token = ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token,username=username)

视图类代码封装

# views.py
class UserView(GenericViewSet):
    queryset = User.objects.filter(is_active=True).all()
    serializer_class = UserLoginSerializer

  # 多方式登录 @action(methods
=['POST'],detail=False) def login_mul(self,request,*args,**kwargs): return self._login(request,*args,**kwargs)   
  # 发送短信 @action(methods
=['POST'],detail=False) def send_msg(self,request): try: mobile = request.data['mobile'] # 生成验证码 code=get_code() # 一定要找个地方存一下,存到缓存中,key值唯一 # 目前没学redis,暂时先存在内存中【项目部署会有坑】,后期要使用redis作为缓存 cache.set('sms_code_%s'%mobile,code) # 使用异步发送短信 t = Thread(target=send_sms,args=[code,mobile]) t.start() return APIResponse(msg='短信已发送') except Exception as e: raise APIException(str(e)) # login_mul与login_smg的唯一区别就是使用的序列化类不一样,可以重写get_serializer_class方法 # 如果访问login_smg方法,使用的序列化类是UserMobileSerializer # 如果是其他,还使用之前的序列化类get_serializer_class def get_serializer_class(self): if self.action == 'login_msg': return UserMobileSerializer else: return super().get_serializer_class()   # 短信验证码登录 @action(methods=['POST'], detail=False) def login_msg(self, request,*args,**kwargs): return self._login(request,*args,**kwargs) # 封装一个_login方法 def _login(self,request,*args,**kwargs): ser = self.get_serializer(data=request.data) ser.is_valid(raise_exception=True) token = ser.context.get('token') username = ser.context.get('username') return APIResponse(token=token,username=username)

序列化类

序列化类初级代码

# serializer.py
1 多方式登录序列化类
# 这个序列化类用来校验字段,不用来做序列化和反序列化
class UserLoginSerializer(serializers.ModelSerializer):
    # 重写username字段,把原来的校验规则去掉
    username = serializers.CharField()
    class Meta:
        model = User
        # username映射过来,是唯一的,字段自己的校验就过不了
        fields = ['username','password']

    def validate(self, attrs):
        """
        1.取出前端传入的用户名和密码
        2.通过用户名和密码去数据库查询用户
        3.如果查询到,签发token
        4.返回给前端登录成功
        """
        # attrs是前端传入的数据,经过字段自己校验和局部钩子校验过后的数据
        user=self._get_user(attrs)
        token=self._get_token(user)
        # 把用户名和token放到ser的context中
        self.context['token']=token
        self.context['username']=user.username
        return attrs

    def _get_user(self,attrs):
        username=attrs.get('username')
        password=attrs.get('password')
        if re.match(r'^1[3-9][0-9]{9}$',username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$',username):
            user=User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            return user
        else:
            # 用户不存在或密码错误   这里的代码,还是在全局钩子中执行,全局钩子校验失败,要抛异常,所以在这抛异常
            raise APIException('用户不存在或密码错误')

    def _get_token(self,user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token

2 验证码短信发送与登录校验序列化类
class UserMobileSerializer(serializers.ModelSerializer):
    mobile=serializers.CharField()
    code=serializers.CharField()
    class Meta:
        model = User
        # code不是表字段,要重写,mobile有唯一约束,需要重写
        fields = ['mobile','code']

    def validate(self, attrs):
        """
        1.取出前端传入的用户名和密码
        2.通过用户名和密码去数据库查询用户
        3.如果查询到,签发token
        4.返回给前端登录成功
        """
        # attrs是前端传入的数据,经过字段自己校验和局部钩子校验过后的数据
        user=self._get_user(attrs)
        token=self._get_token(user)
        # 把用户名和token放到ser的context中
        self.context['token']=token
        self.context['username']=user.username
        return attrs

    def _get_user(self,attrs):
        code =attrs.get('code')
        mobile=attrs.get('mobile')
        # 从缓存中取出
        old_code=cache.get('sms_code_%s'%mobile)
        if old_code and old_code==code:
            # 根据手机号,查到用户
            user=User.objects.filter(mobile=mobile).first()
            if user:
                return user
            else:
                raise APIException('用户不存在')
        else:
            raise APIException('验证码错误')

    def _get_token(self,user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token

序列化类代码封装

import re
from .models import User
from rest_framework import serializers
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
from django.core.cache import cache


# 写一个父类,两个序列化类都继承这个父类,只要在各自的类中重写_get_user方法即可
class BaseUserSerializer:
    def validate(self, attrs):
        """
        1.取出前端传入的用户名和密码
        2.通过用户名和密码去数据库查询用户
        3.如果查询到,签发token
        4.返回给前端登录成功
        """
        # attrs是前端传入的数据,经过字段自己校验和局部钩子校验过后的数据
        user=self._get_user(attrs)
        token=self._get_token(user)
        # 把用户名和token放到ser的context中
        self.context['token']=token
        self.context['username']=user.username
        return attrs

    def _get_user(self,attrs):
        raise Exception('你必须重写它')

    def _get_token(self,user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token


# 这个序列化类用来校验字段,不用来做序列化和反序列化
class UserLoginSerializer(BaseUserSerializer,serializers.ModelSerializer,):
    # 重写username字段,把原来的校验规则去掉
    username = serializers.CharField()
    class Meta:
        model = User
        # username映射过来,是唯一的,字段自己的校验就过不了
        fields = ['username','password']

    def _get_user(self,attrs):
        username=attrs.get('username')
        password=attrs.get('password')
        if re.match(r'^1[3-9][0-9]{9}$',username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$',username):
            user=User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            return user
        else:
            # 用户不存在或密码错误   这里的代码,还是在全局钩子中执行,全局钩子校验失败,要抛异常,所以在这抛异常
            raise APIException('用户不存在或密码错误')


class UserMobileSerializer(BaseUserSerializer,serializers.ModelSerializer):
    mobile=serializers.CharField()
    code=serializers.CharField()
    class Meta:
        model = User
        # code不是表字段,要重写,mobile有唯一约束,需要重写
        fields = ['mobile','code']

    def _get_user(self,attrs):
        code =attrs.get('code')
        mobile=attrs.get('mobile')
        # 从缓存中取出
        old_code=cache.get('sms_code_%s'%mobile)
        if old_code and old_code==code:
            # 根据手机号,查到用户
            user=User.objects.filter(mobile=mobile).first()
            if user:
                return user
            else:
                raise APIException('用户不存在')
        else:
            raise APIException('验证码错误')

短信注册接口

视图类

from rest_framework.mixins import CreateModelMixin

class RegisterUserView(GenericViewSet,CreateModelMixin):
    queryset = User.objects.all()
    serializer_class = RegisterSerializer
    def create(self, request, *args, **kwargs):

        # 两种写法:
        # 方法一:调用父类的create方法,但是父类的create方法中有serializer.data,只要调用它,就会触发序列化,必须在序列化类中指明字段write_only或read_only
        # super().create(request,*args,**kwargs)

        # 方法二:把父类中的create方法拿过来进行修改
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        return APIResponse(msg='注册成功')

序列化类

# 数据校验,反序列化
class RegisterSerializer(serializers.ModelSerializer):
    code=serializers.CharField(max_length=4)
    class Meta:
        model=User
        fields=['mobile','code','password']
        extra_kwargs={
            'code':{'write_only':True},
            'password':{'write_only':True}
        }

    def validate(self, attrs):  # 全局钩子
        '''
        1.取出前端传入的code,校验code是否正确
        2.把username设置成手机号(你可以随机生成)
        3.code不是数据库的字段,从attr中剔除
        '''
        mobile=attrs.get('mobile')
        code=attrs.get('code')
        old_code=cache.get('sms_code_%s'%mobile)
        if old_code and old_code==code:
            attrs['username']=mobile
            attrs.pop('code')
        else:
            raise APIException('验证码校验失败')
        return attrs

    # 一定要重写create,因为密码是明文,如果不重写,存入到数据库也是明文
    def create(self, validated_data):   
        # 创建user用户用户
        user = User.objects.create_user(**validated_data)
        return user

 

posted @ 2023-03-05 14:20  莫~慌  阅读(202)  评论(0编辑  收藏  举报