目录
一、腾讯云短信开发
上节课我们介绍了如何申请腾讯云短信,今天具体讲解后续的步骤
腾讯云短信文档:https://cloud.tencent.com/document/product/382/43196
使用腾讯云发送短信有两种方式——API和SDK,有sdk优先用sdk
sdk版本介绍
-
3.0版本,云操作的sdk,不仅仅有发送短信,还有云功能的其他功能
-
2.0版本,简单,只有发送短信功能
在学习了两个版本的区别之后,我们了解到3.0版本功能更强,因此我们这里就安装3.0版本的
1.1SDK安装操作流程
步骤一
打开文档,阅读相关教程
步骤二
- 方式一:pip安装
使用pip安装没有什么需要介绍的,根据文档的命令安装即可
pip install --upgrade tencentcloud-sdk-python
ps:--upgrade可以不要,是用于更新模块的
- 方式二:源码下载
操作一:使用克隆命令下载,或直接在网页下载zip文件(zip方式下载可能速度慢)
ps:我们使用源码安装的原因,是因为有些情况下SDK安装的模块一些功能没有,因此需要介绍一下源码安装的方式
操作二:安装
首先我们需要进入到源码文件所在的目录内
接着根据文档提示我们运行命令安装
这里我们直接在项目的terminal中安装
python setup.py install
运行命令后需要等待一会
安装后查看结果
ps:如果出问题了,就把这个ten开头的模块删了重新装
1.2 发送短信
根据官网的文档:https://cloud.tencent.com/document/product/382/43196
我们可以直接把人家的演示代码复制过来使用,注释中有详细的讲解
# -*- 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:
# 必要步骤:
# 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
# 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
# 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
# 以免泄露密钥对危及你的财产安全。
# SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi
cred = credential.Credential("secretId", "secretKey")
# 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 = "1400787878"
# 短信签名内容: 使用 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 = "449739"
# 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空
req.TemplateParamSet = ["1234"]
# 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
# 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
req.PhoneNumberSet = ["+8613711112222"]
# 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回
req.SessionContext = ""
# 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手]
req.ExtendCode = ""
# 国际/港澳台短信 senderid(无需要可忽略): 国内短信填空,默认未开通,如需开通请联系 [腾讯云短信小助手]
req.SenderId = ""
resp = client.SendSms(req)
# 输出json格式的字符串回包
print(resp.to_json_string(indent=2))
# 当出现以下错误码时,快速解决方案参考
# - [FailedOperation.SignatureIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.signatureincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
# - [FailedOperation.TemplateIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.templateincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
# - [UnauthorizedOperation.SmsSdkAppIdVerifyFail](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunauthorizedoperation.smssdkappidverifyfail-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
# - [UnsupportedOperation.ContainDomesticAndInternationalPhoneNumber](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunsupportedoperation.containdomesticandinternationalphonenumber-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
# - 更多错误,可咨询[腾讯云助手](https://tccc.qcloud.com/web/im/index.html#/chat?webAppId=8fa15978f85cb41f7e2ea36920cb3ae1&title=Sms)
except TencentCloudSDKException as err:
print(err)
根据注释中的提示,我们得知需要用到腾讯云账户密钥对secretId,secretKey
查询地址:https://console.cloud.tencent.com/cam/capi
点入该网站后,如果是第一次使用,需要点击左侧的新建密匙
获取后我们直接在代码的相应位置写死(以后使用的时候推荐在环境变量中使用,比较安全)
接着我们需要去短信控制台获取短信应用ID
查询地址:https://console.cloud.tencent.com/smsv2/app-manage
接着我们根据注释的提示查询签名信息(国内和国外用的地址不一样)
查询地址:https://console.cloud.tencent.com/smsv2/csms-sign
下一步是获取模板ID(国内和国外用的地址不一样)
查询地址:https://console.cloud.tencent.com/smsv2/csms-template
填写完上述信息后,我们需要给短信填写发送内容
返回的信息如下
1.3封装发送短信功能
测试发送短信的功能后,我们需要把他进行封装
这里我们把发送短信功能相关的代码都放到libs目录下,并且创建一个包来存储这些文件
结构如下:
-libs下:
send_sms_v3
__init__.py
settings.py
sms.py
接着我们把发送短信的代码封装到sms中(发送成功返回True,否则返回Fales),写成一个函数
并且写一个生成四位随机数字的函数
然后我们再把一些配置信息存放到settings.py中设置成常量,导入使用
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 random
from . import settings
import json
'这里就是封装后的发送短信的代码'
def send_sms(code, mobile):
try:
cred = credential.Credential(settings.SECRETID, settings.SECRETKEY)
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 = settings.APP_ID
req.SignName = settings.SIGN_NAME
req.TemplateId = settings.TEMPLATE_ID
req.TemplateParamSet = [code, "5"]
'这里对验证码和电话做了一个动态匹配'
req.PhoneNumberSet = ["+86" + mobile]
req.SessionContext = ""
req.ExtendCode = ""
req.SenderId = ""
resp = client.SendSms(req)
# 输出json格式的字符串回包
# print(resp.to_json_string(indent=2))
'这里我们看到他返回的是json格式数据,因此我们需要对他进行反序列化'
res = json.loads(resp.to_json_string(indent=2))
'这里我们需要根据短信是否发送成功做一个判断,给出不同的返回结果'
if res.get('SendStatusSet')[0].get('Code') == 'Ok':
return True
else:
return False
except TencentCloudSDKException as err:
print(err)
return False
'这里我们定义一个函数获取四位的随机数字'
def get_code(number=4):
code = ''
for i in range(number):
code += str(random.randint(0, 9))
'这里如果不转换数据类型会报错的'
'因为python是个动态强类型语言,不同类型的值不能相加'
return code
settings.py
SECRETID = 'secretId'
SECRETKEY = 'secretKey'
APP_ID = '短信应用ID'
SIGN_NAME = '短信签名内容'
TEMPLATE_ID = '模板 ID'
这里是对包的双下init文件进行一个配置,让外部在导入我们编写的函数时可以直接导入名称
# __init__.py
from .sms import get_code,send_sms
二、短信验证码接口
class UserView(GenericViewSet):
serializer_class = UserLoginSerializer
queryset = User.objects.all().filter(is_active=True)
@action(methods=['POST'], detail=False)
def send_sms(self, request):
try:
mobile = request.data['mobile']
# 生成验证码
code = get_code()
res = send_sms_ss(code, mobile) # 同步发送,后期可以改成异步 后期学了celery可以加入异步 目前咱们可以使用 多线程
if res:
return APIResponse(msg='发送成功')
else:
return APIResponse(code=101, msg='发送失败')
except Exception as e:
raise APIException(str(e))
三、短信登录接口
目前位置我们写完了短信验证码的发送接口
现在我们就来写短信验证码登陆的接口
短信登陆接口的逻辑
前端发送请求后我们收到的数据格式如下
# 前端---》{mobile:122334,code:8888}---->post----》
视图类的方法中的逻辑
1 取出手机号和验证码
2 校验验证码是否正确(发送验证码接口,存储验证码)
-session:根本不用
-全局变量:不好,可能会取不到,集群环境中
-缓存:django 自带缓存
-from django.core.cache import cache
-cache.set()
'添加数据'
-cache.get()
'查找数据'
3 根据手机号查询用户,如果能查到
4 签发token
5 返回给前端
为什么不使用全局变量存储验证码?
django的web服务网关接口,处理请求的时候使用的是 多进程多线程架构的方式
因为多个进程间数据不共享,如果请求来的时候被不同的进程处理了,就会导致找不到我们存储的验证码
因此我们需要使用cache或radis存储数据
经过分析,得到的修改后的发送短信验证码接口代码
@action(methods=['POST'], detail=False)
def send_sms(self, request):
try:
mobile = request.data.get('mobile')
# 生成验证码
code = get_code()
# 当我们发送了验证码之后,我们需要存储这个随机字符串,用于比对,这里我们存在cache中
# 这里我们在存储的时候需要把变量名设置成唯一的,这样我们获取的时候才不会被别的记录覆盖
cache.set('sms_code_%s'%mobile, code)
# 使用异步发送短信
t = Thread(target=send_sms_st, args=[code, mobile])
t.start()
return APIResponse(msg='短信已发送')
except Exception as e:
raise APIException(str(e))
3.1 视图类
在编写短信登陆接口的时候,我们也想使用序列化类来校验,因此我们需要在内部重写get_serializer_class方法(因为这个方法返回的就是serializer_class)
def get_serializer_class(self):
'''
如果我们要访问login_sms函数,需要更换序列化类
因此这里通过重写实现序列化类的更改
'''
if self.action == 'login_sms':
return UserMobileLoginSerializer
else:
return super().get_serializer_class()
重写了这个方法后,我们先不去编写对应的序列化类,先把视图类中的登陆的代码拿出来封装一下,这样之前编写的多种方式登陆接口和短信登陆接口都可以少些重复的代码
@action(methods=['POST'], detail=False)
def send_mul(self, request, *args, **kwargs):
return self._login(request)
@action(methods=['POST'], detail=False)
def login_sms(self, request, *args, **kwargs):
return self._login(request)
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)
接下来我们去编写对应的序列化类
4.2 序列化类
根据现在的需求,我们可以分析得到下列信息:
- 我们需要编写的两个序列化类,基本功能都相似,除了_get_user中进行校验的逻辑需要变更,因此这里我们考虑把功能的方法提取到一个父类中,通过继承使用
- 因为_get_user方法中的逻辑不一样,所以我们就在父类中编写该方法的时候,模仿源码中的方式,写成如果不改写该方法就主动抛异常
提取代码整合到父类前的代码
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
import rest_framework_jwt
from django.core.cache import cache
'这个序列化类不做序列化和反序列化,只是用于登陆的校验'
class UserLoginSerializer(serializers.ModelSerializer):
username = serializers.CharField()
'''
这里需要我们重新定义字段自己的校验
否则就会因为用户表中定义的时候,规定的唯一限制导致报错
unique=True
'''
class Meta:
model = User
fields = ['username', 'password']
'这是全局钩子'
def validate(self, attrs):
'''
这里我们使用把校验逻辑放在序列化类中的方式实现多方式的登陆接口
1、取出前端传入的用户名和密码
2、通过用户名和密码去数据库查询对象
3、如果能查到,签发token
4、返回数据给前端
'''
'这里也是在之前研究源码的时候了解到的,在使用序列化类校验的时候,是想校验字段本身的条件,然后是局部钩子,再是全局钩子'
'因此这里的attrs就是经过前两者校验的数据,就是{username:xxx, password:123}'
user = self._get_user(attrs)
token = self._get_token(user)
'这里我们还需要把token和用户名放到ser的context中去'
'''
在视图函数中我们把user和token存储在content中
但是这不是随意命名的,序列化类继承了ModelSerializer
我们可以看到ModelSerializer是Serializer
Serializer的父类是BaseSerializer
BaseSerializer中有__init__方法
前面的几个类中都没有
因此创建对象的时候就是 BaseSerializer创建的
而他的源码中创建了一个content属性
self._context = kwargs.pop('context', {})
因此我们可以传数据的时候传入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'^[3-9][0-9]{9}$', username):
user = User.objects.filter(mobile=username).first()
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()
if user and user.check_password(password):
return user
else:
raise APIException('用户名不存在或密码错误')
def _get_token(self, user):
'''
这里是根据源码分析的时候
我们提到JSONWebTokenAPIView就是他的序列化类
然后再他的validate方法中payload获取荷载(通过用户名)
然后通过荷载获得token
'''
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
class UserMobileLoginSerializer(serializers.ModelSerializer):
mobile = serializers.CharField()
code = serializers.CharField(max_length=4)
'''
因为这里的code不是我们模型表中的字段,因此我们需要重写
mobile字段因为唯一键的约束,所以也需要重写
'''
class Meta:
model = User
fields = ['mobile', 'code']
def validate(self, attrs):
user = self._get_user(attrs)
token = self._get_token(user)
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 code == old_code:
'判断验证码正确后,我们通过手机号数据库查找,找到了就登陆成功了'
user = User.objects.filter(mobile=mobile).first()
if user:
return user
else:
raise APIException('用户不存在')
else:
print(code)
print(old_code)
raise APIException('验证码错误')
def _get_token(self, user):
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
测试结果如下
提取代码整合到父类后的代码
整合代码后,我们需要注意父类的继承顺序,我们自行编写的BaseUserSerializer必须要放在前面
因为我们这样设计后,如果把BaseUserSerializer放在后面,就会导致序列化类先调用ModelSerializer中的validate
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
import rest_framework_jwt
from django.core.cache import cache
class BaseUserSerializer:
def validate(self, attrs):
user = self._get_user(attrs)
token = self._get_token(user)
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 = serializers.CharField()
'''
这里需要我们重新定义字段自己的校验
否则就会因为用户表中定义的时候,规定的唯一限制导致报错
unique=True
'''
class Meta:
model = User
fields = ['username', 'password']
def _get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
'接下来我们使用正则来匹配电话的格式'
if re.match(r'^[3-9][0-9]{9}$', username):
user = User.objects.filter(mobile=username).first()
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()
if user and user.check_password(password):
return user
else:
raise APIException('用户名不存在或密码错误')
class UserMobileLoginSerializer(BaseUserSerializer, serializers.ModelSerializer):
mobile = serializers.CharField()
code = serializers.CharField(max_length=4)
'''
因为这里的code不是我们模型表中的字段,因此我们需要重写
mobile字段因为唯一键的约束,所以也需要重写
'''
class Meta:
model = User
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 code == old_code:
'判断验证码正确后,我们通过手机号数据库查找,找到了就登陆成功了'
user = User.objects.filter(mobile=mobile).first()
if user:
return user
else:
raise APIException('用户不存在')
else:
print(code)
print(old_code)
raise APIException('验证码错误')
测试结果如下
四、短信注册接口
短信注册接口分析
- 前端发送的数据格式和请求方式
# 前端---》{mobile:1888344,code:8888,password:123}--->post
ps:注册的时候最好不要设计的太复杂,容易让客户萌生退意
4.1 路由
# http://127.0.0.1:8000/api/v1/user/register/ --->post 请求
router.register('register',views.RegisterUserView,'register')
4.2 视图类
class RegisterUserView(GenericViewSet, CreateModelMixin):
queryset = User.objects.all()
serializer_class = RegisterSerializer
'因为返回格式不对,所以我们这里使用派生方法重写一下'
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='注册成功')
4.3 序列化类
前期我们根据逻辑正常编写全局钩子即可
代码如下:
class RegisterSerializer(serializers.ModelSerializer):
'这里的code还是因为模型表中没有这个字段,所以需要我们重写'
code = serializers.CharField(max_length=4)
class Meta:
model = User
fields = ['mobile', 'code', 'password']
'全局钩子校验'
def validate(self, attrs):
'''
1 取出前端传入的code,校验code是否正确
2 把username设置成手机号(你可以随机生成),用户名如果不传,存库进不去
3 code 不是数据库的字段,从attrs中剔除
'''
mobile = attrs.get('mobile')
code = attrs.get('code')
old_code = cache.get('sms_code_%s' % mobile)
if code == old_code:
'当验证码校验成功后,我们把用户名设置成电话号码,然后删除code字段,因为创建用户的时候用不到code字段'
attrs['username'] = mobile
attrs.pop('code')
else:
raise APIException('验证码错误')
return attrs
'这里我们要重写create方法,因为不重写的话,密码会以明文的方式存储到数据库中'
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
return user
测试结果
但是这时我们去测试接口,他会报错,这是因为序列化类中,code和password字段(password影响不大)在序列化过程中因为code字段不存在而报错,而且数据库中成功写入了数据,解决方案有两种
方案一
改写create方法。不再使用super()去调用父类方法,我们可以看到父类中的create方法在返回值的时候传入了serializer.data这就会导致序列化校验的进行,这里我们只需要把headers变量部分的代码删除,然后返回的方式使用我们自定义的APIResponse即可
class CreateModelMixin:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
方案二
在序列化类中重写对应字段,并加上write_only=True约束,让序列化类不校验code字段
class RegisterSerializer(serializers.ModelSerializer):
# code 不是数据库字段,重写
mobile = serializers.CharField()
code = serializers.CharField(max_length=4, write_only=True)
class Meta:
model = User
fields = ['mobile', 'code', 'password']
extra_kwargs = {
'password': {'write_only': True}
}
def validate(self, attrs): # 全局钩子
'''
1 取出前端传入的code,校验code是否正确
2 把username设置成手机号(你可以随机生成),用户名如果不传,存库进不去
3 code 不是数据库的字段,从attrs中剔除
'''
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
def create(self, validated_data): # 一定要重写create,因为密码是明文,如果不重写,存入到数据库的也是明文
# validated_data={username:18888,mobile:18888,password:123}
# 创建用户
user = User.objects.create_user(**validated_data)
# 不要忘了return,后期,ser.data 会使用当前返回的对象做序列化
return user
测试结果