【Python百日进阶-WEB开发】Day179 - Django案例:11短信验证码

九、短信验证码

9.1 短信验证码逻辑分析

在这里插入图片描述

9.2 容联云通讯短信平台

了解容联云通讯平台和短信SDK的使用方式,

9.2.1 容联云通讯短信平台介绍

  1. 容联云官网:https://www.yuntongxun.com/
    在这里插入图片描述
    2.注册登录
    在这里插入图片描述
  2. 通过认证,企业认证或个人认证,提交申请后一般需要第二天通过。发送短信0.06元/条,注册赠送8元,我发过一条了。
    在这里插入图片描述
  3. 添加容联云子应用,通过认证后可以上线应用
    在这里插入图片描述
  4. 添加测试号码
    在这里插入图片描述
  5. 短信模板
    在这里插入图片描述
  6. Python Demo中模板短信的使用说明
    https://doc.yuntongxun.com/p/5a533e0c3b8496dd00dce08c
    在这里插入图片描述
  7. 开发文档-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终端输出
在这里插入图片描述

  1. 测试手机收到的短信
    在这里插入图片描述

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 短信验证码后端逻辑

  1. 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()),

]
  1. 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 短信验证码前端逻辑

  1. 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>
  1. 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;
        })
},

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岳涛@心馨电脑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值