十九. 用户注册 — 短信验证码实现
注:该篇文章接上一篇 十八.用户注册 ---- 用户注册 ---- 用户名/用户密码/手机号验证
在上一篇文章我们实现了用户名验证,密码验证,以及手机号验证,这一章我们将实现短信验证码实现
实现注册要完成的图表
实现注册模块的整体流程
根据流程图总结注册业务包含如下功能
注册页面图片验证码用户名检测是否注册手机号检测是否注册短信验证码注册保存用户数据
八、获取短信验证码功能
1.业务流程分析
生成短信验证码发送短信保存短信验证码与发送记录(如果发生没收到验证码问题,可以进行查询,判断哪里出了问题)2.接口设计
接口说明:
参数说明:
返回结果:
{"errno": "0", "errmsg": "发送短信验证码成功!", }
3.短信验证码平台-云通讯
本项目中使用的短信验证码平台为云通讯平台,文档参考地址
主要是因为可以免费测试,注册后赠送8元用于测试。
开发参数:
_accountSid = '开发者主账号中的ACCOUNT SID'# 说明:主账号Token,登陆云通讯网站后,可在控制台-应用中看到开发者主账号AUTH TOKEN_accountToken = '开发者主账号中的AUTH TOKEN'# 请使用管理控制台首页的APPID或自己创建应用的APPID_appId = '开发者主账号中的AppID(默认)'# 说明:请求地址,生产环境配置成_serverIP = ''# 说明:请求端口 ,生产环境为8883_serverPort = "8883"# 说明:REST API版本号保持不变_softVersion = '-12-26'
设置测试手机号码
3. 创建utils文件将yuntongxun文件夹移到该文件夹下
yuntongxun文件资源:链接:/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 formsfrom 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 loggingimport randomfrom django.http import HttpResponsefrom django.views import Viewfrom django_redis import get_redis_connectionfrom user.models import Userfrom utils.json_res import json_responsefrom utils.res_code import Code, error_mapfrom utils.captcha.captcha import captchafrom utils.yuntongxun.sms import CCPfrom . import constantsfrom .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 testerr_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触发函数fnCheckUsernamefunction fnCheckUsername() {isUsernameReady = false; //校验用户名,没填入用户名为falselet 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触发函数fnCheckPasswordfunction fnCheckPassword() {isPasswordReady = false; //校验密码,没填入密码为falselet 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触发函数fnCheckMobilefunction 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 protectionreturn (/^(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','monMiddleware',# 'django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',]