美多商城之短信验证码
1.短信验证码逻辑分析
- 知识要点:
- 保存短信验证码是为注册做准备的。
- 为了避免用户使用图形验证码恶意测试,后端提取了图形验证码后,立即删除图形验证码。
- Django不具备发送短信的功能,所以我们借助第三方的容联云通讯短信平台来帮助我们发送短信验证码。
2.容联云通讯短信平台
目的:了解荣联运通讯平台和短信SDK的使用方式
步骤:
- 荣联云通信短信平台认识
- 荣联云通讯短信SDK测试
- 封装发送短信单例类
- 问题:如果同时发送多个短信验证码,那么就会同时创建多个RET SDK 的对象,会额外的消耗内存空间
- 解决方案:
- 单例设计模式
- 特点:单例类只有一个实例存在
- 使用场景:在整个系统中,某个类只出现一个实例时,就可以使用单例设计模式
- 单例设计模式
-
容联云通讯短信平台介绍
-
容联云官网
- 容联云通讯网址:链接地址
- 注册并登陆
-
容联云管理控制台
开发者主账号:- ACCOUNT SID:账号的唯一标识
- AUTH TOKEN:认证口令,相当于暗号
- Rest URL(生产):对接平台时的请求地址(上线使用,非开发)
- AppID(默认):: 申请子应用时分配,
-
容联云创建应用
-
应用申请上线,并进行资质认证
-
完成资质认证,应用成功上线
-
添加测试号码
-
短信模板
-
-
容联云通讯短信SDK测试
-
集成模板短信SDK
- CCPRestSDK.py:由容联云通讯开发者编写的官方SDK文件,包括发送模板短信的方法
- ccp_sms.py:调用发送模板短信的方法
-
模板短信SDK测试
- ccp_sms.py文件中
# -*- coding:utf-8 -*- from verifications.libs.yuntongxun.CCPRestSDK import REST # 说明:主账号,登陆云通讯网站后,可在"控制台-应用"中看到开发者主账号ACCOUNT SID _accountSid = 'ACCOUNT SID' # 说明:主账号Token,登陆云通讯网站后,可在控制台-应用中看到开发者主账号AUTH TOKEN _accountToken = 'AUTH TOKEN' # 请使用管理控制台首页的APPID或自己创建应用的APPID _appId = 'APPID' # 说明:请求地址,生产环境配置成app.cloopen.com _serverIP = 'app.cloopen.com' # 说明:请求端口 ,生产环境为8883 _serverPort = "8883" # 说明:REST API版本号保持不变 _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) for k, v in result.items(): if k == 'templateSMS': for k, s in v.items(): print('%s:%s' % (k, s)) else: print('%s:%s' % (k, v)) if __name__ == '__main__': # 注意: 测试的短信模板编号为1 sendTemplateSMS('17600992168', ['123456', 5], 1)
- ccp_sms.py文件中
-
模板短信SDK返回结果说明
{ 'statusCode': '000000', // 状态码。'000000'表示成功,反之,失败 'templateSMS': { 'smsMessageSid': 'b5768b09e5bc4a369ed35c444c13a1eb', // 短信唯一标识符 'dateCreated': '20190125185207' // 短信发送时间 } }
-
封装发送短信单例类
-
封装发送短信单例类
class CCP(object): """发送短信的单例类""" def __new__(cls, *args, **kwargs): # 判断是否存在类属性_instance,_instance是类CCP的唯一对象,即单例 if not hasattr(CCP, "_instance"): cls._instance = super(CCP, cls).__new__(cls, *args, **kwargs) 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, temp_id): """ 发送模板短信单例方法 :param to: 注册手机号 :param datas: 模板短信内容数据,格式为列表,例如:['123456', 5],如不需替换请填 '' :param temp_id: 模板编号,默认免费提供id为1的模板 :return: 发短信结果 """ result = self.rest.sendTemplateSMS(to, datas, temp_id) if result.get("statusCode") == "000000": # 返回0,表示发送短信成功 return 0 else: # 返回-1,表示发送失败 return -1
-
测试单例类发送模板短信结果
if __name__ == '__main__': # 注意: 测试的短信模板编号为1 CCP().send_template_sms('17600992168', ['123456', 5], 1)
-
-
知识要点
- 容联云通讯只是发送短信的平台之一,还有其他云平台可用,比如,阿里云等,实现套路都是相通的。
- 将发短信的类封装为单例,属于性能优化的一种方案。
3.短信验证码后端逻辑
目的:借助荣联云通讯完成发送短信验证码的后端逻辑
步骤:
- 设计和定义后端接口
- 实现后端逻辑
- 短信验证码接口设计
- 请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /sms_codes/(?P1[3-9]\d{9})/ |
2. 请求参数:路径参数和查询字符串
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
mobile | string | 是 | 手机号 |
image_code | string | 是 | 图形验证码 |
uuid | string | 是 | 唯一编号 |
3. 响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 信息 |
-
短信验证码接口定义
class SMSCodeView(View): """短信验证码""" def get(self, reqeust, mobile): """ :param reqeust: 请求对象 :param mobile: 手机号 :return: JSON """ pass
-
短信验证码后端逻辑实现
class SMSCodeView(View): """短信验证码""" def get(self, reqeust, mobile): """ :param reqeust: 请求对象 :param mobile: 手机号 :return: JSON """ # 接收参数 image_code_client = reqeust.GET.get('image_code') uuid = reqeust.GET.get('uuid') # 校验参数 if not all([image_code_client, uuid]): return http.JsonResponse({'code': RETCODE.NECESSARYPARAMERR, 'errmsg': '缺少必传参数'}) # 创建连接到redis的对象 redis_conn = get_redis_connection('verify_code') # 提取图形验证码 image_code_server = redis_conn.get('img_%s' % uuid) if image_code_server is None: # 图形验证码过期或者不存在 return http.JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '图形验证码失效'}) # 删除图形验证码,避免恶意测试图形验证码 try: redis_conn.delete('img_%s' % uuid) except Exception as e: logger.error(e) # 对比图形验证码 image_code_server = image_code_server.decode() # bytes转字符串 if image_code_client.lower() != image_code_server.lower(): # 转小写后比较 return http.JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '输入图形验证码有误'}) # 生成短信验证码:生成6位数验证码 sms_code = '%06d' % random.randint(0, 999999) logger.info(sms_code) # 保存短信验证码 redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code) # 发送短信验证码 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': '发送短信成功'})
4.短信验证码前端逻辑
目的:会使用Vue.js的双向绑定展示倒计时60秒的效果
步骤:
- axios发送ajax请求获取短信验证码
- 展示倒计时60秒效果
- 短信验证码用户交互和校验
-
Vue绑定短信验证码界面
-
register.html
<li> <label>短信验证码:</label> <input type="text" v-model="sms_code" @blur="check_sms_code" name="sms_code" id="msg_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
check_sms_code(){ if(this.sms_code.length != 6){ this.error_sms_code_message = '请填写短信验证码'; this.error_sms_code = true; } else { this.error_sms_code = false; } },
-
-
axios请求短信验证码
-
发送短信验证码事件处理
send_sms_code(){ // 避免重复点击 if (this.sending_flag == true) { return; } this.sending_flag = true; // 校验参数 this.check_mobile(); this.check_image_code(); if (this.error_mobile == true || this.error_image_code == true) { this.sending_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') { // 倒计时60秒 var num = 60; var t = setInterval(() => { if (num == 1) { clearInterval(t); this.sms_code_tip = '获取短信验证码'; this.sending_flag = false; } else { num -= 1; // 展示倒计时信息 this.sms_code_tip = num + '秒'; } }, 1000, 60) } else { if (response.data.code == '4001') { this.error_image_code_message = response.data.errmsg; this.error_image_code = true; } else { // 4002 this.error_sms_code_message = response.data.errmsg; this.error_sms_code = true; } this.generate_image_code(); this.sending_flag = false; } }) .catch(error => { console.log(error.response); this.sending_flag = false; }) },
-
-
短信验证码效果展示
5.补充注册时短信验证逻辑
目的:完善注册过程中的短信验证逻辑
步骤:
- 短信验证后端逻辑
- 对比用户输入和Redis中存储的短信验证码食是否一致
- 短信验证功前端逻辑
-
补充注册时短信验证后端逻辑
-
接收短信验证码参数
sms_code_client = request.POST.get('sms_code')
-
保存注册数据之前,对比短信验证码
redis_conn = get_redis_connection('verify_code') sms_code_server = redis_conn.get('sms_%s' % mobile) if sms_code_server is None: return render(request, 'register.html', {'sms_code_errmsg':'无效的短信验证码'}) if sms_code_client != sms_code_server.decode(): return render(request, 'register.html', {'sms_code_errmsg': '输入短信验证码有误'})
-
-
补充注册时短信验证前端逻辑
-
register.html
<li> <label>短信验证码:</label> <input type="text" v-model="sms_code" @blur="check_sms_code" name="sms_code" id="msg_code" class="msg_input"> <a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]</a> <span v-show="error_sms_code" class="error_tip">[[ error_sms_code_message ]]</span> {% if sms_code_errmsg %} <span class="error_tip">{{ sms_code_errmsg }} </span> {% endif %} </li>
-
6.避免频繁发送短信验证码
存在的问题:
- 虽然我们在前端界面做了60秒倒计时功能。但是恶意用户可以绕过前端界面向后端频繁请求短信验证码。
解决办法:
- 在后端也要限制用户请求短信验证码的频率。60秒内只允许一次请求短信验证码。
- 在Redis数据库中缓存一个数值,有效期设置为60秒。
-
避免频繁发送短信验证码逻辑分析
-
避免频繁发送短信验证码逻辑实现
-
提取、校验send_flag
send_flag = redis_conn.get('send_flag_%s' % mobile) if send_flag: return http.JsonResponse({'code': RETCODE.THROTTLINGERR, 'errmsg': '发送短信过于频繁'})
-
重新写入send_flag
# 保存短信验证码 redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code) # 重新写入send_flag redis_conn.setex('send_flag_%s' % mobile, constants.SEND_SMS_CODE_INTERVAL, 1)
-
界面渲染频繁发送短信提示信息
if (response.data.code == '4001') { this.error_image_code_message = response.data.errmsg; this.error_image_code = true; } else { // 4002 this.error_sms_code_message = response.data.errmsg; this.error_sms_code = true; }
-
7.pipeline操作Redis数据库
目的:
Redis的 C - S 架构:
- 基于客户端-服务端模型以及请求/响应协议的TCP服务。
- 客户端向服务端发送一个查询请求,并监听Socket返回。
- 通常是以阻塞模式,等待服务端响应。
- 服务端处理命令,并将结果返回给客户端。
存在的问题:
- 如果Redis服务端需要同时处理多个请求,加上网络延迟,那么服务端利用率不高,效率降低。
解决的办法:
- 管道pipeline
-
pipeline的介绍
-
管道pipeline
- 可以一次性发送多条命令并在执行完后一次性将结果返回。
- pipeline通过减少客户端与Redis的通信次数来实现降低往返延时时间。
-
实现的原理
- 实现的原理是队列。
- Client可以将三个命令放到一个tcp报文一起发送。
- Server则可以将三条命令的处理结果放到一个tcp报文返回。
- 队列是先进先出,这样就保证数据的顺序性。
-
-
pipeline操作Redis数据库
-
实现步骤
- 创建Redis管道
- 将Redis请求添加到队列
- 执行请求
-
代码实现
# 创建Redis管道 pl = redis_conn.pipeline() # 将Redis请求添加到队列 pl.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code) pl.setex('send_flag_%s' % mobile, constants.SEND_SMS_CODE_INTERVAL, 1) # 执行请求 pl.execute()
-
- 以凡人之躯,比肩神明