Django项目实践(商城):五、验证(图形验证码、短信验证码)

在这里插入图片描述

(根据居然老师直播课内容整理)
  • 验证在项目中可能多处使用,还有多种验证方法(本项目只涉及图形验证和短信验证),所以需要将验证独立成一个APP应用

一、准备工作

1、创建子应用 verifications

  • 在命令行,进入apps包目录下
  • 执行命令:python …\manage.py startapp verifications

2 、注册此子应用

  • ./lgshop/dev.py
    在这里插入图片描述

3、定义主路由

在这里插入图片描述

  • 如果在主路由中进行区分代码,直接象users子应用一样即可,如果不区分,就需要到子路由中去区分,如 验证模块、首页等

3、定义子路由

  • ./apps/verifications/urls.py
    在这里插入图片描述
  • 因此模块无需单独使用,故不需要命名

一、生成图形验证码

1、生成图形验证码逻辑分析

在这里插入图片描述

  • 前端向后端申请图形验证码,通过uuid来区分身份
  • 后端生成图形验证码,并制作成验证码图片数据
  • 将图形验证码强果保存到redis中,以便后面验证
  • 将验证码图片数据传回前端进行展示

2、图形验证码接口设计

2.1 请求方式

选项 方案
请求方法 GET
请求地址 image_codes/(?P[\w-]+)/

2.2 请求参数:路径参数

参数名 类型 是否必传 说明
uuid string 唯一编号

2.3 响应结果

在这里插入图片描述

3、生成图形验证码接口实现

  • 生成图形验证码接口只提供GET即可

  • 通用调用captcha.generate_captcha()即可生成图形验证码

    • captcha是一个图形验证码生成包,复制到特定目录,导入即可使用
      在这里插入图片描述
  • 将验证码文件部分保存到redis中,以使后面校验用

    • 将验证码存放到redis 3数据库中
# ./lgshop/dev.py 
    "verify_code": {
    
      # 验证码
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/2",
        "OPTIONS": {
    
    
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
  • 用http方式,将图片数据传回前端
    在这里插入图片描述
# ./verifications/views.py
from django.shortcuts import render
from django.views import View
from .libs.captcha.captcha import captcha
from django_redis import get_redis_connection
from django import http


class ImageCodeView(View):
    """图形验证码"""

    def get(self,request,uuid):
        '''
        :param request:
        :param uuid:  通用唯一识别符,用于标识唯一图片验证码属于哪个用户的
        :return:  image/jpg
        '''

        # 生成图片验证码
        text, image = captcha.generate_captcha()
        # print(text, image)

        # 保存图像验证码,保存到redis
        redis_conn = get_redis_connection('verify_code')

        # name time value
        redis_conn.setex('img_%s' % uuid, 300, text)

        # 响应图形验证码
        return http.HttpResponse(image, content_type='image/png')

4、生成图形验证码子路由定义

在这里插入图片描述

# ./verifications/urls.py
from django.urls import path, re_path
from . import views

urlpatterns = [
    # 图形验证码 \w  [A-Za-z0-9_]-  uuid 78b4d5b7-5157-4b2a-bf48-ba616e169d66
    re_path(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImageCodeView.as_view())
]
  • 注:[\w-]+:\w表示匹配字母数字及下划线,[]表示匹配里面包含的字符,+表示匹配多次

5、前端代码实现

5.1 JS方法实现

  • 当注册页面一刷,图形验证码就应该更新并显示出来
    • vue.js 中有一个方法:mounted(),页面加载完成之后会被调用的方法
  • 在mounted()方法中,向后端发送一个请求,申请一个图形验证码
  • 发送请求前,还需要生成一个uuid (common.js中generateUUID()可以生成uuid)
  • 因 mounted()和 图形验证码点击事件都要调用 生成图形验证码调用
    在这里插入图片描述
# ./static/js/register.js
    data: {
    
         // 数据对象
        // ... ...
        image_code_url: '',
        uuid: '',

	// 页面加载完成之后会被调用的方法
	mounted(){
    
    
        // 生成图形验证码
        this.generate_image_code()
    },
    methods: {
    
    
        // 生成图片验证码
        generate_image_code(){
    
    
            // uuid 发生变化
            this.uuid = generateUUID();
            this.image_code_url = '/image_codes/'+ this.uuid +'/'
        },

5.2 register.html相应修改

  • 显示图形验证码的地方图形验证码,需要绑定属性 v-bind
  • 同时,还要设置点击事件@click,当点击此图片时,会自动刷新图形验码在这里插入图片描述

二、封装容联云短信发送类

1、在容联云注册用户(免费注册,完成个人认证)

2、创建应用,并复制APP ID 和 APP TOKEN

3、定制短信模板(也可以使用系统提供的测试模板1 号模板发送验证码)

4、添加测试号码:

5、参考开发文档,开发演示代码

6、开发短信发送模块

  • 在libs包下建立ronglianyun包,并建 ccp_SMS.py
from ronglian_sms_sdk import SmsSDK
 
accId = '容联云通讯分配的主账号ID'
accToken = '容联云通讯分配的主账号TOKEN'
appId = '容联云通讯分配的应用ID'
 
def send_message():
    sdk = SmsSDK(accId, accToken, appId)
    tid = '1'    # 应用是系统提供的开发测试模板及编号
    mobile = '手机号1,手机号2'
    datas = ('变量1', '变量2') 
    resp = sdk.sendMessage(tid, mobile, datas)
	result=json.load(resp)
    if result["statusCode"]=="000000":
        return 0
    else:
        return -1
        
if __name__=="__main__":
    send_message()
   

7、优化封装短信发送模块

  • 每发送一次短信,就会调用发送短信发送模块时,都会实例化SmsSDK类,对资源消耗较大,应采用单例模式

7.1 定义发送短信的单例类

  • 定义CCP类,重写__new__()方法
    • 首先判断是否存在一个属性(_instance,自定义的)
    • 不存在,调用父类super()的__new__()方法,给属性赋值,并将相关参数传递进去,然后再设置该属性添加一个sdk,赋值为发送短信类实例
    • 最后返回该属性
class CCP(object):
    """发送短信的单例类"""
    def __new__(cls,*args,**kwargs):
        if not hasattr(cls,"_instance"):
            cls._instance=super().__new__(cls,*args,**kwargs)
            cls._instance.sdk=SmsSDK(accId, accToken, appId)
        return cls._instance

7.2 优化发送短信模块

  • 用单例化短信发送类优化
  • 发送验证码参数化
from ronglian_sms_sdk import SmsSDK
import json

accId = '容联云通讯分配的主账号ID'
accToken = '容联云通讯分配的主账号TOKEN'
appId = '容联云通讯分配的应用ID'

class CCP(object):
    """发送短信的单例类"""

    def __new__(cls, *args, **kwargs):
        # 如果是第一次实例化,应该返回实例化后的对象,如果是第二次实例化,应该返回上一次实例化后的对象
        # 判断是否存在类属性 _instance
        if not hasattr(cls, "_instance"):
            cls._instance = super().__new__(cls, *args, **kwargs)
            cls._instance.sdk = SmsSDK(accId, accToken, appId)
        return cls._instance

    def send_message(self, tid, mobile, datas):
        sdk = self._instance.sdk
        # tid = '容联云通讯创建的模板ID'
        # mobile = '手机号1,手机号2'
        # datas = ('变量1', '变量2')
        # tid = '1'
        # mobile = '18908656327'
        # datas = ('2345', '5')
        resp = sdk.sendMessage(tid, mobile, datas)
        result = json.loads(resp)
        if result["statusCode"] == "000000":
            return 0
        else:
            return -1


if __name__ == "__main__":
    c = CCP()
    c.send_message("1", "18908656327", ("1234", "5"))    # 测试1号模板,手机号,(短信验证码,有效时间)

三 、发送短信验证码

1、发送短信验证码逻辑分析

在这里插入图片描述

  • 点击“发送短信验证码”时,判断图形验码是否输入,
  • 为提高用户体验,前端采用ajax发送请求
  • 后端接收请求并校验参数
    • 接收并判断手机号、uuid 和图形验证码等必须参数是否为空
    • 校验手机号是否正确
  • 从redis中提取图形验证码,并删除此图形验证码
  • 比较前端传递的图形难证码和后端是否相同
    • 如果不相同,返回错误
  • 生成短信验证码,并保存到redis中
  • 通过容联云平台 发送短信验证码
  • 向前端发送响应结果

2、短信验证码接口设计

2.1 请求方式

|选项|方案|

|请求方法|GET|
|请求地址|/sms_codes/(?P1[3-9]\d{9})/|

2.2 请求参数:路径参数和查询字符串

参数名 类型 是否必传 说明
mobile string 手机号
image_code string 图形验证码
uuid string 唯一编号

2.3 响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息

3、发送短信验证码接口实现

3.1 初步实现

  • 接收参数:uuid和图形验证码,手机号已在路由中解析出来了,并对正确性进行了校验
  • 判断两个参数是否存在:这两上是必传参数
    • 如果不存在,返回错误代码及错误信息
  • 连接redis数据库,读取后端存储的图形验证码
    • 如果为None,表示图形验证码过期失效,返回错误
    • redis中取出来的二进制,需要.decode()
    • 为了提高用户体验,不区分大小写,全部转换成大写或小写再比较
  • 删除后端存储的图形验证码(图形验证码只能使用一次,否认对错)
  • 判断前后端图形验证码是否一致
    • 如果不一致,返回错误
  • 生成6位短信验码 : “%06d” % random.ranint(0,999999999)
    • “%d” :整数,“%6d”:不超过6位的整数,可能小于6位,“%06d”:6位整数,不够6位补0
  • 保存短信验证码到redis中
  • 调用容联云发送短信类,发送短信验证码
  • 需要导入短信发送验证码类, 路径从apps开始,
  • 短信验证码类需要实例化,即 CCP()
  • 返回发送状态

3.2 代码优化 : 避免频繁发送短信验证码

  • 防止恶意注册,需要限制短信发送频次: 1分钟内仅能发送一次,否则不发送
  • 解决办法:
    • 在后端也要限制用户请求短信验证码的频率。60秒内只允许一次请求短信验证码。
    • 在Redis数据库中缓存一个数值,有效期设置为60秒。
  • 实现方法:
    • 校验完用户参数后,读取redis中发送状态(send_flag_手机号),如果存在,返回错误信息(过于频繁)
    • 当保存短信验证码后,就保存发送状态(send_flag_手机号)并设置有效期1分钟

3.3 完整代码

class SMSCodeView(View):
    '''短信验证码发送'''

    def get(self,request,mobile):
        '''
        :param request:
        :param mobile:
        :return:
        '''
        # http://127.0.0.1:8000/sms_codes/手机号/?uuid=cea94f82-4329-41e4-80df-cca815875a43&image_code=XQDI
        # 接收参数,校验参数

        # print(mobile)
        uuid = request.GET.get('uuid')  # uuid
        image_code_client = request.GET.get('image_code')   # 图形验证码,查看前端JS中传递参数名称(不是表单提交)
        if not all([uuid, image_code_client]):
            return http.HttpResponseForbidden('缺少必传参数')  # HttpResponseForbidden:返回403 status code.

        # 提取图形验证码
        redis_conn = get_redis_connection('verify_code')

        # 判断用户是否频繁发生短信验证码
        send_flag = redis_conn.get('send_flag_%s' % mobile)
        if send_flag:
            return http.JsonResponse({
    
    'code': RETCODE.THROTTLINGERR, 'errmsg': '发送短信过于频繁'})

        image_code_server = redis_conn.get('img_%s' % uuid)
        # 提取图形验证码失效了
        if image_code_server is None:
            return http.JsonResponse({
    
    'code': RETCODE.IMAGECODEERR, 'errmsg': '图形验证码已失效'})

        # 删除图形验证码
        redis_conn.delete('img_%s' % uuid)

        # 对比图形验证码
        # print(image_code_client)    # kl5X
        # print(image_code_server)
        image_code_server = image_code_server.decode()
        if image_code_client.lower() != image_code_server.lower():
            return http.JsonResponse({
    
    'code': RETCODE.IMAGECODEERR, 'errmsg': '输入图形验证码有误'})

        # 生成短信验证码
        # 生成6位的随机数  %6d
        sms_code = "%06d" % random.randint(0, 999999)

        # 保存短信验证码
        redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
        # 保存发送短信验证码的标记
        redis_conn.setex('send_flag_%s' % mobile, constants.SEND_SMS_CODE_TIMES, 1)

        # 发送短信 send_message(self, mobile, datas, tid): 300/60 浮点数
        CCP().send_message( constants.SEND_SMS_TEMPLATE_ID, mobile, (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60))

        # 响应结果
        return http.JsonResponse({
    
    'code': RETCODE.OK, 'errmsg': '发送短信验证码成功'})

4、发送短信验证码子路由定义

在这里插入图片描述

re_path(r'^sms_codes/(?P<mobile>1[3-9]\d{9})/$', views.ImageCodeView.as_view()),

5、前端代码实现

5.1 vue绑定界面

  • register.html页面中进行修改
# ./templates/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中定义相关变量和方法

5.2 发送短信验证码的JS方法实现

  • 发送短信验证码,前提条件是必须有正确的手机号并输入图形验证码
    • 故先校验手机号和图形验证码,如果有true ,返回
  • 检查发送标志是否为已发送,如果为已发送,返回
  • 发送标志置为已发送(上锁)
  • 生成请求url,并通过ajax方式发送向后发送
	axios.get(url,{
    
    responseType: 'json'})
  • 如果请求发送正常: .then(response=>{}) ,发送异常 : .catch(error=>{})
    • 如果返回正常,显示60秒倒计时: 定时器 setInterval(‘回调函数’, ‘时间间隔(毫秒)’)
    • 回调函数:()=>{}
      • 定义一个计数变量,初始值60(每1秒减1 )
      • 当 计数变量=1 时,停止回调函数:clearInterval(t); 将信息显示改回“获取短信验证码” ; 刷新图形验证码;发送标志置为未发送(解锁)
      • 当 计数变量>1 时, 计数变量减1 ;将信息显示改回“计数变量秒” ;
    • 如果返回不是0,分类处理错误类型;发送标志置为未发送(解锁)
      • 如果返回4001,显示错误信息,否则都是短信验证码错误,显示错误信息
  • 如果发送异常 : .catch(error=>{})
    • 打印错误信息;发送标志置为未发送(解锁)
        // 发送短信验证码  ajax
        send_sms_code(){
    
    
             //校验手机号和图形验证码
             this.check_mobile();
             this.check_image_code();

             // alert(this.error_mobile);
             // alert(this.error_image_code);
             if (this.error_mobile ==true || this.error_image_code==true){
    
         // 如果有误,不发送短信验证码申请
                return;
             }

            let url = '/sms_codes/'+ this.mobile +'/?uuid='+ this.uuid +'&image_code=' + this.image_code;
            alert(url)
            axios.get(url,{
    
    
                responseType: 'json'
            })
                .then(response => {
    
         //请求正常
                     if (response.data.code == '0'){
    
        //返回成功
                        // 展示倒计时60秒
                        let num = 60;
                        // setInterval('回调函数', '时间间隔(毫秒)')
                        let t = setInterval(()=>{
    
    
                            if (num == 1){
    
    
                                // 停止回调函数
                                clearInterval(t);
                                this.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_sms_code_message = response.data.errmsg;
                            this.error_sms_code = true;
                        }else {
    
    
                             // 短信证码错误
                            this.error_sms_code_message = response.data.errmsg;
                            this.error_sms_code = true;
                        }
                        this.send_flag = false;
                     }
                })
                .catch(error => {
    
           //请求异常
                    console.log(error.response);
                    this.send_flag = false;
                })
        },

四、完善一下注册

1、后端注册逻辑,增加短信验证码判断

            # 短信验证码
            sms_code_client = register_form.cleaned_data.get('sms_code')

            # 判断短信验证码输入是否正确
            redis_conn = get_redis_connection('verify_code')
            sms_code_server = redis_conn.get('sms_%s' % mobile)

            if sms_code_server.decode() is None:
                return render(request, 'register.html', {
    
    'sms_code_errmsg': '短信验证码已失效'})
            # print(sms_code_server.decode())
            # print(sms_code_client)

            if sms_code_server.decode() != sms_code_client:
                return render(request, 'register.html', {
    
    'sms_code_errmsg': '输入短信验证码有误'})

2 、前端增加错误信息显示

2.1 图形验证码错误或失效(发送短信验证码时返回错误信息)

在这里插入图片描述

                                {
    
    % if sms_code_errmsg %}
                                <span class="error_tip">
                                    {
    
    {
    
     sms_code_errmsg }}
                                </span>
                                {
    
    % endif %}

五、数据库管道处理

  • 短信验证码处理时,需要向redis保存短信验证码,紧接着要向redis中保存发送标志
  • 同一时间执行两次redis操作,可以用管道进行优化
  • redis管道 详见 《redis管道》
    在这里插入图片描述
		# 创建管道
        pl = redis_conn.pipeline()
        # 将命令添加到队列
        # 保存短信验证码
        pl.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
        # 保存发送短信验证码的标记
        pl.setex('send_flag_%s' % mobile, constants.SEND_SMS_CODE_TIMES, 1)
        # 执行
        pl.execute()

猜你喜欢

转载自blog.csdn.net/laoluobo76/article/details/113358689