• 【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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    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 ='''%s\
                %s\
                \
                '''%(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':'网络错误'}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    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[\w-]+)/$', views.ImageCodeView.as_view()),
    
        # 短信验证码,re_path路由正则校验,响应json数据,不需要重定向,也就不需要命名空间
        re_path(r'^sms_codes/(?P1[3-9]\d{9})/$', views.SMSCodeView.as_view()),
    
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    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': '短信验证码发送成功!'})
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

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

  • 相关阅读:
    更新npm和node
    一篇文章告诉你,为什么必须要学Excel?
    机器学习——评估和改进学习算法
    springcloud、springboot、springcloudalibaba版本依赖关系
    HTTP 消息头
    nginx反向代理了解
    Java学习 --- 设计模式七大原则的依赖倒转原则
    【小程序】导航栏和内容页面联动效果实现
    基于电动汽车的带时间窗的路径优化(Python&Matlab代码实现)
    查看linux系统情况常用命令
  • 原文地址:https://blog.csdn.net/yuetaope/article/details/123004459