前言
滑块验证的核心逻辑是:前端渲染滑块+缺口背景图,采集用户滑动轨迹;后端校验轨迹是否为真人行为(非机器匀速滑动),验证通过后生成时效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验证); - 依赖安装:
# 后端依赖 pip install flask pillow redis requests # redis(本地/云服务器,用于存储验证参数和token) # 参考安装:https://redis.io/docs/getting-started/installation/
步骤2:前端实现(滑块渲染+轨迹采集)
新建slider.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调用):
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→放行/重定向”:
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次);limit_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、设备、频率),构建多层防御体系,平衡安全与用户体验。
到此这篇关于滑块验证完整实现部署流程的文章就介绍到这了,更多相关前端+后端+nginx集成实现滑块验证内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论