文章目录
- 九、短信验证码
- 9.1 短信验证码逻辑分析
- 9.2 容联云通讯短信平台
- 9.2.1 容联云通讯短信平台介绍
- 9.2.2 容联云通讯短信SDK测试
- 9.2.2.1 美多商城meiduo_mall.apps.verifications.libs中新建yuntongxun包,结构如下:
- 9.2.2.2 ccp_sms.py代码
- 9.2.2.3 CCPRestSDK.py ,python3代码
- 9.2.2.4 测试发送短信
- 9.2.3 封装发送短信单例类
- 9.3 短信验证码接口设计和定义
- 9.4 短信验证码后端逻辑
- 9.5 短信验证码前端逻辑
九、短信验证码
9.1 短信验证码逻辑分析
9.2 容联云通讯短信平台
了解容联云通讯平台和短信SDK的使用方式,
9.2.1 容联云通讯短信平台介绍
- 容联云官网:https://www.yuntongxun.com/
2.注册登录
- 通过认证,企业认证或个人认证,提交申请后一般需要第二天通过。发送短信0.06元/条,注册赠送8元,我发过一条了。
- 添加容联云子应用,通过认证后可以上线应用
- 添加测试号码
- 短信模板
- Python Demo中模板短信的使用说明
https://doc.yuntongxun.com/p/5a533e0c3b8496dd00dce08c
- 开发文档-SDK接口文件
https://www.yuntongxun.com/doc/ready/demo/1_4_1_2.html
9.2.2 容联云通讯短信SDK测试
9.2.2.1 美多商城meiduo_mall.apps.verifications.libs中新建yuntongxun包,结构如下:
9.2.2.2 ccp_sms.py代码
#-*- coding: UTF-8 -*-
from meiduo_mall.apps.verifications.libs.yuntongxun.CCPRestSDK import REST
# import ConfigParser
import ssl
# 全局取消证书验证
ssl._create_default_https_context = ssl._create_unverified_context
#主帐号
accountSid= '8aaf07 这里填真实的主账号 5b0963df1';
#主帐号Token
accountToken= 'b809 这里填真实的Token 4018733';
#应用Id
appId='8a21 这里填真实的AppID 10d53ba6';
#请求地址,格式如下,不需要写http://
serverIP='app.cloopen.com';
#请求端口
serverPort='8883';
#REST版本号
softVersion='2013-12-26';
# 发送模板短信
# @param to 手机号码
# @param datas 内容数据 格式为数组 例如:{'12','34'},如不需替换请填 ''
# @param $tempId 模板Id
def sendTemplateSMS(to,datas,tempId):
#初始化REST SDK
rest = REST(serverIP,serverPort,softVersion)
rest.setAccount(accountSid,accountToken)
rest.setAppId(appId)
result = rest.sendTemplateSMS(to,datas,tempId)
print(result)
#sendTemplateSMS(手机号码,内容数据,模板Id)
if __name__ == '__main__':
# 注意测试的短信模板编号为1,短信验证码为123456,有效期为5分钟
sendTemplateSMS('13953800865', ['123456', 5], 1)
9.2.2.3 CCPRestSDK.py ,python3代码
这个文件官网示例文件使用python2.7写的,有五六个地方需要修改,这是修改测试过的python3代码
修改内容主要包括:
1、头部导包
2、MD5加密
3、req.add_data
4、base64加密等
from hashlib import md5
import base64
import datetime
import urllib.request # py3
import json
from meiduo_mall.apps.verifications.libs.yuntongxun.xmltojson import xmltojson # py3
from xml.dom import minidom
class REST:
AccountSid=''
AccountToken=''
AppId=''
SubAccountSid=''
SubAccountToken=''
ServerIP=''
ServerPort=''
SoftVersion=''
Iflog=True #是否打印日志
Batch='' #时间戳
BodyType = 'xml'#包体格式,可填值:json 、xml
# 初始化
# @param serverIP 必选参数 服务器地址
# @param serverPort 必选参数 服务器端口
# @param softVersion 必选参数 REST版本号
def __init__(self,ServerIP,ServerPort,SoftVersion):
self.ServerIP = ServerIP;
self.ServerPort = ServerPort;
self.SoftVersion = SoftVersion;
# 设置主帐号
# @param AccountSid 必选参数 主帐号
# @param AccountToken 必选参数 主帐号Token
def setAccount(self,AccountSid,AccountToken):
self.AccountSid = AccountSid;
self.AccountToken = AccountToken;
# 设置子帐号
#
# @param SubAccountSid 必选参数 子帐号
# @param SubAccountToken 必选参数 子帐号Token
def setSubAccount(self,SubAccountSid,SubAccountToken):
self.SubAccountSid = SubAccountSid;
self.SubAccountToken = SubAccountToken;
# 设置应用ID
#
# @param AppId 必选参数 应用ID
def setAppId(self,AppId):
self.AppId = AppId;
def log(self,url,body,data):
print('这是请求的URL:')
print (url);
print('这是请求包体:')
print (body);
print('这是响应包体:')
print (data);
print('********************************')
# 创建子账号
# @param friendlyName 必选参数 子帐号名称
def CreateSubAccount(self, friendlyName):
self.accAuth()
nowdate = datetime.datetime.now()
self.Batch = nowdate.strftime("%Y%m%d%H%M%S")
#生成sig
signature = self.AccountSid + self.AccountToken + self.Batch;
signature = signature.encode('utf-8') # py3
# sig = md5.new(signature).hexdigest().upper()
sig = md5(signature).hexdigest().upper() # py3
#拼接URL
url = "https://"+self.ServerIP + ":" + self.ServerPort + "/" + self.SoftVersion + "/Accounts/" + self.AccountSid + "/SubAccounts?sig=" + sig
#生成auth
src = self.AccountSid + ":" + self.Batch;
# auth = base64.encodestring(src).strip()
auth = base64.encodestring(src.encode()).strip() # py3
req = urllib.request.Request(url)
self.setHttpHeader(req)
req.add_header("Authorization", auth)
#xml格式
body ='''<?xml version="1.0" encoding="utf-8"?><SubAccount><appId>%s</appId>\
<friendlyName>%s</friendlyName>\
</SubAccount>\
'''%(self.AppId, friendlyName)
if self.BodyType == 'json':
#json格式
body = '''{"friendlyName": "%s", "appId": "%s"}'''%(friendlyName,self.AppId)
data=''
# req.add_data(body)
req.data = body.encode() # py3
try:
res = urllib.request.urlopen(req);
data = res.read()
res.close()
if self.BodyType=='json':
#json格式
locations = json.loads(data)
else:
#xml格式
xtj=xmltojson()
locations=xtj.main(data)
if self.Iflog:
self.log(url,body,data)
return locations
except Exception as error:
if self.Iflog:
self.log(url,body,data)
return {'172001':'网络错误'}
9.2.2.4 测试发送短信
1.vscode终端输出
- 测试手机收到的短信
9.2.3 封装发送短信单例类
问题:如果同时发送多个短信验证码,那么就会同时创建多个RET SDK的对象,会消耗很多额外的内存空间。
解决方法:使用单例类,它的特点是只有一个实例存在
使用场景:当我们希望在整个系统中,某个类只出现一个实例时,就可以使用单例类设计模式
改写后的代码:
#-*- coding: UTF-8 -*-
from meiduo_mall.apps.verifications.libs.yuntongxun.CCPRestSDK import REST
# import ConfigParser
import ssl
# 全局取消证书验证
ssl._create_default_https_context = ssl._create_unverified_context
#主帐号
accountSid= '8aaf07087a331dc7017afb85b0963df1';
#主帐号Token
accountToken= 'b809c84015db41c8a4a3d84224018733';
#应用Id
appId='8a216da87a332d53017afb8d10d53ba6';
#请求地址,格式如下,不需要写http://
serverIP='app.cloopen.com';
#请求端口
serverPort='8883';
#REST版本号
softVersion='2013-12-26';
# 发送模板短信
# @param to 手机号码
# @param datas 内容数据 格式为数组 例如:{'12','34'},如不需替换请填 ''
# @param $tempId 模板Id
# def sendTemplateSMS(to,datas,tempId):
# #初始化REST SDK
# rest = REST(serverIP,serverPort,softVersion)
# rest.setAccount(accountSid,accountToken)
# rest.setAppId(appId)
# result = rest.sendTemplateSMS(to,datas,tempId)
# print(result)
class CCP(object):
""" 发送短信验证码的单例类 """
def __new__(cls, *args, **kwargs):
""" 定义单例化的初始化方法,返回值为单例 """
# 判断单例是否存在,利用动态赋值的_instance属性。如果单例不存在,就初始化单例
if not hasattr(cls, '_instance'):
cls._instance = super(CCP, cls).__new__(cls, *args, **kwargs)
#初始化REST SDK,赋值给单例属性,实现与单例同生共死,唯一存在
cls._instance.rest = REST(serverIP,serverPort,softVersion)
cls._instance.rest.setAccount(accountSid,accountToken)
cls._instance.rest.setAppId(appId)
# 返回单例
return cls._instance
def send_template_sms(self, to, datas, tempId):
"""
定义对象方法,发送短信验证码
to:手机号码,字符串,多个手机号码用逗号分隔
datas:发送内容,双元素列表,第一个元素为验证码字符串,第二个元素为整数有效时间(分钟)
tempID:模板ID,测试模板为1
返回值:成功:0,失败:-1
"""
result = self.rest.sendTemplateSMS(to,datas,tempId)
print(result)
# 根据发送是否成功返回0或-1
if result.get('statusCode') == '000000':
return 0
else:
return -1
#sendTemplateSMS(手机号码,内容数据,模板Id)
if __name__ == '__main__':
# 注意测试的短信模板编号为1,短信验证码为123456,有效期为5分钟
# sendTemplateSMS('13953800865', ['123456', 5], 1)
# 单例类发送短信验证码
CCP().send_template_sms('13953800865', ['6543258', 5], 1)
9.3 短信验证码接口设计和定义
9.4 短信验证码后端逻辑
- verifications.urls.py中
from django.urls import path, re_path
from . import views
app_name = 'verifications'
urlpatterns = [
# 图形验证码,re_path路由正则校验,响应json数据,不需要重定向,也就不需要命名空间
re_path(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImageCodeView.as_view()),
# 短信验证码,re_path路由正则校验,响应json数据,不需要重定向,也就不需要命名空间
re_path(r'^sms_codes/(?P<mobile>1[3-9]\d{9})/$', views.SMSCodeView.as_view()),
]
- verifications.views.py中的短信验证码类视图
from django.views import View
from django_redis import get_redis_connection
from django import http
import random, logging
from meiduo_mall.apps.verifications.libs.captcha.captcha import captcha
from . import constants
from meiduo_mall.utils.response_code import RETCODE
from meiduo_mall.apps.verifications.libs.yuntongxun.ccp_sms import CCP
# 创建日志输出器
logger = logging.getLogger('django')
class SMSCodeView(View):
""" 短信验证码 """
def get (self, request, mobile):
"""
param:request,请求对象;mobile,手机号
return:JSON
"""
""" 接收和校验参数 """
# 接收参数
image_code_client = request.GET.get('image_code')
uuid = request.GET.get('uuid')
# 校验参数,mobile不需要视图内校验,在路由处已经校验完毕了,错误进不了视图
if not all([image_code_client, uuid]):
return http.HttpResponseForbidden('缺少必传参数!')
# 判断用户是否频繁发送短信验证码
redis_conn = get_redis_connection('verify_code') # 创建redis库的连接
send_flag = redis_conn.get(f'send_flag_{mobile}')
if send_flag: # 已存在
return http.JsonResponse({'code': RETCODE.THROTTLINGERR, 'errmsg': '发送短信验证码过于频繁!'})
"""" 主体业务逻辑 """
# 1.从redis库中提取图形验证码
image_code_server = redis_conn.get(f'img_{uuid}')
if image_code_server is None:
return http.JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '图形验证码已失效!'})
# 2.删除redis中存储的图形验证码
redis_conn.delete(f'img_{uuid}')
# 3.对比图形验证码
image_code_server = image_code_server.decode() # 提取的数据时bytes类型,需要转换为字符串
if image_code_client.lower() != image_code_server.lower(): # 全部转为小写
return http.JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '输入图形验证码有误!'})
# 4.生成短信验证码:随机6位数字,不足前面补0, 000007
sms_code = '%06d' % random.randint(0, 999999)
logger.info(sms_code) # 手动输出短信验证码的日志
# 5.保存短信验证码,为优化redis的性能,使用管道队列操作
# 5.1 创建redis pipeline管道队列
pl = redis_conn.pipeline()
# 5.2 将命令添加到队列中
pl.setex(f'sms_{mobile}', constants.SMS_CODE_REDIS_EXPIRES, sms_code) # sms_code存储到redis数据库
pl.setex(f'send_flag_{mobile}', constants.SEND_SMS_CODE_INTERVAL, 1) # 保存短信验证码标记,有效期60秒,标记1表示60秒内给该手机发送了验证码
# 5.3 执行队列命令
pl.excute()
# 6.单例类发送短信验证码
CCP().send_template_sms(mobile, [sms_code, constants.SMS_CODE_REDIS_EXPIRES // 60], constants.SEND_SMS_TEMPLATE_ID)
""" 响应结果 """
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '短信验证码发送成功!'})
9.5 短信验证码前端逻辑
- register.html中的短信验证码部分
<li>
<label for="">短信验证码</label>
<input type="text" v-model="sms_code" @blur="check_sms_code" name="sms_code" id="sms_code" class="msg_input">
<a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]</a>
<span class="error_tip" v-show="error_sms_code">[[ error_sms_code_message]]</span>
</li>
- register.js中的方法
//发送手机验证码
send_sms_code(){
//避免恶意用户频繁点击获取短信验证码的A标签
if (this.send_flag == true) { //已经点击了发送短信验证码
return;
}
this.send_flag = true;
//校验用户输入的mobile和image_code
this.check_mobile();
this.check_image_code();
if (this.error_image_code == true || this.error_mobile == true) {
this.send_flag == false;
return;
}
//?后面为查询字符串参数
let url = '/sms_codes/'+ this.mobile +'/?image_code=' + this.image_code + '&uuid=' + this.uuid;
axios.get(url, {
responseType: 'json'
})
.then(response => {
if (response.data.code == '0') { //发送短信验证码成功
//展示倒计时60S效果 setInterval('回调函数', '时间间隔1000毫秒')
let num = 60;
let t = setInterval(() => { // t 为定时器编号
if (num == 1){ //倒计时即将结束
clearInterval(t); // 停止回调函数的执行
this.sms_code_tip = '获取短信验证码'; // 还原 sms_code_tip 的提示信息
this.generate_image_code(); //重新生成图形验证码
this.send_flag == false;
} else { // 正在倒计时
num -= 1;
this.sms_code_tip = num + '秒';
}
}, 1000)
} else {
if (response.data.code == '4001') { // 图形验证码错误
// 渲染错误信息
this.error_image_code_message = response.data.errmsg;
this.error_image_code = true;
this.send_flag == false;
}
}
})
.catch(error => {
console.log(error.response);
this.send_flag == false;
})
},