这一篇文章将会完整的实现文字点选验证码,包括前端后端的实现逻辑,老规矩,交流QAQ 1690361973
项目示例用的是flask做后端,前端HTML+css+js,整体项目的文件结构如下
结构说明:手搓验证——整个项目的存放目录。static——静态文件存放的目录,包括,font——字体,js——js文件,Text_selectionimg——用于渲染文字的背景图。templates——存放HTML文件。333.py——后端代码,这里333只是个示例,文件名自取。
字体跟背景图就不多讲了,下面按文件目录顺序来分步讲解。
一,js文件实现的功能,从后端请求渲染好的点选图片,加载到网页元素中,实现点击并且记录点击的坐标,根据要点击的文字来记录点击次数,点击完成后自动提交坐标给后端。
具体代码如下
let clickCoordinates = []; // 用于存储点击坐标和顺序
document.getElementById('captcha-image').addEventListener('click', function (event) {
const img = event.target;
const rect = img.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
console.log('点击坐标:', x, y);
// 记录点击坐标
clickCoordinates.push({ x, y });
// 显示点击坐标
const coordDisplay = document.createElement('div');
coordDisplay.className = 'click-marker';
coordDisplay.style.left = `${x}px`;
coordDisplay.style.top = `${y}px`;
img.parentElement.appendChild(coordDisplay);
// 检查是否点击了四次
if (clickCoordinates.length === 4) {
console.log('四次点击坐标:', clickCoordinates);
// 发送点击坐标到后端
const xhr = new XMLHttpRequest();
xhr.open('POST', '/Click_to_verify/', true); // 替换为你的后端 API 端点
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
localStorage.setItem('sliderToken', response.token);
document.getElementById('error-message-1').textContent = response.message;
} else {
document.getElementById('error-message-1').textContent = response.message;
initializeCaptcha(); // 重新加载图像和初始化
}
}
};
xhr.send(JSON.stringify({ clickCoordinates }));
// 重置点击坐标
clickCoordinates = [];
}
});
function initializeCaptcha() {
// 清除之前的点击坐标显示
const clickMarkers = document.querySelectorAll('.click-marker');
clickMarkers.forEach(marker => marker.remove());
// 使用 Ajax 请求从后端获取 Base64 编码的图片和缺口坐标
const xhr = new XMLHttpRequest();
xhr.open('GET', '/Slide_verification/', true); // 替换为你的后端 API 端点
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const response = xhr.responseText;
console.log(response);
const backgroundBase64 = response;
const captchaImage = document.getElementById('captcha-image');
captchaImage.src = `data:image/png;base64,${backgroundBase64}`;
}
};
xhr.send();
}
// 获取 CSRF token 的函数
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const 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;
}
// 初始化验证码
initializeCaptcha();
二.HTML的作用也就是实现一个交互页面,代码如下
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>点击坐标记录示例</title>
<style>
.captcha-container {
position: relative;
width: 260px;
height: 130px;
border: 1px solid #ccc;
margin-bottom: 1px;
}
.captcha-image {
width: 100%;
height: 130px;
}
#captcha-container {
position: relative;
}
.click-marker {
position: absolute;
background-color: red;
width: 10px;
height: 10px;
}
</style>
</head>
<body>
<div class="captcha-container">
<img src="" class="captcha-image" id="captcha-image">
</div>
<div>
<span id="error-message-1">{{error_message}}</span>
</div>
</body>
<script src="{{ url_for('static', filename='js/Text_selection.js') }}"></script>
</html>
三.后端,后端的逻辑是接收到前端的请求之后处理渲染图片并记录文字的顺序跟坐标,将图片转成bs64编码返回,这样就不需要另外保存图片。根据前端请求返回的坐标跟存储的坐标进行比对,如果误差在你设置范围内那么就返回验证通过,反之返回验证失败,前端重新请求验证码。
具体代码如下
from PIL import Image, ImageDraw, ImageFont
import random
from flask import Flask,render_template, request, jsonify, session
import cv2
import numpy as np
import base64
from io import BytesIO
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
# 获取当前运行文件的绝对路径
current_file_path = os.path.abspath(__file__)
# 获取当前运行文件所在的目录
current_directory = os.path.dirname(current_file_path)
print(f"当前运行文件所在的目录: {current_directory}")
def generate_captcha(image_path, text, font_path=fr'{current_directory}\static\font\HYDiShengXiaRiTiU-2.ttf', font_size=24):
# 加载背景图像
background = cv2.imread(image_path)
# 调整图像大小为 260x130
target_width = 260
target_height = 130
background = cv2.resize(background, (target_width, target_height))
# 将 OpenCV 图像转换为 PIL 图像
pil_image = Image.fromarray(cv2.cvtColor(background, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_image)
# 加载字体
font = ImageFont.truetype(font_path, font_size)
# 分配区域
num_chars = len(text)
char_width = target_width // num_chars
# 在背景图像上绘制文字,并记录每个字符的坐标
coordinates = []
for index, char in enumerate(text):
max_x = target_width - 50 # 避免文字超出边界
max_y = target_height - 50
offset_x = random.randint(index * char_width, (index + 1) * char_width - 50)
offset_y = random.randint(0, max_y)
# 随机颜色
color = tuple(random.randint(0, 255) for _ in range(3))
# 获取文字的边界框
bbox = font.getbbox(char)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1] +10
# 绘制文字
draw.text((offset_x, offset_y), char, font=font, fill=color)
# 计算中心点坐标
center_x = offset_x + text_width / 2
center_y = offset_y + text_height / 2
# 记录字符及其中心点坐标和索引
coordinates.append((char, (center_x, center_y), index))
# 绘制边界框
draw.rectangle([(offset_x, offset_y), (offset_x + text_width, offset_y + text_height)], outline="red", width=1)
# 将 PIL 图像转换回 OpenCV 图像
background = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
# 显示图片
#cv2.imshow("Captcha", background)
#cv2.waitKey(0)
#cv2.destroyAllWindows()
# 将图片转换为 base64 编码
buffered = BytesIO()
pil_image.save(buffered, format="PNG")
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
# 保存图片
#cv2.imwrite(fr'{current_directory}\static\Text_selectionimg\captcha_output.png', background)
#print(fr'{current_directory}\static\Text_selectionimg\captcha_output.png')
# 返回字符及其中心点坐标
print("字符及其中心点坐标:", coordinates)
return {'captcha': {
"backgroundImage": img_base64,
"txt1": {'x':coordinates[0][1][0],'y':coordinates[0][1][1],'index':coordinates[0][2]},
"txt2": {'x':coordinates[1][1][0],'y':coordinates[1][1][1],'index':coordinates[1][2]},
"txt3": {'x':coordinates[2][1][0],'y':coordinates[2][1][1],'index':coordinates[2][2]},
"txt4": {'x':coordinates[3][1][0],'y':coordinates[3][1][1],'index':coordinates[3][2]},
}
}
# 使用示例
#coords = generate_captcha(fr'{current_directory}\1.jpg', '劳动部轻', font_path=fr'{current_directory}\static\font\HYDiShengXiaRiTiU-2.ttf')
#print("字符及其中心点坐标:", coords)
@app.route('/Slide_verification/',methods = ['GET'])
def Slide_verification():
# 获取文件夹中的所有图片文件
# 指定图片文件夹路径
folder_path = f"{current_directory}\static\Text_selectionimg"
valid_extensions = ['.jpg', '.jpeg', '.png']
images = [f for f in os.listdir(folder_path) if os.path.splitext(f)[1].lower() in valid_extensions]
if not images:
raise ValueError("No images found in the specified folder.")
# 随机选择一张图片
random_image = random.choice(images)
random_image_path = os.path.join(folder_path, random_image)
#print('random_image_path',random_image_path)
captcha_data = generate_captcha(random_image_path, '劳动部轻', font_path=fr'{current_directory}\static\font\HYDiShengXiaRiTiU-2.ttf')
#print('captcha_data',captcha_data)
session["coordi"] = [captcha_data['captcha']["txt1"],captcha_data['captcha']["txt2"],captcha_data['captcha']["txt3"],captcha_data['captcha']["txt4"]]
return captcha_data['captcha']['backgroundImage']
@app.route('/Click_to_verify/',methods = ['POST'])
def Click_to_verify():
# 获取提交的滑块位置
user_gap_left = request.json.get('clickCoordinates')
#user_gap_left = int(user_gap_left)
print('user_gap_left',user_gap_left)
# 获取存储的滑块位置
coordinate = session.get('coordi')
print('coordinate',coordinate)
# 检查每组值的 x 和 y 是否相差不超过容差范围
tolerance = 5 # 容差范围
correct = True
for user_coord, original_coord in zip(user_gap_left, coordinate):
distance_x = abs(user_coord['x'] - original_coord['x'])
distance_y = abs(user_coord['y'] - original_coord['y'])
if distance_x > tolerance or distance_y > tolerance:
correct = False
break
if correct:
return jsonify({"success": True, "message": "验证通过"})
else:
return jsonify({"success": False, "message": "验证失败"})
#先写好html 模板,然后再往里面传值
@app.route('/muban/',methods = ['POST', 'GET']) #主网址
def index():
if request.method == 'GET':
# render_template方法:渲染模板
# 参数1: 模板名称 参数n: 传到模板里的数据
return render_template('文字点选.html')
if __name__ == '__main__':
#app.config['JSON_AS_ASCII'] = False
app.run(host = '192.168.50.21', port=5000, debug =True,threaded=True) # debug =True 调试模式,代码有更改 服务器会自动重启
完整的步骤就是这样,不喜勿喷,转载请注明出处,维护开源和谐环境。