用户注册功能实现 三

用户注册功能实现 三

目的:

1、图片验证码校验
2、短信验证码校验功能实现

应用技术:
1、前端:js(jQuery框架)、ajax(前后端交互)、
2、后端:django接口设计(json),csrf防跨域攻击,form表单校验功能

一、获取短信验证码功能

1.业务流程分析

  • 检查手机号码 (参数一)
  • 检查图片验证码是否正确 (参数二)
  • 检查是否在60s内发送记录
  • 生成短信验证码
  • 发送短信
  • 保存短信验证码(300s)与发送记录(log)

2.接口设计

2.1、接口说明:
类目 说明
请求方式 POST
url定义 /sms_code/
参数格式 表单

get:获取图片,下载,获取某一个页面,使用get请求方法。
post:生成,新建数据时,用post,这里是后端生成一个短信验证码,所以使用post。

2.2、参数说明:
参数名 类型 是否必须 描述
moblie 字符串 用户输入的手机号码
captcha 字符串 用户输入图形验证码文本
2.3、返回数据:
{
    "errno": "0", 
 	"errmsg": "发送短信验证码成功!", 
}

3.后端代码

把form验证抽取到另一个文件中

  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')

class SmsCodeView(View):
	'''
	- 检查手机号码                     (参数一)
	- 检查图片验证码是否正确    (参数二)
	- 检查是否在60s内发送记录
	- 生成短信验证码
	- 发送短信
	- 保存短信验证码(300s)与发送记录(log)

	'''
	def post(self, request):
	
        # 1.校验参数(手机号,图形验证码)
        #使用django的form自带的检验机制,将request.POST作为参数传入,会把字段值一一对应的传入表单。
        form = CheckImagForm(request.POST, request=request) 
        if form.is_valid():
        
        # 2.获取手机
        mobile = form.cleaned_data.get('mobile')
        
        # 3.生成手机验证码(4位验证码,可以控制变量修改)
        sms_code = ''.join([random.choice('0123456789') for _ in range(constants.SMS_CODE_LENGTH)])
        
        # 4.发送手机验证码
        ccp = CCP()
        try:
            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数据库
           
           这里从redis、mysql和session中选择redis的原因:
             (1)、mysql太慢
             (2)、session不能随意修改时间,当修改过期时间,会把原来的时间一起修改(比如:图形验证码的session)
             因此,我们选择用redis来进行保存短信验证码发送时间和保存时间
             
           # 创建短信验证码发送记录 (发送时间的控制,不能一直发送【60s】)
           #使用手机号区别不同用户
           sms_flag_key = 'sms_flag_{}'.format(mobile)
           # 创建短信验证码内容记录(保存短信验证码用来验证【300s】)
           sms_text_key = 'sms_text_{}'.format(mobile)
			
		   注意:alias='verify_code'中的(verify_code)值必须是在settings中设置的缓存。
           redis_conn = get_redis_connection(alias='verify_code')
           #创建管道连接数据库
           pl = redis_conn.pipeline()

           try:
           #设置过期时间   3个参数(键,过期时间,值),过期时间在全局变量中进行申明
               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()
               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 = []
            #从form表单验证错误信息(是一个类字典)中拿去值
            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)	



  1. verification/forms.py文件代码如下:(使用form表单的校验功能)
from django import forms
from django.core.validators import RegexValidator
from django_redis import get_redis_connection

from user.models import User

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


class CheckImagForm(forms.Form):
    """
    check image code
    """
  #拿到request参数,又不影响以前的参数。
    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': '图片验证码不能为空'
                              })
                              
#前面可以看做是对输入手机号和图片验证码格式的判断,后面是对逻辑和行为的判断。

	#当检验完参数后,会进入clean方法。
    def clean(self):
        clean_data = super().clean()
        
        #拿到手机号和图片验证码
        mobile = clean_data.get('mobile')
        captcha = clean_data.get('captcha')
        if mobile and captcha:
        如果手机号和图片验证码都是合法的
        
	    # 1.校验图片验证码
	    
	   	 	#image_code是我们以前在session中定义的键,用来保存图片验证码。
	        image_code = self.request.session.get('image_code')
	        
	    	#upper是把字母都变为大写,可以忽略大小写。
	        if (not image_code) or (image_code.upper() != captcha.upper()):
	            raise forms.ValidationError('图片验证码校验失败!')
	
	    	# 2.校验是否在60秒内已发送过短信,也是使用的verify_code这个redis缓存。
	        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/constants.py代码如下:(全局变量定义)
# 图片验证码过期时间 单位秒
IMAGE_CODE_EXPIRES = 300

# 短信验证码长度
SMS_CODE_LENGTH = 4

# 短信验证码发送间隔 秒
SMS_CODE_INTERVAL = 60

# 短信验证码过期时间 分
SMS_CODE_EXPIRES = 5

# 短信发送模板
SMS_CODE_TEMP_ID = 1

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

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

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

开发参数:

5.前端js代码

user/register.js代码:

$(() => {
    // 1、点击刷新图像验证码
    $('.captcha-graph-img img').click(function () {
        $(this).attr('src', '/image_code/?rand=' + Math.random())
    });
    //校验功能
    //定义一些状态变量
    let isUsernameReady = false,
        isPasswoedReady = false,
        isMobileReady = false,
        isSmsCodeReady = false;

    // 2.鼠标离开用户名输入框校验用户名
    let $username = $('#user_name');
    $username.blur(fnCheckUsername);

    function fnCheckUsername () {
        isUsernameReady = 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 + '/',  //例如 127.0.0.1:8000/username/zhangsan/ 发送请求
            type: 'GET',
            dataType: 'json',
            success: function (res) {
                if(res.data.count !== 0){
                    message.showError(res.data.username + '已经注册,请重新输入!')
                }else {
                    message.showInfo(res.data.username + '可以正常使用!')
                    isUsernameReady = true
                }
            },
            error: function (xhr, msg) {
                message.showError('服务器超时,请重试!')
            }
        });
    }

    // 3.检测密码是否一致
    let $passwordRepeat = $('input[name="password_repeat"]');
    $passwordRepeat.blur(fnCheckPassword);

    function fnCheckPassword () {
        isPasswordReady = 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"]');
    $mobile.blur(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('服务器超时,请重试!')
            }
        });

    });
});

因为用到了post方法,django默认带有csrf防护,所以在base/common.js中添加如下代码:
要在form表单中加入{% csrf_token %} 来生成csrf。

$(()=>{
    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'));
            }
        }
    });
});

注意:
1、csrf
csrf是一种对post,delete,put请求的一种安全措施。
csrf生成 → csrf中间件在response的cookies中生成csef的sessionid →
当用户发送表单数据时,会携带sessionid → 匹配csrf值是否相同。

2、form
用户注册功能实现 三_第1张图片
在这里插入图片描述

你可能感兴趣的:(python,django)