滑块验证完整实现教程(前端+后端+Nginx集成)
滑块验证的核心逻辑是:前端渲染滑块+缺口背景图,采集用户滑动轨迹;后端校验轨迹是否为真人行为(非机器匀速滑动),验证通过后生成时效token;Nginx拦截业务请求,校验token有效性后放行。以下是可直接落地的完整方案,包含前端、后端、部署全流程。
一、核心原理
- 前端:生成随机的背景图+缺口,监听鼠标/触摸滑动事件,采集滑动轨迹(时间戳、X/Y坐标、速度、加速度),滑动完成后将轨迹和缺口偏移量传给后端。
- 后端:校验轨迹特征(如滑动时长、速度波动、是否匀速、缺口偏移匹配度),真人轨迹会有"先快后慢/轻微抖动",机器轨迹多为"匀速直线";验证通过则生成短期有效token。
- Nginx:拦截业务请求,校验请求头/Cookie中的验证token,有效则放行,无效则重定向到滑块验证页面。
二、完整实现步骤
步骤1:环境准备
-
前端:无需框架,原生HTML+JS即可(也可适配Vue/React);
-
后端:Python 3.8+ + Flask + Pillow(生成验证图) + Redis(存储token/验证参数);
-
Nginx:确保包含
ngx_http_auth_request_module模块(默认编译,nginx -V验证); -
依赖安装:
bash# 后端依赖 pip install flask pillow redis requests # Redis(本地/云服务器,用于存储验证参数和token) # 参考安装:https://redis.io/docs/getting-started/installation/
步骤2:前端实现(滑块渲染+轨迹采集)
新建slider.html,实现滑块渲染、滑动监听、轨迹采集和验证请求:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滑块验证</title>
<style>
/* 滑块容器样式 */
.slider-container {
width: 320px;
height: 160px;
margin: 50px auto;
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.slider-bg {
width: 100%;
height: 120px;
position: relative;
border-radius: 4px;
overflow: hidden;
background: #f5f5f5;
}
.slider-gap {
position: absolute;
width: 40px;
height: 40px;
background: #fff;
border: 1px solid #e5e5e5;
box-shadow: 0 0 5px rgba(0,0,0,0.2);
cursor: move;
/* 缺口位置由后端返回,初始隐藏 */
display: none;
}
.slider-bar {
width: 100%;
height: 30px;
background: #f8f8f8;
margin-top: 10px;
border-radius: 15px;
position: relative;
cursor: pointer;
}
.slider-btn {
width: 40px;
height: 30px;
background: #409eff;
border-radius: 15px;
position: absolute;
top: 0;
left: 0;
box-shadow: 0 0 5px rgba(64,158,255,0.5);
cursor: move;
text-align: center;
line-height: 30px;
color: #fff;
font-size: 12px;
}
.tips {
text-align: center;
margin-top: 10px;
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div class="slider-container">
<div class="slider-bg" id="sliderBg">
<div class="slider-gap" id="sliderGap"></div>
</div>
<div class="slider-bar" id="sliderBar">
<div class="slider-btn" id="sliderBtn">→</div>
</div>
<div class="tips" id="tips">请拖动滑块完成验证</div>
</div>
<script>
// 核心变量
const sliderBtn = document.getElementById('sliderBtn');
const sliderBar = document.getElementById('sliderBar');
const sliderGap = document.getElementById('sliderGap');
const sliderBg = document.getElementById('sliderBg');
const tips = document.getElementById('tips');
let startX = 0; // 滑动起始X坐标
let isDragging = false; // 是否正在滑动
let track = []; // 滑动轨迹:[{time: 时间戳, x: 坐标, y: 坐标}]
let verifyId = ''; // 本次验证的唯一ID(后端生成)
let targetOffset = 0; // 目标缺口偏移量(后端返回)
// 1. 初始化:从后端获取验证图和缺口参数
async function initVerify() {
try {
const res = await fetch('/api/slider/init');
const data = await res.json();
if (data.code === 200) {
verifyId = data.data.verifyId;
targetOffset = data.data.offset; // 目标偏移量(像素)
// 设置背景图
sliderBg.style.background = `url(${data.data.bgImg}) no-repeat center/contain`;
// 设置缺口位置
sliderGap.style.left = `${targetOffset}px`;
sliderGap.style.top = `${data.data.top}px`;
sliderGap.style.display = 'block';
tips.textContent = '请拖动滑块完成验证';
tips.style.color = '#666';
}
} catch (e) {
tips.textContent = '初始化失败,请刷新重试';
tips.style.color = '#f56c6c';
console.error('初始化失败:', e);
}
}
// 2. 采集滑动轨迹(每10ms记录一次坐标和时间)
function recordTrack(x, y) {
track.push({
time: Date.now(),
x: x,
y: y
});
}
// 3. 滑动结束:提交轨迹到后端验证
async function submitVerify() {
if (track.length < 5) { // 轨迹过短,判定为机器
tips.textContent = '验证失败:滑动轨迹异常';
tips.style.color = '#f56c6c';
resetSlider();
return;
}
try {
const res = await fetch('/api/slider/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
verifyId: verifyId,
track: track, // 滑动轨迹
finalOffset: sliderBtn.offsetLeft // 最终滑块偏移量
})
});
const data = await res.json();
if (data.code === 200) {
// 验证通过:设置token到Cookie(供Nginx校验)
document.cookie = `slider_token=${data.data.token}; max-age=300; path=/`;
tips.textContent = '验证通过!即将跳转...';
tips.style.color = '#67c23a';
// 跳转到业务页面(或通知父页面)
setTimeout(() => {
window.location.href = '/'; // 业务首页
}, 1000);
} else {
tips.textContent = `验证失败:${data.msg}`;
tips.style.color = '#f56c6c';
resetSlider();
// 重新初始化验证
setTimeout(initVerify, 1000);
}
} catch (e) {
tips.textContent = '验证请求失败,请重试';
tips.style.color = '#f56c6c';
resetSlider();
console.error('验证提交失败:', e);
}
}
// 4. 重置滑块和轨迹
function resetSlider() {
sliderBtn.style.left = '0px';
isDragging = false;
track = [];
}
// 5. 绑定滑动事件
// 鼠标按下/触摸开始
sliderBtn.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX - sliderBtn.offsetLeft;
// 开始记录轨迹
recordTrack(sliderBtn.offsetLeft, e.clientY);
// 每10ms持续记录轨迹
trackTimer = setInterval(() => {
if (isDragging) {
recordTrack(sliderBtn.offsetLeft, e.clientY);
}
}, 10);
});
// 触摸适配(移动端)
sliderBtn.addEventListener('touchstart', (e) => {
isDragging = true;
startX = e.touches[0].clientX - sliderBtn.offsetLeft;
recordTrack(sliderBtn.offsetLeft, e.touches[0].clientY);
trackTimer = setInterval(() => {
if (isDragging) {
recordTrack(sliderBtn.offsetLeft, e.touches[0].clientY);
}
}, 10);
});
// 鼠标移动/触摸移动
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const newX = e.clientX - startX;
// 限制滑块范围:0 ~ 滑块条宽度 - 滑块宽度
const maxX = sliderBar.offsetWidth - sliderBtn.offsetWidth;
const finalX = Math.max(0, Math.min(newX, maxX));
sliderBtn.style.left = `${finalX}px`;
});
document.addEventListener('touchmove', (e) => {
if (!isDragging) return;
const newX = e.touches[0].clientX - startX;
const maxX = sliderBar.offsetWidth - sliderBtn.offsetWidth;
const finalX = Math.max(0, Math.min(newX, maxX));
sliderBtn.style.left = `${finalX}px`;
});
// 鼠标松开/触摸结束
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
clearInterval(trackTimer);
submitVerify(); // 提交验证
});
document.addEventListener('touchend', () => {
if (!isDragging) return;
isDragging = false;
clearInterval(trackTimer);
submitVerify();
});
// 页面加载时初始化验证
window.onload = initVerify;
</script>
</body>
</html>
步骤3:后端实现(验证图生成+轨迹校验)
新建slider_server.py,实现3个核心接口:初始化验证(生成背景图/缺口)、校验轨迹、验证token(供Nginx调用):
python
import os
import uuid
import random
import time
import json
from PIL import Image, ImageDraw
from flask import Flask, jsonify, request, send_file, make_response
import redis
import hashlib
app = Flask(__name__)
# Redis配置(存储验证参数和token,过期时间5分钟)
redis_client = redis.Redis(
host='127.0.0.1',
port=6379,
db=0,
decode_responses=True,
password='' # 如有密码请填写
)
# 配置:验证图存储路径(临时)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
IMG_DIR = os.path.join(BASE_DIR, 'slider_imgs')
if not os.path.exists(IMG_DIR):
os.makedirs(IMG_DIR)
# -------------------------- 核心工具函数 --------------------------
# 1. 生成随机验证图(带缺口)
def generate_slider_img():
# 生成背景图(随机颜色+随机线条,模拟真实图片)
width, height = 300, 100 # 背景图尺寸
bg_img = Image.new('RGB', (width, height), (random.randint(230, 255), random.randint(230, 255), random.randint(230, 255)))
draw = ImageDraw.Draw(bg_img)
# 画随机线条(增加干扰)
for _ in range(5):
x1 = random.randint(0, width)
y1 = random.randint(0, height)
x2 = random.randint(0, width)
y2 = random.randint(0, height)
draw.line((x1, y1, x2, y2), fill=(random.randint(100, 200), random.randint(100, 200), random.randint(100, 200)), width=2)
# 生成缺口(随机位置:X轴 80~220,Y轴 30~70)
gap_width, gap_height = 40, 40
gap_x = random.randint(80, 220) # 缺口X偏移(目标偏移量)
gap_y = random.randint(30, 70) # 缺口Y偏移
# 画缺口(白色矩形,模拟缺失)
draw.rectangle((gap_x, gap_y, gap_x+gap_width, gap_y+gap_height), fill=(255, 255, 255))
# 保存背景图
img_name = f'{uuid.uuid4()}.png'
img_path = os.path.join(IMG_DIR, img_name)
bg_img.save(img_path)
return {
'img_path': img_path,
'img_name': img_name,
'offset': gap_x, # 缺口X偏移量(目标值)
'top': gap_y # 缺口Y偏移量
}
# 2. 校验滑动轨迹(核心:区分真人/机器)
def check_track(track, final_offset, target_offset):
"""
:param track: 滑动轨迹列表 [{time, x, y}]
:param final_offset: 用户最终滑动的偏移量
:param target_offset: 目标缺口偏移量
:return: (是否通过, 失败原因)
"""
# 1. 偏移量校验:误差±5像素内
offset_error = abs(final_offset - target_offset)
if offset_error > 5:
return False, f'偏移量错误(目标{target_offset},实际{final_offset})'
# 2. 轨迹长度校验:至少5个点(避免瞬间滑动)
if len(track) < 5:
return False, '轨迹过短'
# 3. 滑动时长校验:0.5~3秒(真人滑动不会太快/太慢)
total_time = track[-1]['time'] - track[0]['time']
total_time_s = total_time / 1000
if total_time_s < 0.5 or total_time_s > 3:
return False, f'滑动时长异常({total_time_s:.2f}秒)'
# 4. 速度波动校验:真人速度有波动,机器多匀速
speeds = []
for i in range(1, len(track)):
time_diff = track[i]['time'] - track[i-1]['time']
x_diff = track[i]['x'] - track[i-1]['x']
if time_diff == 0:
continue
speed = x_diff / time_diff # 像素/毫秒
speeds.append(speed)
# 计算速度标准差(波动值):<0.01 判定为匀速(机器)
if len(speeds) < 3:
return False, '轨迹点数不足'
avg_speed = sum(speeds) / len(speeds)
std_speed = (sum([(s - avg_speed)**2 for s in speeds]) / len(speeds)) ** 0.5
if std_speed < 0.01:
return False, '滑动速度匀速(疑似机器)'
# 5. 轨迹Y轴校验:真人滑动Y轴有轻微波动,机器Y轴固定
y_values = [p['y'] for p in track]
y_max = max(y_values)
y_min = min(y_values)
if y_max - y_min < 2:
return False, 'Y轴无波动(疑似机器)'
return True, '验证通过'
# 3. 生成验证token(供Nginx校验)
def generate_token(verify_id):
token = hashlib.md5(f'{verify_id}_{int(time.time())}_slider_secret'.encode()).hexdigest()
# 存储token到Redis,过期5分钟
redis_client.setex(f'slider_token:{token}', 300, 'valid')
return token
# -------------------------- 接口实现 --------------------------
# 1. 初始化验证接口(生成背景图+缺口参数)
@app.route('/api/slider/init', methods=['GET'])
def init_slider():
try:
# 生成验证图
img_info = generate_slider_img()
# 生成唯一验证ID
verify_id = str(uuid.uuid4())
# 存储验证参数到Redis(过期5分钟)
redis_client.setex(
f'slider_verify:{verify_id}',
300,
json.dumps({
'offset': img_info['offset'],
'top': img_info['top'],
'img_name': img_info['img_name']
})
)
# 返回结果(图片路径为访问路径)
return jsonify({
'code': 200,
'msg': '初始化成功',
'data': {
'verifyId': verify_id,
'offset': img_info['offset'],
'top': img_info['top'],
'bgImg': f'/api/slider/img/{img_info["img_name"]}' # 图片访问接口
}
})
except Exception as e:
return jsonify({'code': 500, 'msg': f'初始化失败:{str(e)}'}), 500
# 2. 验证图片访问接口
@app.route('/api/slider/img/<img_name>', methods=['GET'])
def get_slider_img(img_name):
img_path = os.path.join(IMG_DIR, img_name)
if not os.path.exists(img_path):
return jsonify({'code': 404, 'msg': '图片不存在'}), 404
# 返回图片,并设置缓存(短期)
response = make_response(send_file(img_path, mimetype='image/png'))
response.headers['Cache-Control'] = 'max-age=300'
return response
# 3. 滑块验证接口(校验轨迹)
@app.route('/api/slider/verify', methods=['POST'])
def verify_slider():
try:
data = request.get_json()
verify_id = data.get('verifyId')
track = data.get('track', [])
final_offset = data.get('finalOffset', 0)
# 1. 校验参数
if not verify_id or not track or final_offset is None:
return jsonify({'code': 400, 'msg': '参数缺失'}), 400
# 2. 获取Redis中的验证参数
verify_info_str = redis_client.get(f'slider_verify:{verify_id}')
if not verify_info_str:
return jsonify({'code': 400, 'msg': '验证已过期,请刷新'}), 400
verify_info = json.loads(verify_info_str)
target_offset = verify_info['offset']
# 3. 校验轨迹
is_pass, msg = check_track(track, final_offset, target_offset)
if not is_pass:
return jsonify({'code': 403, 'msg': msg}), 403
# 4. 验证通过:生成token,删除验证参数(防止复用)
token = generate_token(verify_id)
redis_client.delete(f'slider_verify:{verify_id}')
return jsonify({
'code': 200,
'msg': '验证通过',
'data': {'token': token}
})
except Exception as e:
return jsonify({'code': 500, 'msg': f'验证失败:{str(e)}'}), 500
# 4. Nginx校验token接口(内部调用)
@app.route('/api/slider/check_token', methods=['GET'])
def check_token():
# 从Cookie获取token
token = request.cookies.get('slider_token')
if not token:
return '', 401 # 无token,验证失败
# 校验token是否有效
is_valid = redis_client.get(f'slider_token:{token}')
if is_valid == 'valid':
# 验证通过,删除token(防止复用)
redis_client.delete(f'slider_token:{token}')
return '', 200
else:
return '', 401 # token无效
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001, debug=False)
步骤4:Nginx配置(拦截请求+校验token)
修改nginx.conf,实现"拦截业务请求→校验滑块token→放行/重定向":
nginx
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 后端滑块验证服务
upstream slider_server {
server 127.0.0.1:5001;
}
server {
listen 80;
server_name localhost;
# 业务根路径(需要验证的路径)
location / {
# 1. 校验滑块token(内部子请求)
auth_request /api/slider/check_token;
# 2. token无效/缺失 → 重定向到滑块验证页面
error_page 401 = @redirect_slider;
# 3. token有效 → 放行到业务服务(替换为你的实际业务地址)
proxy_pass http://127.0.0.1:8080; # 你的业务服务地址
}
# 重定向到滑块验证页面
location @redirect_slider {
rewrite ^/(.*)$ /slider.html permanent;
}
# 滑块验证相关接口 → 转发到后端服务
location /api/slider/ {
proxy_pass http://slider_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 滑块验证页面(静态文件)
location /slider.html {
root /path/to/your/static/files; # 替换为slider.html所在目录
expires 0; # 禁止缓存
}
# Nginx内部调用的token校验接口(禁止外部访问)
location = /api/slider/check_token {
internal;
proxy_pass http://slider_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
步骤5:启动与测试
- 启动Redis :
redis-server(确保端口6379); - 启动后端服务 :
python slider_server.py; - 放置前端文件 :将
slider.html放到Nginx配置的静态文件目录; - 重启Nginx :
nginx -s reload; - 测试 :访问
http://localhost→ 自动重定向到滑块验证页面 → 拖动滑块完成验证 → 验证通过后跳转到业务页面。
三、防破解优化(关键!避免被机器破解)
1. 前端优化
- 轨迹加密:将轨迹数据用AES加密后传输(避免抓包篡改);
- 混淆JS:对滑动事件的JS代码进行混淆(防止逆向分析);
- 禁用模拟 :检测是否为模拟器/自动化工具(如检测
webdriver标识); - 动态样式:滑块样式随机变化(颜色、大小、形状),避免固定模板。
2. 后端优化
- 动态阈值:根据IP/设备调整校验阈值(如高频验证的IP提高校验严格度);
- 防重放:验证ID仅能使用一次,校验后立即删除;
- 图片增强:验证图加入随机水印、扭曲、噪点(防止图像识别破解);
- 风控结合:结合IP黑名单、设备指纹(如浏览器指纹)、访问频率限制。
3. 部署优化
-
频率限制 :Nginx配置
limit_req_module限制验证接口请求频率(如每秒1次);nginxlimit_req_zone $binary_remote_addr zone=slider:10m rate=1r/s; location /api/slider/ { limit_req zone=slider burst=2 nodelay; proxy_pass http://slider_server; } -
分布式部署:Redis使用集群,支持多实例后端服务;
-
日志监控:记录验证失败日志,分析异常IP/设备,及时调整策略。
四、扩展适配
1. 移动端适配
- 前端已兼容
touch事件,只需调整样式适配移动端屏幕; - 优化滑块大小(如宽度280px,适配手机屏幕)。
2. 集成第三方滑块(简化开发)
如果不想自研,可直接集成成熟的第三方滑块:
- 极验(GEETEST):https://www.geetest.com/ (文档完善,支持私有化部署);
- 顶象:https://www.dingxiang-inc.com/ (风控能力强,适合高安全场景);
- 集成方式:替换前端滑块代码为第三方SDK,后端调用第三方校验接口即可。
五、常见问题排查
- 验证图无法显示 :检查
IMG_DIR路径是否正确,Nginx是否有权限访问图片目录; - 轨迹校验失败 :调整
check_track函数的阈值(如速度标准差、滑动时长); - Redis连接失败:检查Redis地址、端口、密码是否正确,确保Redis服务运行;
- Nginx重定向循环 :确保
/api/slider/接口不被auth_request拦截(Nginx配置中排除)。
总结
滑块验证的核心是轨迹特征校验(区分真人/机器),而非单纯的偏移量匹配。自研方案适合中小场景,若追求更高安全性/更低开发成本,建议集成极验、顶象等第三方滑块服务。实际部署时,需结合风控策略(IP、设备、频率),构建多层防御体系,平衡安全与用户体验。