目的:
1、图片验证码校验
2、短信验证码校验功能实现
应用技术:
1、前端:js(jQuery框架)、ajax(前后端交互)、
2、后端:django接口设计(json),csrf防跨域攻击,form表单校验功能
类目 | 说明 |
---|---|
请求方式 | POST |
url定义 | /sms_code/ |
参数格式 | 表单 |
get:获取图片,下载,获取某一个页面,使用get请求方法。
post:生成,新建数据时,用post,这里是后端生成一个短信验证码,所以使用post。
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
moblie | 字符串 | 是 | 用户输入的手机号码 |
captcha | 字符串 | 是 | 用户输入图形验证码文本 |
{
"errno": "0",
"errmsg": "发送短信验证码成功!",
}
把form验证抽取到另一个文件中
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)
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('手机号已注册,请重新输入')
# 图片验证码过期时间 单位秒
IMAGE_CODE_EXPIRES = 300
# 短信验证码长度
SMS_CODE_LENGTH = 4
# 短信验证码发送间隔 秒
SMS_CODE_INTERVAL = 60
# 短信验证码过期时间 分
SMS_CODE_EXPIRES = 5
# 短信发送模板
SMS_CODE_TEMP_ID = 1
本项目中使用的短信验证码平台为云通讯平台,文档参考地址
主要是因为可以免费测试,注册后赠送8元用于测试。
开发参数:
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值是否相同。