1 图形验证码接口设计
将后端⽣成的图⽚验证码存储在redis数据库2号库。
结构:
- {'img_uuid':'0594'}
1.1 创建验证码⼦应⽤
$ cd apps
$ python ../../manage.py startapp verifications
# 注册新应⽤
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'userapp',
'newsapp',
'verifications',
]
1.2 图形验证码接⼝设计
1.2.1 请求⽅式
选项 | ⽅案 |
请求⽅法 | GET |
请求地址 1 | /imgcodes/(?P[\w-]+)/ |
1.2.2 请求参数:路径参数
参数名 | 类型 | 是否必传 | 说明 |
uuid | string | 是 | 唯⼀编号 |
1.2.3 响应结果 图⽚验证码格式:image/png
1.3 图形验证码接⼝定义
1.3.1 图形验证码视图
# views.py视图⽂件
class ImageCode(View):
def get(self, request, uuid):
pass
1.3.2 配置路由
# 项⽬根路由
re_path('^', include(('verifications.urls', 'verifications',),
namespace='verify')),
# ⼦路由
re_path('^image_code/(?P<uuid>[\w-]+)/$', views.ImageCode.as_view())
2 图片验证码后端逻辑
2.1 配置Redis数据库
# 配置redis数据库专⻔存储验证码
"verify_code": { # 验证码
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://192.168.1.6:6379/2",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
2.2 安装模块
pip install pillow
pip install captcha
2.3 图⽚验证码视图
# constants.py⽂件内容
# 图⽚验证码有效期,单位:秒
IMAGE_CODE_REDIS_EXPIRES = 300
# views.py视图⽂件
from captcha.image import ImageCaptcha
from django_redis import get_redis_connection
from newsdemo.apps.verifications import constants
class ImageCode(View):
def get(self, request, uuid):
# 随机⽣成四位数字
seeds = string.digits
random_str = random.choices(seeds, k=4)
imgcode = "".join(random_str)
# ⽣成图⽚验证码
img = ImageCaptcha().generate(chars=imgcode)
# 保存图⽚验证码到redis
redis_conn = get_redis_connection('verify_code')
redis_conn.setex('img_%s' % uuid,
constants.IMAGE_CODE_REDIS_EXPIRES, imgcode)
return http.HttpResponse(img, content_type='image/png')
3 图片验证码前端逻辑
3.1 Vue实现图形验证码展示
3.1.1 register.js
mounted(){
// ⽣成图形验证码
this.generate_img_code();
},
methods:{
generate_img_code:function () {
// ⽣成UUID generateUUID() : 封装在common.js⽂件中,需要提前引⼊
this.uuid = generateUUID();
// 拼接图形验证码请求地址
this.img_url = "/imgcodes/" + this.uuid + "/";
},
...
}
3.1.2 register.html
<p class="form-row form-row-wide">
<input style="width: 250px;" placeholder="图⽚验证码"
type="text" class="input-text">
<img style="height: 40px;float: right;" :src="img_url"
@click="generate_img_code">
<span class="error-tip">图⽚验证码有误</span>
</p>
3.1.3 图形验证码展示和存储效果
3.2 Vue实现图形验证码校验
3.2.1 register.html
<p class="form-row form-row-wide">
<input style="width: 250px;" placeholder="图⽚验证码" v-model='imgcode'
@blur="check_imgcode" name="imgcode"
type="text" class="input-text">
<img style="height: 40px;float: right;" :src="img_url"
@click="generate_img_code">
<span class="error-tip" v-show="error_imgcode">${error_imgcode_msg}</span>
</p>
3.2.2 register.js
// 校验图⽚验证码
check_imgcode:function () {
if (!this.imgcode) {
this.error_imgcode_msg = '请填写图⽚验证码';
this.error_imgcode = true;
} else {
this.error_imgcode = false;
}
}
3.2.3 校验效果
4 短信验证码接口设计
4.1 短信验证码接⼝设计
4.1.1 请求⽅式
选项 | ⽅案 |
请求⽅法 | GET |
请求地址 1 | /sms_codes/(?P1[35789]\d{9})/ |
4.1.2 请求参数:路径参数和查询字符串
参数名 | 类型 | 是否必传 | 说明 |
phone | string | 是 | ⼿机号 |
imgcode | string | 是 | 图⽚验证码 |
uuid | string | 是 | 唯⼀编号 |
4.1.3 响应结果:JSON
响应结果 | 响应内容 |
code | 状态码 |
errmsg | 错误信息 |
4.2 短信验证码接⼝定义
class SMSCode(View):
"""短信验证码"""
def get(self, reqeust, phone):
"""
:param reqeust: 请求对象
:param phone: ⼿机号
:return: JSON
"""
pass
4.3 知识要点
- 保存短信验证码是为注册做准备的。
- 为了避免⽤户使⽤图形验证码恶意测试,后端提取了图形验证码后,⽴即删除图形验证码。
- Django不具备发送短信的功能,所以我们借助 第三⽅的互亿⽆线短信平台 来帮助我们发送短信验证码。
5 互亿无线短信平台
5.1 平台介绍
互亿⽆线官⽹ https://www.ihuyi.com/
⽬前注册可免费使⽤50条短信验证码
5.2 平台管理中⼼
5.3 接⼊⽂档
跳转地址:短信验证码接入指南_短信平台帮助_互亿无线 (ihuyi.com)
5.4 配置参数
在dev.py配置⽂件中添加参数
# 互亿⽆线短信验证码参数
# APIID
APIID = "C74**64"
# APIKEY
APIKEY = "62de***8932d50c2"
5.5 下载Python3.8⽀持的SDK
# utils/huyi_sms/sms3.py
# !/usr/local/bin/python
# -*- coding:utf-8 -*-
from urllib.request import urlopen
from urllib.parse import urlencode
from django.conf import settings
import json
def send_sms_code(smscode, phone):
# APIID(⽤户中⼼【验证码通知短信】-【产品纵览】查看)
account = settings.APIID
# APIKEY(⽤户中⼼【验证码通知短信】-【产品纵览】查看)
password = settings.APIKEY
text = "您的验证码是:%s。请不要把验证码泄露给其他⼈。" % smscode
data = {'account': account, 'password': password, 'content': text,
'mobile': phone, 'format': 'json'}
req = urlopen(url='https://106.ihuyi.com/webservice/sms.php?'
'method=Submit',
data=urlencode(data).encode())
content = req.read().decode()
print(content)
# code等于2代表提交成功,否则提交失败
# smsid等于0代表提交失败,否则显示⻓度20流⽔号
# b'{"code":2,"msg":"\xe6\x8f\x90\xe4\xba\xa4\xe6\x88\x90\xe5\x8a\x9f",
# "smsid":"16063783563405105174"}'
return json.loads(content)
6 短信验证码后端逻辑
6.1 短信验证码后端逻辑实现
class SMScodeView(View):
def get(self, request, phone):
"""
匹配并删除图形验证码
发送短信验证码
:param request:
:param phone:
:return:
"""
# 1. 获取请求参数(路径参数+查询参数)
imgcode_client = request.GET.get('imgcode', '')
uuid = request.GET.get('uuid', '')
# 2. 校验参数
if not all([phone, imgcode_client, uuid]):
return JsonResponse({'code': '4001',
'errormsg': '缺少必须传递的参数'})
# 3. 校验图⽚验证码(⽤户输⼊验证码和⽣成验证码)
redis_conn = django_redis.get_redis_connection('verify_code')
imgcode_server = redis_conn.get('img_%s' % uuid)
print(uuid)
print(imgcode_server)
# 3.1 图⽚验证码是否过期
if imgcode_server is None:
return JsonResponse({'code': '4002',
'errormsg': '图⽚验证码已经过期'})
# 3.2 匹配图⽚验证码
if imgcode_client.lower() != imgcode_server.decode('utf-8').lower():
return JsonResponse({'code': '4003',
'errormsg': '图⽚验证码不匹配'})
try:
# 删除redis中的图⽚验证码
redis_conn.delete('img_%s' % uuid)
except Exception as e:
logger.error(e)
# 4. ⽣成短信验证码(6位)
seed = string.digits
r = random.choices(seed, k=6)
smscode_str = "".join(r)
# 5. 保存短信验证码(redis数据库2号库存储)
redis_conn.setex('sms_%s' % uuid, 60, smscode_str)
# 6. 发送短信验证码
ret = send_sms_code(smscode_str, phone)
if ret.code == 2:
return JsonResponse({'code': 200, 'errormsg': 'OK'})
# 7. 返回响应结果
return JsonResponse({'code': 5001, 'errormsg': '发送短信验证码错误'})
7 短信验证码前端逻辑
7.1 Vue绑定短信验证码
7.1.1 register.html
<p class="form-row form-row-wide">
<input style="width: 250px;" placeholder="短信验证码" v-model='smscode'
@blur="check_smscode" name="msgcode"
type="text" class="input-text" id="reg_mescode">
<span class="error-tip" v-show="error_code">${error_msgcode_msg}</span>
<a href="javascript:;" style="position: relative;left:150px;"
@click="send_code()">${smscode_btn} </a>
</p>
7.1.2 register.js
check_smscode:function () {
//获取验证码⻓度
let reg = /^\d{6}$/;
//校验规则
if (!reg.test(this.smscode)) {
this.error_smscode_msg = '请填写短信验证码';
this.error_smscode = true;
} else {
this.error_smscode = false;
}
}
7.2 axios请求短信验证码
7.2.1 发送短信验证码事件处理
send_smscode:function () {
//发送短信验证码
//1.判断短信验证码是否正在发送
if (this.send_flag) {
return;
}
//2.修改发送状态
this.send_flag = true;
//3.校验⽤户输⼊的⼿机号和图⽚验证码
this.check_phone();
this.check_imgcode();
if (this.error_phone || this.error_imgcode) {
this.send_flag = false;
return;
}
//4.发送短信验证码
var url = '/smscodes/' + this.phone + '/?imgcode=' +
this.imgcode + '&uuid=' + this.uuid;
axios.get(url, {
responseType: 'json'
}).then(response => {
console.log(response.data.code);
console.log(typeof (response.data.code));
if (response.data.code == '200') {
let num = 60;
var i = setInterval(() => {
if (num == 1) {
clearInterval(i);
this.smscode_btn = '获取短信验证码';
this.send_flag = false;
} else {
num -= 1;
this.smscode_btn = '倒计时:' + num + '秒';
}
}, 1000, 60)
} else {
if (response.data.data == '4001' || response.data.data == '4002'
|| response.data.data == '4003' ||
response.data.data == '5001') {
this.error_smscode_msg =
response.data.errormsg;
this.error_smscode = true;
}
//重新⽣成图⽚验证码
this.generate_imgcode();
//重置发送状态
this.send_flag = false;
}
}).catch(error => {
console.log(error.response);
});
}
8 用户注册时短信验证码校验功能
8.1 注册时短信验证前端逻辑
8.1.1 register.html
<p class="form-row form-row-wide">
<input style="width: 230px;" v-model="smscode" placeholder="短信验证码"
@blur="check_smscode" name="msgcode"
type="text" class="input-text" id="reg_mescode">
<span class="error-tip" v-show="error_smscode">${error_smscode_msg}</span>
<a href="javascript:;" style="font-size: 16px;text-align: center;
font-weight: normal;float: right" id="reg_mescode_btn"
able="able" @click="send_smscode">${smscode_btn}</a>
</p>
8.1.2 register.js
check_smscode:function () {
// 1.短信验证码格式校验
let reg = /^\d{6}$/;
if (!reg.test(this.smscode)) {
this.error_smscode = true;
} else {
this.error_smscode = false;
}
// 2.⼀致性校验
if (!this.error_smscode) {
axios.get('/check_smscode/' + this.phone + '/?smscode='
+ this.smscode, {
responseType: 'json'
}).then(response => {
let code = response.data.code;
if (code == '4001' || code == '4002' || code ==
if (code == '4001' || code == '4002' || code == '4003') {
this.error_smscode = true;
this.error_smscode_msg =
response.data.errormsg;
} else {
this.error_smscode = false;
}
})
}
}
8.2 注册时短信验证后端逻辑
# verifications/views.py
class CheckSMScode(View):
def get(self, request, phone):
"""
⽤户注册时短信验证码校验
:param request:
:param phone:
:return:
"""
# 接收请求参数
smscode_client = request.GET.get('smscode', '')
# 校验参数
if not all([phone, smscode_client]):
return JsonResponse({'code': '4001', 'errormsg': '缺少必传参数'})
# 查询服务器端短信验证码
redis_conn = django_redis.get_redis_connection('verify_code')
smscode_server = redis_conn.get('sms_%s' % phone)
# 匹配(⾮空判断/有效性判断)
if smscode_server is None:
return JsonResponse({'code': '4002', 'errormsg': '短信验证码失效'})
smscode_server = smscode_server.decode('utf-8')
if smscode_client != smscode_server:
return JsonResponse({'code': '4003',
'errormsg': '短信验证码不⼀致'})
# 响应结果
return JsonResponse({'code': '200', 'errormsg': 'OK'})
9 避免频繁发送短信验证码
存在的问题:
- 虽然我们在前端界⾯做了60秒倒计时功能。
- 但是恶意⽤户可以 绕过前端界⾯向后端频繁请求短信验证码。
解决办法:
- 在 后端也要限制 ⽤户请求短信验证码的频率。60秒内只允许⼀次请求短信 验证码。
- 在Redis数据库中缓存⼀个数值,有效期设置为60秒。
9.1 避免频繁发送短信验证码逻辑实现
9.1.1 提取并校验 is_send
is_send = redis_conn.get('is_send_%s' % phone)
if is_send:
return JsonResponse({'code': 4001, 'error_msg': '发送短信过于频繁'})
9.1.2 is_send 、smscode 存⼊redis数据库
# 保存短信验证码
redis_conn.setex('sms_%s' % phone, 60, smscode)
# 保存is_send
redis_conn.setex('is_send_%s' % phone, 60, 1)
9.1.3 界⾯渲染 频繁发送短信提示信息
if (response.data.code == '4001') {
this.error_smscode_msg = response.data.error_msg;
this.error_smscode_code = true;
}