十九. 用户注册 — 短信验证码实现
注:该篇文章接上一篇 十八.用户注册 ---- 用户注册 ---- 用户名/用户密码/手机号验证
在上一篇文章我们实现了用户名验证,密码验证,以及手机号验证,这一章我们将实现短信验证码实现
实现注册要完成的图表
实现注册模块的整体流程
根据流程图总结注册业务包含如下功能
- 注册页面
- 图片验证码
- 用户名检测是否注册
- 手机号检测是否注册
- 短信验证码
- 注册保存用户数据
八、获取短信验证码功能
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('手机号已注册,请重新输入')
- 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('服务器超时,请重试!')
}
});
});
});
- 因为用到了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',
]