- 验证在项目中可能多处使用,还有多种验证方法(本项目只涉及图形验证和短信验证),所以需要将验证独立成一个APP应用
一、准备工作
1、创建子应用 verifications
- 在命令行,进入apps包目录下
- 执行命令:python …\manage.py startapp verifications
2 、注册此子应用
- ./lgshop/dev.py
3、定义主路由
- 如果在主路由中进行区分代码,直接象users子应用一样即可,如果不区分,就需要到子路由中去区分,如 验证模块、首页等
3、定义子路由
- ./apps/verifications/urls.py
- 因此模块无需单独使用,故不需要命名
一、生成图形验证码
1、生成图形验证码逻辑分析
- 前端向后端申请图形验证码,通过uuid来区分身份
- 后端生成图形验证码,并制作成验证码图片数据
- 将图形验证码强果保存到redis中,以便后面验证
- 将验证码图片数据传回前端进行展示
2、图形验证码接口设计
2.1 请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | image_codes/(?P[\w-]+)/ |
2.2 请求参数:路径参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
uuid | string | 是 | 唯一编号 |
2.3 响应结果
3、生成图形验证码接口实现
-
生成图形验证码接口只提供GET即可
-
通用调用captcha.generate_captcha()即可生成图形验证码
- captcha是一个图形验证码生成包,复制到特定目录,导入即可使用
- captcha是一个图形验证码生成包,复制到特定目录,导入即可使用
-
将验证码文件部分保存到redis中,以使后面校验用
- 将验证码存放到redis 3数据库中
# ./lgshop/dev.py
"verify_code": {
# 验证码
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/2",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
- 用http方式,将图片数据传回前端
# ./verifications/views.py
from django.shortcuts import render
from django.views import View
from .libs.captcha.captcha import captcha
from django_redis import get_redis_connection
from django import http
class ImageCodeView(View):
"""图形验证码"""
def get(self,request,uuid):
'''
:param request:
:param uuid: 通用唯一识别符,用于标识唯一图片验证码属于哪个用户的
:return: image/jpg
'''
# 生成图片验证码
text, image = captcha.generate_captcha()
# print(text, image)
# 保存图像验证码,保存到redis
redis_conn = get_redis_connection('verify_code')
# name time value
redis_conn.setex('img_%s' % uuid, 300, text)
# 响应图形验证码
return http.HttpResponse(image, content_type='image/png')
4、生成图形验证码子路由定义
# ./verifications/urls.py
from django.urls import path, re_path
from . import views
urlpatterns = [
# 图形验证码 \w [A-Za-z0-9_]- uuid 78b4d5b7-5157-4b2a-bf48-ba616e169d66
re_path(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImageCodeView.as_view())
]
- 注:[\w-]+:\w表示匹配字母数字及下划线,[]表示匹配里面包含的字符,+表示匹配多次
5、前端代码实现
5.1 JS方法实现
- 当注册页面一刷,图形验证码就应该更新并显示出来
- vue.js 中有一个方法:mounted(),页面加载完成之后会被调用的方法
- 在mounted()方法中,向后端发送一个请求,申请一个图形验证码
- 发送请求前,还需要生成一个uuid (common.js中generateUUID()可以生成uuid)
- 因 mounted()和 图形验证码点击事件都要调用 生成图形验证码调用
# ./static/js/register.js
data: {
// 数据对象
// ... ...
image_code_url: '',
uuid: '',
// 页面加载完成之后会被调用的方法
mounted(){
// 生成图形验证码
this.generate_image_code()
},
methods: {
// 生成图片验证码
generate_image_code(){
// uuid 发生变化
this.uuid = generateUUID();
this.image_code_url = '/image_codes/'+ this.uuid +'/'
},
5.2 register.html相应修改
- 显示图形验证码的地方,需要绑定属性 v-bind
- 同时,还要设置点击事件@click,当点击此图片时,会自动刷新图形验码
二、封装容联云短信发送类
1、在容联云注册用户(免费注册,完成个人认证)
2、创建应用,并复制APP ID 和 APP TOKEN
3、定制短信模板(也可以使用系统提供的测试模板1 号模板发送验证码)
4、添加测试号码:
5、参考开发文档,开发演示代码
6、开发短信发送模块
- 在libs包下建立ronglianyun包,并建 ccp_SMS.py
from ronglian_sms_sdk import SmsSDK
accId = '容联云通讯分配的主账号ID'
accToken = '容联云通讯分配的主账号TOKEN'
appId = '容联云通讯分配的应用ID'
def send_message():
sdk = SmsSDK(accId, accToken, appId)
tid = '1' # 应用是系统提供的开发测试模板及编号
mobile = '手机号1,手机号2'
datas = ('变量1', '变量2')
resp = sdk.sendMessage(tid, mobile, datas)
result=json.load(resp)
if result["statusCode"]=="000000":
return 0
else:
return -1
if __name__=="__main__":
send_message()
7、优化封装短信发送模块
- 每发送一次短信,就会调用发送短信发送模块时,都会实例化SmsSDK类,对资源消耗较大,应采用单例模式
7.1 定义发送短信的单例类
- 定义CCP类,重写__new__()方法
- 首先判断是否存在一个属性(_instance,自定义的)
- 不存在,调用父类super()的__new__()方法,给属性赋值,并将相关参数传递进去,然后再设置该属性添加一个sdk,赋值为发送短信类实例
- 最后返回该属性
class CCP(object):
"""发送短信的单例类"""
def __new__(cls,*args,**kwargs):
if not hasattr(cls,"_instance"):
cls._instance=super().__new__(cls,*args,**kwargs)
cls._instance.sdk=SmsSDK(accId, accToken, appId)
return cls._instance
7.2 优化发送短信模块
- 用单例化短信发送类优化
- 发送验证码参数化
from ronglian_sms_sdk import SmsSDK
import json
accId = '容联云通讯分配的主账号ID'
accToken = '容联云通讯分配的主账号TOKEN'
appId = '容联云通讯分配的应用ID'
class CCP(object):
"""发送短信的单例类"""
def __new__(cls, *args, **kwargs):
# 如果是第一次实例化,应该返回实例化后的对象,如果是第二次实例化,应该返回上一次实例化后的对象
# 判断是否存在类属性 _instance
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls, *args, **kwargs)
cls._instance.sdk = SmsSDK(accId, accToken, appId)
return cls._instance
def send_message(self, tid, mobile, datas):
sdk = self._instance.sdk
# tid = '容联云通讯创建的模板ID'
# mobile = '手机号1,手机号2'
# datas = ('变量1', '变量2')
# tid = '1'
# mobile = '18908656327'
# datas = ('2345', '5')
resp = sdk.sendMessage(tid, mobile, datas)
result = json.loads(resp)
if result["statusCode"] == "000000":
return 0
else:
return -1
if __name__ == "__main__":
c = CCP()
c.send_message("1", "18908656327", ("1234", "5")) # 测试1号模板,手机号,(短信验证码,有效时间)
三 、发送短信验证码
1、发送短信验证码逻辑分析
- 点击“发送短信验证码”时,判断图形验码是否输入,
- 为提高用户体验,前端采用ajax发送请求
- 后端接收请求并校验参数
- 接收并判断手机号、uuid 和图形验证码等必须参数是否为空
- 校验手机号是否正确
- 从redis中提取图形验证码,并删除此图形验证码
- 比较前端传递的图形难证码和后端是否相同
- 如果不相同,返回错误
- 生成短信验证码,并保存到redis中
- 通过容联云平台 发送短信验证码
- 向前端发送响应结果
2、短信验证码接口设计
2.1 请求方式
|选项|方案|
|请求方法|GET|
|请求地址|/sms_codes/(?P1[3-9]\d{9})/|
2.2 请求参数:路径参数和查询字符串
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
mobile | string | 是 | 手机号 |
image_code | string | 是 | 图形验证码 |
uuid | string | 是 | 唯一编号 |
2.3 响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
3、发送短信验证码接口实现
3.1 初步实现
- 接收参数:uuid和图形验证码,手机号已在路由中解析出来了,并对正确性进行了校验
- 判断两个参数是否存在:这两上是必传参数
- 如果不存在,返回错误代码及错误信息
- 连接redis数据库,读取后端存储的图形验证码
- 如果为None,表示图形验证码过期失效,返回错误
- redis中取出来的二进制,需要.decode()
- 为了提高用户体验,不区分大小写,全部转换成大写或小写再比较
- 删除后端存储的图形验证码(图形验证码只能使用一次,否认对错)
- 判断前后端图形验证码是否一致
- 如果不一致,返回错误
- 生成6位短信验码 : “%06d” % random.ranint(0,999999999)
- “%d” :整数,“%6d”:不超过6位的整数,可能小于6位,“%06d”:6位整数,不够6位补0
- 保存短信验证码到redis中
- 调用容联云发送短信类,发送短信验证码
- 需要导入短信发送验证码类, 路径从apps开始,
- 短信验证码类需要实例化,即 CCP()
- 返回发送状态
3.2 代码优化 : 避免频繁发送短信验证码
- 防止恶意注册,需要限制短信发送频次: 1分钟内仅能发送一次,否则不发送
- 解决办法:
- 在后端也要限制用户请求短信验证码的频率。60秒内只允许一次请求短信验证码。
- 在Redis数据库中缓存一个数值,有效期设置为60秒。
- 实现方法:
- 校验完用户参数后,读取redis中发送状态(send_flag_手机号),如果存在,返回错误信息(过于频繁)
- 当保存短信验证码后,就保存发送状态(send_flag_手机号)并设置有效期1分钟
3.3 完整代码
class SMSCodeView(View):
'''短信验证码发送'''
def get(self,request,mobile):
'''
:param request:
:param mobile:
:return:
'''
# http://127.0.0.1:8000/sms_codes/手机号/?uuid=cea94f82-4329-41e4-80df-cca815875a43&image_code=XQDI
# 接收参数,校验参数
# print(mobile)
uuid = request.GET.get('uuid') # uuid
image_code_client = request.GET.get('image_code') # 图形验证码,查看前端JS中传递参数名称(不是表单提交)
if not all([uuid, image_code_client]):
return http.HttpResponseForbidden('缺少必传参数') # HttpResponseForbidden:返回403 status code.
# 提取图形验证码
redis_conn = get_redis_connection('verify_code')
# 判断用户是否频繁发生短信验证码
send_flag = redis_conn.get('send_flag_%s' % mobile)
if send_flag:
return http.JsonResponse({
'code': RETCODE.THROTTLINGERR, 'errmsg': '发送短信过于频繁'})
image_code_server = redis_conn.get('img_%s' % uuid)
# 提取图形验证码失效了
if image_code_server is None:
return http.JsonResponse({
'code': RETCODE.IMAGECODEERR, 'errmsg': '图形验证码已失效'})
# 删除图形验证码
redis_conn.delete('img_%s' % uuid)
# 对比图形验证码
# print(image_code_client) # kl5X
# print(image_code_server)
image_code_server = image_code_server.decode()
if image_code_client.lower() != image_code_server.lower():
return http.JsonResponse({
'code': RETCODE.IMAGECODEERR, 'errmsg': '输入图形验证码有误'})
# 生成短信验证码
# 生成6位的随机数 %6d
sms_code = "%06d" % random.randint(0, 999999)
# 保存短信验证码
redis_conn.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
# 保存发送短信验证码的标记
redis_conn.setex('send_flag_%s' % mobile, constants.SEND_SMS_CODE_TIMES, 1)
# 发送短信 send_message(self, mobile, datas, tid): 300/60 浮点数
CCP().send_message( constants.SEND_SMS_TEMPLATE_ID, mobile, (sms_code, constants.SMS_CODE_REDIS_EXPIRES//60))
# 响应结果
return http.JsonResponse({
'code': RETCODE.OK, 'errmsg': '发送短信验证码成功'})
4、发送短信验证码子路由定义
re_path(r'^sms_codes/(?P<mobile>1[3-9]\d{9})/$', views.ImageCodeView.as_view()),
5、前端代码实现
5.1 vue绑定界面
- register.html页面中进行修改
# ./templates/register.html
<li>
<label>短信验证码:</label>
<input type="text" v-model="sms_code" @blur="check_sms_code" name="sms_code" id="msg_code" class="msg_input">
<a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]</a>
<span class="error_tip" v-show="error_sms_code">[[ error_sms_code_message ]]</span>
</li>
- register.js中定义相关变量和方法
5.2 发送短信验证码的JS方法实现
- 发送短信验证码,前提条件是必须有正确的手机号并输入图形验证码
- 故先校验手机号和图形验证码,如果有true ,返回
- 检查发送标志是否为已发送,如果为已发送,返回
- 发送标志置为已发送(上锁)
- 生成请求url,并通过ajax方式发送向后发送
axios.get(url,{
responseType: 'json'})
- 如果请求发送正常: .then(response=>{}) ,发送异常 : .catch(error=>{})
- 如果返回正常,显示60秒倒计时: 定时器 setInterval(‘回调函数’, ‘时间间隔(毫秒)’)
- 回调函数:()=>{}
- 定义一个计数变量,初始值60(每1秒减1 )
- 当 计数变量=1 时,停止回调函数:clearInterval(t); 将信息显示改回“获取短信验证码” ; 刷新图形验证码;发送标志置为未发送(解锁)
- 当 计数变量>1 时, 计数变量减1 ;将信息显示改回“计数变量秒” ;
- 如果返回不是0,分类处理错误类型;发送标志置为未发送(解锁)
- 如果返回4001,显示错误信息,否则都是短信验证码错误,显示错误信息
- 如果发送异常 : .catch(error=>{})
- 打印错误信息;发送标志置为未发送(解锁)
// 发送短信验证码 ajax
send_sms_code(){
//校验手机号和图形验证码
this.check_mobile();
this.check_image_code();
// alert(this.error_mobile);
// alert(this.error_image_code);
if (this.error_mobile ==true || this.error_image_code==true){
// 如果有误,不发送短信验证码申请
return;
}
let url = '/sms_codes/'+ this.mobile +'/?uuid='+ this.uuid +'&image_code=' + this.image_code;
alert(url)
axios.get(url,{
responseType: 'json'
})
.then(response => {
//请求正常
if (response.data.code == '0'){
//返回成功
// 展示倒计时60秒
let num = 60;
// setInterval('回调函数', '时间间隔(毫秒)')
let t = setInterval(()=>{
if (num == 1){
// 停止回调函数
clearInterval(t);
this.sms_code_tip = '获取短信验证码';
// 重新生成图形验证码
this.generate_image_code();
this.send_flag = false;
}else{
num -= 1;
this.sms_code_tip = num + '秒';
}
}, 1000)
}else{
if(response.data.code == '4001'){
// 图形验证码错误
this.error_sms_code_message = response.data.errmsg;
this.error_sms_code = true;
}else {
// 短信证码错误
this.error_sms_code_message = response.data.errmsg;
this.error_sms_code = true;
}
this.send_flag = false;
}
})
.catch(error => {
//请求异常
console.log(error.response);
this.send_flag = false;
})
},
四、完善一下注册
1、后端注册逻辑,增加短信验证码判断
# 短信验证码
sms_code_client = register_form.cleaned_data.get('sms_code')
# 判断短信验证码输入是否正确
redis_conn = get_redis_connection('verify_code')
sms_code_server = redis_conn.get('sms_%s' % mobile)
if sms_code_server.decode() is None:
return render(request, 'register.html', {
'sms_code_errmsg': '短信验证码已失效'})
# print(sms_code_server.decode())
# print(sms_code_client)
if sms_code_server.decode() != sms_code_client:
return render(request, 'register.html', {
'sms_code_errmsg': '输入短信验证码有误'})
2 、前端增加错误信息显示
2.1 图形验证码错误或失效(发送短信验证码时返回错误信息)
{
% if sms_code_errmsg %}
<span class="error_tip">
{
{
sms_code_errmsg }}
</span>
{
% endif %}
五、数据库管道处理
- 短信验证码处理时,需要向redis保存短信验证码,紧接着要向redis中保存发送标志
- 同一时间执行两次redis操作,可以用管道进行优化
- redis管道 详见 《redis管道》
# 创建管道
pl = redis_conn.pipeline()
# 将命令添加到队列
# 保存短信验证码
pl.setex('sms_%s' % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
# 保存发送短信验证码的标记
pl.setex('send_flag_%s' % mobile, constants.SEND_SMS_CODE_TIMES, 1)
# 执行
pl.execute()