前言
验证码中还有一种依靠鼠标点击验证的验证码
这类验证码主要由背景图片与字符以及点击后的反馈三部分组成,本文为简单版,故只实现前两部分,并且不使用汉字作为展示,转而使用英文字符或数字。
后端
验证码生成
首先处理验证码部分,这一部分不仅要生成验证码,还要生成验证码在背景中的位置信息即x、y值,同时需要保证验证码位置不能有重合部分。其中生成位置部分使用了最简单的while循环来保证位置不重合
# locations = [[左上x, 左上y], ...]
def location_determine(locations):
# 采用简单但耗时的方法
while 1:
x = random.randint(20, 390)
y = random.randint(15, 230)
flag = True
# 逐个判断
for location in locations:
location_x, location_y = location[0], location[1]
# 双重满足,构成方形区域
if x in range(location_x, location_x + 46) and y in range(location_y, location_y + 46):
flag = False
break
if flag:
return x, y
接着将各个字符的位置信息存储进session中,方便后续调用进行判断操作
# locations = [[左上x, 左上y], ...]
def location_set(request, locations):
# 传入[]表示为第一次取点
locations = [[-45, -46]] if locations == [] else locations
# 调用函数,确定位置
x, y = location_determine(locations)
if locations[0][1] < 0:
request.session.update({'x1': x, 'y1': y})
else:
length = len(locations) + 1
request.session.update({f'x{length}': x, f'y{length}': y})
return [x, y]
生成验证码时使用set存储验证码防止出现相同字符,并且调用以上函数确定验证码的值与位置
# 生成验证码
def code_determine():
# 使用集合存储验证码,防止重复
key = set()
code = ''
while len(key) != 3:
random_upper = chr(random.randint(65, 90)) # 大写字母
random_lower = chr(random.randint(97, 122)) # 小写字母
random_int = str(random.randint(0, 9)) # 数字
# 随机选择
temp = random.choice([random_int, random_upper, random_lower])
# 如果重复则跳过该次循环
if temp in key:
continue
key.add(temp)
code += temp
return code
背景图片处理
背景的处理与滑块验证码一文类似
def click_background(request):
# 选择图片
all_images = list(os.listdir('././static/imgs/click'))
img_url = os.path.join('././static/imgs/click', random.choice(all_images))
background_obj = Image.open(img_url)
# 修改图片大小
background_obj = background_obj.resize((500, 300))
# 实例化画笔
img_draw = ImageDraw.Draw(background_obj)
# 设置字体样式, 字体大小
img_font = ImageFont.truetype('static/font/font_1.ttf', 45)
locations = []
code = request.session.get('code')
for i in code:
# 确定位置
x, y = location_set(request, locations)
img_draw.text(xy=(x, y), text=i, font=img_font,
fill=random.choice(['red', 'green', 'blue', 'black']))
# 在locations中添加
locations.append([x, y])
# 临时存储
io_obj = BytesIO()
background_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
判断部分
判断用户点击的正确与否不仅包括点击位置的正确,也包括点击顺序的正确和点击次数的正确。了解判断原理后,代码实现就更为简单
def click(request):
# 验证部分
if request.method == 'POST':
# 获取前端传的数据
data = json.loads(request.body)
coordinates = data.get('coordinates', [])
length = len(coordinates)
# 排除多点和少点
if length != 3:
return JsonResponse({'info': 2})
flag = True
# 按顺序验证
for i in range(3):
x = int(request.session.get(f'x{i+1}'))
y = int(request.session.get(f'y{i+1}'))
# 设置合理的判断点击范围
if coordinates[i]['x'] not in range(x, x+45) or coordinates[i]['y'] not in range(y, y+45):
flag = False
if flag:
return JsonResponse({'info': 1})
else:
return JsonResponse({'info': 2})
# 设置验证码
code = code_determine()
# 验证码存储
request.session.update({'code': code})
return render(request, 'click.html')
前端
前端的重点为如何将用户点击的位置信息传递给后端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>验证码</title>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<h1 style="text-align: center">
请依次点击 {{ request.session.code|join:" " }}
</h1>
<div style="text-align: center;margin-bottom: 20px">
<img src="/picture/click_background" id="area_click">
</div>
<div style="text-align: center">
<button type="submit" style="text-align: center" id="id_confirm">确认</button>
</div>
<script>
let list = []
document.getElementById('area_click').addEventListener('click', function (event) {
let x = event.offsetX;
let y = event.offsetY;
list.push({x, y})
})
document.getElementById('id_confirm').addEventListener('click', function () {
fetch('/picture/click/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({coordinates: list}) // 将坐标数组作为 payload 发送
})
.then(response => response.json())
.then(data => {
let obj = JSON.stringify(data);
let info = JSON.parse(obj);
// 处理从后端接收到的数据
if(info.info === 1){
alert('成功');
}else {
alert('失败');
}
})
})
</script>
</body>
</html>
效果展示
总结
后端关键部分的代码几乎没有使用算法,但是简单易懂,同时改进空间较大。对于用户点击后的反馈展示这一拓展也没有实现,当然背景图片处理部分作为一个独立的函数,实现反馈功能对函数结构的改变较少,还是便于扩展的。
大二在读,不足请指正