十九. 用户注册 --- 短信验证码实现 2021-04-16

十九. 用户注册 — 短信验证码实现

注:该篇文章接上一篇 十八.用户注册 ---- 用户注册 ---- 用户名/用户密码/手机号验证
在上一篇文章我们实现了用户名验证,密码验证,以及手机号验证,这一章我们将实现短信验证码实现
实现注册要完成的图表
在这里插入图片描述
实现注册模块的整体流程
在这里插入图片描述
根据流程图总结注册业务包含如下功能

  • 注册页面
  • 图片验证码
  • 用户名检测是否注册
  • 手机号检测是否注册
  • 短信验证码
  • 注册保存用户数据

八、获取短信验证码功能

1.业务流程分析

  • 生成短信验证码
  • 发送短信
  • 保存短信验证码与发送记录(如果发生没收到验证码问题,可以进行查询,判断哪里出了问题)

2.接口设计

接口说明:

类目说明
请求方法POST
url定义/sms_code/
参数格式表单

参数说明:

参数名类型是否必须描述
moblie字符串用户输入的手机号码
captcha字符串用户输入的验证码文本

返回结果:

{
    "errno": "0", 
    "errmsg": "发送短信验证码成功!", 
}

3.短信验证码平台-云通讯

本项目中使用的短信验证码平台为云通讯平台,文档参考地址

主要是因为可以免费测试,注册后赠送8元用于测试。

开发参数:
在这里插入图片描述

_accountSid = '开发者主账号中的ACCOUNT SID'# 说明:主账号Token,登陆云通讯网站后,可在控制台-应用中看到开发者主账号AUTH TOKEN
_accountToken = '开发者主账号中的AUTH TOKEN'# 请使用管理控制台首页的APPID或自己创建应用的APPID
_appId = '开发者主账号中的AppID(默认)'# 说明:请求地址,生产环境配置成app.cloopen.com
_serverIP = 'sandboxapp.cloopen.com'# 说明:请求端口 ,生产环境为8883
_serverPort = "8883"# 说明:REST API版本号保持不变
_softVersion = '2013-12-26'

设置测试手机号码
在这里插入图片描述
3. 创建utils文件将yuntongxun文件夹移到该文件夹下
yuntongxun文件资源:链接:https://pan.baidu.com/s/16y73V7mYfoNOHP52uNlzew
提取码:xxl4
在这里插入图片描述
在这里插入图片描述
4.在云通讯下的sms.py文件中设置自己的账户信息
在这里插入图片描述

4.后端代码

1.在setting.py中添加一个redis缓存库

'verify_code': {    'BACKEND': 'django_redis.cache.RedisCache',    'LOCATION': 'redis://127.0.0.1:6379/2',    'OPTIONS': {        'CLIENT_CLASS': 'django_redis.client.DefaultClient',    }},

添加缓存库的原因:
verify_code 为 rides 库取一个别名,用于存储短信验证码
6379/2 —使用2号库,rides一共16个库
在这里插入图片描述
2. 在verification中创建from表单 forms.py

目的:
对手机号码的格式进行验证
使view函数中尽量少些代码,符合高类聚,低耦合的后端开发模式(把一些东西尽量抽出去)

verification/forms.py文件代码如下:


from django import forms
from user.models import User   # 用于后端校验用户信息
from django.core.validators import RegexValidator   # 用于校验手机号
from django_redis import get_redis_connection


#创建手机号正则校验器
mobile_validator = RegexValidator(r'^1[3-9]\d[9]$','手机号格式不正确')

class CheckImagForm(forms.Form):
    """
    check image code
    """

    def __init__(self, *args, **kwargs):
        # 参数用于继承父类
        self.request = kwargs.pop('request')
        super().__init__(*args, **kwargs)

    #验证手机号信息(最大长度,最小长度,正则验证,报错信息)
    mobile = forms.CharField(max_length=11, min_length=11, validators=[mobile_validator, ],
                             error_messages={
                                 'max_length': '手机长度有误',
                                 'min_length': '手机长度有误',
                                 'required': '手机号不能为空'
                             })

    # 图形验证码验证(最大长度,最小长度,错误信息)
    captcha = forms.CharField(max_length=4, min_length=4,
                              error_messages={
                                  'max_length': '验证码长度有误',
                                  'min_length': '图片验证码长度有误',
                                  'required': '图片验证码不能为空'
                              })

   #form表单继承方法clean,通过该方法获取我们想要的东西
    def clean(self):
        clean_data = super().clean()
        mobile = clean_data.get('mobile')    # 获取手机号
        captcha = clean_data.get('captcha')  #获取图形验证码
    # 1.校验图片验证码
        # 图形验证码保存在rides库中数据库1的session中
        image_code = self.request.session.get('image_code')
        # 如果验证码为空或者图形输入的验证码不等于session数据库中的图形验证码   upper() 转换为大写
        if (not image_code) or (image_code.upper() != captcha.upper()):
            raise forms.ValidationError('图片验证码校验失败!')

    # 2.校验是否在60秒内已发送过短信
        #获取rides中缓存的验证码,get_redis_connection() -- 指定与哪个数据库建立连接
        redis_conn = get_redis_connection(alias='verify_code')
        if redis_conn.get('sms_flag_{}'.format(mobile)):
            raise forms.ValidationError('获取短信验证码过于频繁')

    # 3.校验手机号码是否已注册
        if User.objects.filter(mobile=mobile).count():
            raise forms.ValidationError('手机号已注册,请重新输入')
  1. verification/views.py代码如下:

import logging
import random
​
from django.http import HttpResponse
from django.views import View
from django_redis import get_redis_connection
​
from user.models import User
from utils.json_res import json_response
from utils.res_code import Code, error_map
from utils.captcha.captcha import captcha
from utils.yuntongxun.sms import CCP
​
from . import constants
from .forms import CheckImagForm
​
# 日志器
logger = logging.getLogger('django')
​
​
def image_code_view(request):
    """
    生成图片验证码
    url:/image_code/
    :param request:
    :return:
    """
    text, image = captcha.generate_captcha()
    request.session['image_code'] = text
    # 将验证码存入session中
    request.session.set_expiry(constants.IMAGE_CODE_EXPIRES)
    logger.info('Image code:{}'.format(text))return HttpResponse(content=image, content_type='image/jpg')
​
​
def check_username_view(request, username):
    """
    校验用户名是否存在
    url:/username/(?P<username>\w{5,20})/
    :param request:
    :param username:
    :return:
    """
    data = {
        'username': username,
        'count': User.objects.filter(username=username).count()
    }return json_response(data=data)
​
​
def check_mobile_view(request, mobile):
    """
    校验手机号是否存在
    url:/moblie/(?P<moblie>1[3-9]\d{9})/
    :param request:
    :param username:
    :return:
    """
    data = {
        'mobile': mobile,
        'count': User.objects.filter(mobile=mobile).count()
    }return json_response(data=data)
​
​

class SmsCodeView(View):
    """
    发送短信验证码
    POST /sms_codes/
    """
    def post(self, request):
        # 1.校验参数

        form = CheckImagForm(request.POST, request=request)
        if form.is_valid():
            # 2.获取手机
            mobile = form.cleaned_data.get('mobile')
            # 3.生成手机验证码 随机生成0-9 循环几次
            sms_code = ''.join([random.choice('0123456789') for i in range(constants.SMS_CODE_LENGTH)])
            # 4.发送手机验证码
            ccp = CCP()
            try:
                # ccp.send_template_sms(手机号,[验证码,过期时间],内容模板)
                res = ccp.send_template_sms(mobile, [sms_code, constants.SMS_CODE_EXPIRES], "1")
                if res == 0:
                    logger.info('发送短信验证码[正常][mobile: %s sms_code: %s]' % (mobile, sms_code))
                else:
                    logger.error('发送短信验证码[失败][moblie: %s sms_code: %s]' % (mobile, sms_code))
                    return json_response(errno=Code.SMSFAIL, errmsg=error_map[Code.SMSFAIL])
            except Exception as e:
                logger.error('发送短信验证码[异常][mobile: %s message: %s]' % (mobile, e))
                return json_response(errno=Code.SMSERROR, errmsg=error_map[Code.SMSERROR])


            # 5.保存到redis数据库
            # 创建短信验证码发送记录
            sms_flag_key = 'sms_flag_{}'.format(mobile)
            # 创建短信验证码内容记录
            sms_text_key = 'sms_text_{}'.format(mobile)
            # 获取rides中缓存的验证码,get_redis_connection() -- 指定与哪个数据库建立连接
            redis_conn = get_redis_connection(alias='verify_code')
            #绑定传输管道
            pl = redis_conn.pipeline()  #将要保存的东西一并放入管道进行保存

            try:
                #设置过期时间
                pl.setex(sms_flag_key, constants.SMS_CODE_INTERVAL, 1)  
                pl.setex(sms_text_key, constants.SMS_CODE_EXPIRES*60, sms_code)
                # 让管道通知redis执行命令
                pl.execute()
                #先发送后存rides库,确保准确无误
                return json_response(errmsg="短信验证码发送成功!")
            except Exception as e:
                logger.error('redis 执行异常:{}'.format(e))

                return json_response(errno=Code.UNKOWNERR, errmsg=error_map[Code.UNKOWNERR])

        else:
            # 将表单的报错信息进行拼接
            err_msg_list = []
            for item in form.errors.get_json_data().values():
                err_msg_list.append(item[0].get('message'))
                # print(item[0].get('message'))   # for test
            err_msg_str = '/'.join(err_msg_list)  # 拼接错误信息为一个字符串

            return json_response(errno=Code.PARAMERR, errmsg=err_msg_str)

4.设置URL

进入verification/url下设置:

path('sms_code/',views.SmsCodeView.as_view(),name = 'sms_code')

在这里插入图片描述

 

$(function () {

    // 定义状态变量
    //判断状态是false还是ture,如果是ture就直接引用
    let isUsernameReady = false,
        isPasswordReady = false,
        isMobileReady = false,
        isSmsCodeReady = false;

    let $img = $('.form-contain .form-item .captcha-graph-img img');
    // 1.点击刷新图像验证码
    $img.click(function () {
        $img.attr('src', '/image_code/?rand=' + Math.random())
    });

    // 2.鼠标离开用户名输入框校验用户名
    let $username = $('#username'); //获取前端页面中的username框
    $username.blur(fnCheckUsername); //blur是鼠标离开事件

    //鼠标离开username触发函数fnCheckUsername
    function fnCheckUsername() {
        isUsernameReady = false;            //校验用户名,没填入用户名为false
        let sUsername = $username.val();    // 获取用户名字符串
        if (sUsername === '') {
            // 如果用户名为空,返回消息
            message.showError('用户名不能为空')
            return
        }
        if (!(/^\w{5,20}$/).test(sUsername)) {
            //检测用户名长度
            message.showError('请输入5-20个字符的用户名');
            return
        }
        $.ajax({
            url: '/username/' + sUsername + '/',      //发送请求的地址
            type: 'GET',                              //请求方式
            dataType: 'json',                         //预期服务器返回的数据类型
            success: function (data) {
                // data 由服务器返回的数据,我们这里是json类型
                if (data.data.data.count !== 0) {
                    //根据json的数据结构的到用户数量
                    message.showError(data.data.data.username + '已经注册,请重新输入!')
                } else {
                    message.showInfo(data.data.username + '可以正常使用!');
                    isUsernameReady = true
                }
            },
            error: function (xhr, msg) {
                //请求失败时被调用的函数
                message.showError('服务器超时,请重试!')
            }
        });
    }

    // 3.检测密码是否一致
     let $passwordRepeat = $('input[name="password_repeat"]'); //获取前端input[name="password_repeat"] 中的内容(二次密码)
     $passwordRepeat.blur(fnCheckPassword);     //blur是鼠标离开事件

    //鼠标离开username触发函数fnCheckPassword
     function fnCheckPassword() {
        isPasswordReady = false;   //校验密码,没填入密码为false
        let password = $('input[name="password"]').val();   // 获得密码字符串
        let passwordRepeat = $passwordRepeat.val();      // 获取用户二次密码字符串
        if (password === '' || passwordRepeat === '') {
            message.showError('密码不能为空');
            return
        }
        if (password !== passwordRepeat) {
            message.showError('两次密码输入不一致');
            return
        }
        if (password === passwordRepeat) {
            isPasswordReady = true
        }
    }

    // 4.检查手机号码是否可用
    let $mobile = $('input[name="mobile"]'); //获取前端input[name="mobile"] 中的内容(手机密码)
    $mobile.blur(fnCheckMobile);
    //鼠标离开username触发函数fnCheckMobile
    function fnCheckMobile () {
        isMobileReady = true;
        let sMobile = $mobile.val(); // 获得手机字符串
        if(sMobile === ''){
            message.showError('手机号码不能为空');
            return
        }
        if(!(/^1[3-9]\d{9}$/).test(sMobile)){
            message.showError('手机号码格式不正确');
            return
        }

        $.ajax({
            url: '/mobile/' + sMobile + '/',
            type: 'GET',
            dataType: 'json',
            success: function (data) {
                if(data.data.count !== 0){
                    message.showError(data.data.mobile + '已经注册,请重新输入!')
                }else {
                    message.showInfo(data.data.mobile + '可以正常使用!');
                    isMobileReady = true
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });

    }

  // 5.发送手机验证码
    let $smsButton = $('.sms-captcha');
    $smsButton.click(function (){
        let sCaptcha = $('input[name="captcha_graph"]').val();
        if (sCaptcha === '') {
            message.showError('请输入验证码');
            return
        }
        if (!isMobileReady) {
            fnCheckMobile();
            return
        }

        $.ajax({
            url: '/sms_code/',
            type: 'POST',
            data: {
                mobile: $mobile.val(),
                captcha: sCaptcha
            },
            dataType: 'json',
            success: function (data) {
                if (data.errno !== '0') {
                    message.showError(data.errmsg)
                } else {
                    message.showSuccess(data.errmsg);
                    let num = 60;
                    //设置计时器
                    let t = setInterval(function () {
                        if (num === 1) {
                            clearInterval(t)
                        }
                    })
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });
    });

});

  1. 因为用到了post方法,django默认带有csrf防护,所以在base/common.js中添加如下代码(方法一):
$(()=>{
    let $navLi = $('#header .nav .menu li');
    $navLi.click(function(){
        $(this).addClass('active').siblings('li').removeClass('active')
    });function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie !== '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = cookies[i].trim();
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
            }
        }
    });
});

详解django文档

方法二:setting中注释csrf函数:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
  #  'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值