简化版滑块验证(仅X轴滑动+美化UI)
按你的需求优化:无需缺口匹配,只需从左滑到最右侧,通过滑动轨迹(速度、时长、波动)判断人机,同时大幅美化UI样式(渐变背景、圆角设计、滑动动画)。核心逻辑(轨迹校验、Nginx拦截、Redis存储)保持不变。
一、核心优化点
- 操作简化:去掉缺口,滑块从左滑到最右侧即可完成验证,用户无需对准缺口;
- UI美化 :
- 渐变背景+磨砂玻璃效果;
- 滑块带阴影+图标,滑动时有动画反馈;
- 验证成功/失败有明确的颜色和图标提示;
- 交互优化:滑动过程中显示进度,松手后有结果动画。
二、完整实现步骤
步骤1:环境准备(同之前)
-
前端:原生HTML+JS(美化样式);
-
后端:Python 3.8+ + Flask + Pillow + Redis;
-
依赖安装(同之前):
bashpip install flask pillow redis requests
步骤2:前端实现(简化操作+美化UI)
新建simple_slider.html,实现纯X轴滑动、美化样式、轨迹采集:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<!-- 移动端核心适配:视口设置(关键) -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no, email=no">
<title>简易滑块验证</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', sans-serif;
/* 移动端触摸高亮清除 */
-webkit-tap-highlight-color: transparent;
tap-highlight-color: transparent;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 15px; /* 移动端增加内边距,避免贴边 */
/* 移动端滚动优化 */
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}
/* 滑块容器:自适应宽度+移动端适配 */
.slider-container {
width: 100%;
max-width: 380px; /* 最大宽度限制 */
min-width: 280px; /* 最小宽度,适配小屏手机 */
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px 15px; /* 移动端减少内边距 */
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px); /* 兼容iOS */
border: 1px solid rgba(255, 255, 255, 0.18);
}
.slider-title {
font-size: 17px; /* 移动端字体适配 */
color: #333;
margin-bottom: 18px;
text-align: center;
font-weight: 600;
}
/* 修正:添加id="sliderWrap",并优化移动端滑动区域 */
.slider-wrap {
width: 100%;
height: 55px; /* 移动端增加高度,方便手指操作 */
background: linear-gradient(90deg, #f0f2f5 0%, #e8f4f8 100%);
border-radius: 28px; /* 对应高度调整圆角 */
position: relative;
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
/* 移动端防止触摸选中 */
user-select: none;
-webkit-user-select: none;
}
/* 滑块按钮:适配手指操作,增大点击区域 */
.slider-btn {
width: 55px; /* 增大宽度,适配手指 */
height: 55px; /* 增大高度 */
background: linear-gradient(135deg, #4299e1 0%, #38b2ac 100%);
border-radius: 50%;
position: absolute;
top: 0;
left: 0;
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 22px; /* 增大图标 */
transition: all 0.2s ease;
z-index: 2;
/* 移动端触摸优化 */
touch-action: pan-x; /* 只允许水平滑动 */
-webkit-touch-callout: none;
}
.slider-btn:hover {
box-shadow: 0 6px 16px rgba(66, 153, 225, 0.4);
transform: scale(1.03); /* 移动端缩小缩放,避免超出容器 */
}
.slider-btn:active {
transform: scale(0.97);
}
/* 滑动进度条 */
.slider-progress {
height: 100%;
background: linear-gradient(90deg, #4299e1 0%, #38b2ac 100%);
width: 0;
transition: width 0.15s linear; /* 移动端加快过渡,更跟手 */
z-index: 1;
}
/* 提示文字:移动端字体适配 */
.slider-tips {
text-align: center;
margin-top: 12px;
font-size: 14px;
color: #666;
transition: color 0.3s ease;
line-height: 1.4; /* 移动端增加行高 */
}
.tips-success {
color: #48bb78;
font-weight: 500;
}
.tips-error {
color: #f56565;
}
/* 加载动画:适配移动端大小 */
.loading {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 6px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 小屏手机适配(宽度<320px) */
@media (max-width: 320px) {
.slider-container {
padding: 15px 10px;
}
.slider-title {
font-size: 16px;
margin-bottom: 15px;
}
.slider-wrap {
height: 50px;
}
.slider-btn {
width: 50px;
height: 50px;
font-size: 20px;
}
.slider-tips {
font-size: 13px;
}
}
/* 大屏手机适配(宽度>400px) */
@media (min-width: 400px) {
.slider-container {
padding: 25px 20px;
}
.slider-title {
font-size: 18px;
}
}
</style>
</head>
<body>
<div class="slider-container">
<h3 class="slider-title">拖动滑块完成验证</h3>
<!-- 核心修正:添加id="sliderWrap" -->
<div class="slider-wrap" id="sliderWrap">
<div class="slider-progress" id="sliderProgress"></div>
<div class="slider-btn" id="sliderBtn">→</div>
</div>
<div class="slider-tips" id="sliderTips">请按住滑块,从左滑到右</div>
</div>
<script>
// 核心变量
const sliderBtn = document.getElementById('sliderBtn');
const sliderWrap = document.getElementById('sliderWrap');
const sliderProgress = document.getElementById('sliderProgress');
const sliderTips = document.getElementById('sliderTips');
let isDragging = false;
let startX = 0;
let track = [];
let verifyId = '';
let maxOffset = 0;
let trackTimer = null;
// 初始化最大偏移量(适配窗口变化)
function initMaxOffset() {
maxOffset = sliderWrap.offsetWidth - sliderBtn.offsetWidth;
}
// 1. 初始化验证
async function initVerify() {
try {
// 移动端优化:请求添加超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
const res = await fetch('/api/slider/simple/init', {
signal: controller.signal,
// 移动端缓存优化
cache: 'no-cache',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
clearTimeout(timeoutId);
const data = await res.json();
if (data.code === 200) {
verifyId = data.data.verifyId;
resetSlider();
}
} catch (e) {
if (e.name !== 'AbortError') {
sliderTips.textContent = '网络异常,请检查后重试';
sliderTips.className = 'slider-tips tips-error';
} else {
sliderTips.textContent = '请求超时,请重试';
sliderTips.className = 'slider-tips tips-error';
}
console.error('初始化失败:', e);
}
}
// 2. 采集滑动轨迹
function recordTrack(x, y) {
track.push({
time: Date.now(),
x: x,
y: y
});
}
// 3. 提交验证
async function submitVerify() {
const finalOffset = sliderBtn.offsetLeft;
// 移动端容错:允许稍小的偏移(因为手指操作精度低)
if (finalOffset < maxOffset - 5) {
sliderTips.textContent = '请滑动到最右侧';
sliderTips.className = 'slider-tips tips-error';
resetSlider();
return;
}
if (track.length < 3) { // 移动端降低轨迹长度要求(手指滑动更快)
sliderTips.textContent = '滑动轨迹异常,请重试';
sliderTips.className = 'slider-tips tips-error';
resetSlider();
return;
}
sliderTips.innerHTML = '<span class="loading"></span>验证中...';
sliderTips.className = 'slider-tips';
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const res = await fetch('/api/slider/simple/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
verifyId: verifyId,
track: track,
finalOffset: finalOffset,
maxOffset: maxOffset
}),
signal: controller.signal,
cache: 'no-cache'
});
clearTimeout(timeoutId);
const data = await res.json();
if (data.code === 200) {
document.cookie = `slider_token=${data.data.token}; max-age=300; path=/; secure; SameSite=Lax`; // 移动端Cookie安全配置
sliderTips.innerHTML = '✅ 验证通过!即将跳转...';
sliderTips.className = 'slider-tips tips-success';
sliderBtn.innerHTML = '✓';
sliderBtn.style.background = 'linear-gradient(135deg, #48bb78 0%, #38a169 100%)';
setTimeout(() => {
window.location.href = '/';
}, 1500);
} else {
sliderTips.innerHTML = `❌ ${data.msg}`;
sliderTips.className = 'slider-tips tips-error';
resetSlider();
setTimeout(initVerify, 1500);
}
} catch (e) {
if (e.name !== 'AbortError') {
sliderTips.textContent = '验证失败,请重试';
} else {
sliderTips.textContent = '验证超时,请重试';
}
sliderTips.className = 'slider-tips tips-error';
resetSlider();
console.error('验证失败:', e);
}
}
// 4. 重置滑块
function resetSlider() {
sliderBtn.style.left = '0px';
sliderProgress.style.width = '0%';
sliderBtn.innerHTML = '→';
sliderBtn.style.background = 'linear-gradient(135deg, #4299e1 0%, #38b2ac 100%)';
isDragging = false;
track = [];
sliderTips.textContent = '请按住滑块,从左滑到右';
sliderTips.className = 'slider-tips';
// 清除定时器,防止内存泄漏
if (trackTimer) clearInterval(trackTimer);
}
// 5. 绑定事件(优化移动端触摸)
function bindEvents() {
// 鼠标按下/触摸开始
const startHandler = (e) => {
e.preventDefault(); // 阻止默认行为(如页面滚动)
isDragging = true;
// 区分鼠标和触摸事件
let clientX = e.clientX;
let clientY = e.clientY;
if (e.type === 'touchstart') {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
startX = clientX - sliderBtn.offsetLeft;
recordTrack(sliderBtn.offsetLeft, clientY);
trackTimer = setInterval(() => {
if (isDragging) {
// 触摸时实时获取坐标(防止手指移动后坐标不变)
let x = sliderBtn.offsetLeft;
let y = clientY;
if (e.type === 'touchstart' && isDragging) {
const touch = document.querySelector('body').ontouchmove ? event.touches[0] : null;
if (touch) y = touch.clientY;
}
recordTrack(x, y);
}
}, 10);
};
// 鼠标/触摸移动
const moveHandler = (e) => {
if (!isDragging) return;
e.preventDefault(); // 阻止页面滚动
let clientX = e.clientX;
if (e.type === 'touchmove') {
clientX = e.touches[0].clientX;
}
let newX = clientX - startX;
newX = Math.max(0, Math.min(newX, maxOffset));
sliderBtn.style.left = `${newX}px`;
sliderProgress.style.width = `${(newX / maxOffset) * 100}%`;
};
// 鼠标/触摸结束
const endHandler = () => {
if (!isDragging) return;
isDragging = false;
clearInterval(trackTimer);
submitVerify();
};
// 绑定事件(移动端优先)
// 触摸事件:添加passive优化性能
sliderBtn.addEventListener('touchstart', startHandler, { passive: false });
document.addEventListener('touchmove', moveHandler, { passive: false });
document.addEventListener('touchend', endHandler);
document.addEventListener('touchcancel', endHandler); // 处理触摸中断(如来电)
// 鼠标事件
sliderBtn.addEventListener('mousedown', startHandler);
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', endHandler);
document.addEventListener('mouseleave', endHandler); // 鼠标离开窗口时重置
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
initMaxOffset();
initVerify();
bindEvents();
// 窗口大小变化时重新计算偏移量
window.addEventListener('resize', () => {
initMaxOffset();
resetSlider();
});
// 移动端返回键处理
window.addEventListener('popstate', () => {
resetSlider();
});
});
// 防止移动端页面卸载时的定时器泄漏
window.addEventListener('beforeunload', () => {
if (trackTimer) clearInterval(trackTimer);
});
</script>
</body>
</html>
步骤3:后端实现(简化逻辑+保持轨迹校验)
新建simple_slider_server.py,去掉缺口相关逻辑,保留核心轨迹校验:
python
import os
import uuid
import random
import time
import json
from PIL import Image, ImageDraw
from flask import Flask, jsonify, request, make_response
import redis
import hashlib
app = Flask(__name__)
# Redis配置
redis_client = redis.Redis(
host='127.0.0.1',
port=6379,
db=0,
decode_responses=True,
password='' # 如有密码请填写
)
# -------------------------- 核心工具函数 --------------------------
# 1. 校验滑动轨迹(核心:真人vs机器)
def check_track(track, final_offset, max_offset):
"""
:param track: 滑动轨迹列表 [{time, x, y}]
:param final_offset: 用户最终滑动偏移量
:param max_offset: 最大偏移量(滑到最右侧的值)
:return: (是否通过, 失败原因)
"""
# 1. 校验是否滑到最右侧(误差±3px)
if final_offset < max_offset - 3:
return False, '未滑动到最右侧'
# 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)
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_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轴无波动(疑似机器)'
# 6. 滑动加速度校验:真人滑动先加速后减速,机器加速度固定
accelerations = []
for i in range(1, len(speeds)):
acc = speeds[i] - speeds[i-1]
accelerations.append(acc)
if len(accelerations) > 0:
acc_std = (sum([a**2 for a in accelerations]) / len(accelerations)) ** 0.5
if acc_std < 0.005:
return False, '加速度无变化(疑似机器)'
return True, '验证通过'
# 2. 生成验证token(供Nginx校验)
def generate_token(verify_id):
token = hashlib.md5(f'{verify_id}_{int(time.time())}_simple_slider_secret'.encode()).hexdigest()
redis_client.setex(f'slider_token:{token}', 300, 'valid')
return token
# -------------------------- 接口实现 --------------------------
# 1. 初始化验证接口(仅生成唯一ID,无需图片)
@app.route('/api/slider/simple/init', methods=['GET'])
def init_simple_slider():
try:
verify_id = str(uuid.uuid4())
# 存储验证ID到Redis(过期5分钟,用于防重放)
redis_client.setex(f'slider_verify:{verify_id}', 300, 'valid')
return jsonify({
'code': 200,
'msg': '初始化成功',
'data': {'verifyId': verify_id}
})
except Exception as e:
return jsonify({'code': 500, 'msg': f'初始化失败:{str(e)}'}), 500
# 2. 滑块验证接口(校验轨迹)
@app.route('/api/slider/simple/verify', methods=['POST'])
def verify_simple_slider():
try:
data = request.get_json()
verify_id = data.get('verifyId')
track = data.get('track', [])
final_offset = data.get('finalOffset', 0)
max_offset = data.get('maxOffset', 0)
# 1. 校验参数
if not verify_id or not track or final_offset is None or max_offset == 0:
return jsonify({'code': 400, 'msg': '参数缺失'}), 400
# 2. 校验验证ID是否有效(防重放)
if not redis_client.get(f'slider_verify:{verify_id}'):
return jsonify({'code': 400, 'msg': '验证已过期,请重试'}), 400
# 3. 校验轨迹
is_pass, msg = check_track(track, final_offset, max_offset)
if not is_pass:
return jsonify({'code': 403, 'msg': msg}), 403
# 4. 验证通过:生成token,删除验证ID(防止复用)
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
# 3. Nginx校验token接口(同之前,可复用)
@app.route('/api/slider/check_token', methods=['GET'])
def check_token():
token = request.cookies.get('slider_token')
if not token:
return '', 401
is_valid = redis_client.get(f'slider_token:{token}')
if is_valid == 'valid':
redis_client.delete(f'slider_token:{token}')
return '', 200
else:
return '', 401
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001, debug=False)
步骤4:Nginx配置(仅修改静态文件路径)
修改nginx.conf,只需更新滑块页面的路径,其他逻辑不变:
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 / {
auth_request /api/slider/check_token;
error_page 401 = @redirect_slider;
proxy_pass http://127.0.0.1:8080; # 你的业务服务地址
}
# 重定向到简化版滑块页面
location @redirect_slider {
rewrite ^/(.*)$ /simple_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 /simple_slider.html {
root /path/to/your/static/files; # 替换为simple_slider.html所在目录
expires 0;
}
# 内部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; - 启动后端服务:
python simple_slider_server.py; - 放置前端文件:将
simple_slider.html放到Nginx静态目录; - 重启Nginx:
nginx -s reload; - 测试:访问
http://localhost→ 重定向到美化后的滑块页面 → 从左滑到右 → 验证通过后跳转。
三、核心优化说明
1. 操作简化
- 去掉缺口匹配,用户只需"从左滑到右",降低操作成本;
- 滑动过程中显示进度条,用户明确知道滑动目标。
2. UI美化
- 渐变背景+磨砂玻璃卡片,视觉更高级;
- 滑块带阴影和图标,滑动时有缩放/颜色变化反馈;
- 验证结果用图标+颜色区分,用户体验更清晰;
- 响应式设计,适配手机和电脑。
3. 安全性保持
- 保留核心轨迹校验规则(时长、速度波动、Y轴波动、加速度);
- 验证ID防重放、token时效性控制;
- 支持分布式部署(Redis存储)。
四、进一步优化建议
- 前端加密:将轨迹数据用AES加密后传输,防止抓包篡改;
- JS混淆:对滑动事件代码进行混淆,防止逆向分析;
- 设备指纹:结合浏览器指纹(如User-Agent、屏幕分辨率)增强校验;
- 频率限制 :Nginx配置
limit_req限制验证接口请求频率,防止暴力破解。
如果需要进一步调整UI风格(如颜色、大小、动画),可以直接修改前端CSS部分,核心逻辑无需改动。