很多业务场景需要轻量化滑块验证码,本文从原理、可直接运行前端示例、后端生成 & 校验逻辑、风控要点、避坑清单完整梳理。重点提醒:纯前端版本仅适合演示,绝对不能直接上线生产。
一、实现原理
拼图拖拽验证码核心流程:
- 后端预先随机在背景图上生成缺口,抠取拼图碎片,记录缺口横坐标,下发背景图、拼图、验证码唯一 ID、正确偏移值;
- 前端监听鼠标 / 触摸拖拽动作,实时同步滑块与拼图位置,同步采集拖动轨迹(坐标、时间戳);
- 用户释放鼠标 / 手指后,将验证码 ID、拖动偏移距离、轨迹数据提交后端;
- 后端多层校验:验证码是否有效未过期、偏移量是否在误差区间、拖动行为是否符合真人特征;
- 校验通过后销毁当前验证码凭证,下发业务临时凭证,失败则提示重新滑动或刷新验证码。
安全核心:判断逻辑必须放在后端,前端只负责交互与数据采集。
二、前端交互 Demo(PC + 移动端触摸兼容)
下面是原生 HTML + JavaScript 可直接打开运行的示例,仅用于交互演示,校验逻辑写在了前端,生产环境要把校验移到后端。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.captcha-wrap { width: 300px; margin: 30px auto; }
.captcha-img-box {
position: relative;
width: 300px;
height: 180px;
background: #eee;
overflow: hidden;
border-radius: 6px;
}
.bg-img { width:100%; height:100%; object-fit:cover; }
/* 缺口占位阴影 */
.gap {
position: absolute;
width: 40px;
height:40px;
background: rgba(0,0,0,0.4);
border:1px dashed #fff;
}
/* 可拖动拼图 */
.puzzle {
position: absolute;
width: 40px;
height:40px;
background: #fff;
top:0;
left:0;
cursor: grab;
box-shadow: 0 0 6px #0004;
}
.slider-track {
margin-top:12px;
height:34px;
background:#e8e8e8;
border-radius:17px;
position:relative;
}
.slider-btn {
width:40px;
height:34px;
background:#409eff;
border-radius:17px;
position:absolute;
left:0;
top:0;
color:#fff;
text-align:center;
line-height:34px;
cursor:grab;
user-select:none;
}
.tip { margin-top:8px; font-size:14px; }
</style>
</head>
<body>
<div class="captcha-wrap">
<div class="captcha-img-box">
<img class="bg-img" src="https://picsum.photos/300/180" alt="">
<div class="gap" id="gap"></div>
<div class="puzzle" id="puzzle"></div>
</div>
<div class="slider-track">
<div class="slider-btn" id="sliderBtn">▶</div>
</div>
<div class="tip" id="tip">拖动滑块完成验证</div>
</div>
<script>
const boxW = 300;
const blockW = 40;
// 模拟后端下发的缺口位置,生产由后端随机生成
const targetX = 120;
const gapEl = document.getElementById('gap');
const puzzleEl = document.getElementById('puzzle');
const sliderBtn = document.getElementById('sliderBtn');
const tipEl = document.getElementById('tip');
const track = sliderBtn.parentElement;
gapEl.style.left = targetX + 'px';
puzzleEl.style.top = gapEl.offsetTop + 'px';
let isDrag = false;
let startX = 0;
let trackList = []; // 轨迹:{x, y, t}
// PC鼠标事件
sliderBtn.addEventListener('mousedown', startDrag)
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', endDrag)
// 移动端触摸事件
sliderBtn.addEventListener('touchstart', startDrag)
document.addEventListener('touchmove', onDrag)
document.addEventListener('touchend', endDrag)
function startDrag(e){
isDrag = true;
const point = e.touches ? e.touches[0] : e;
startX = point.clientX;
trackList = [];
sliderBtn.style.cursor = 'grabbing';
}
function onDrag(e){
if(!isDrag) return;
e.preventDefault();
const point = e.touches ? e.touches[0] : e;
let dx = point.clientX - startX;
// 边界限制,不能拖出轨道
dx = Math.max(0, Math.min(dx, boxW - blockW));
sliderBtn.style.left = dx + 'px';
puzzleEl.style.left = dx + 'px';
// 持续采集轨迹
trackList.push({
x: dx,
y: point.clientY,
t: Date.now()
})
}
function endDrag(){
if(!isDrag) return;
isDrag = false;
sliderBtn.style.cursor = 'grab';
const currentX = parseInt(sliderBtn.style.left || 0);
// 误差范围 ±5px
const tolerance = 5;
if(Math.abs(currentX - targetX) <= tolerance){
tipEl.innerText = '✅ 验证成功';
tipEl.style.color = '#07c160';
console.log('轨迹上报', trackList);
// 生产环境:请求后端校验接口
// fetch('/api/captcha/verify', { captchaId, offset:currentX, track:trackList })
}else{
tipEl.innerText = '❌ 验证失败,重新滑动';
tipEl.style.color = '#f56c6c';
// 重置滑块
sliderBtn.style.left = '0px';
puzzleEl.style.left = '0px';
}
}
</script>
</body>
</html>
三、生产环境完整后端架构
3.1 接口时序
- 获取验证码:GET /api/captcha/create 后端随机选取图片,使用图像处理库抠出拼图缺口,生成唯一
captchaId,存入 Redis 缓存:captchaId => targetX,设置过期时间(推荐 120s)。 返回数据:背景图 base64、拼图块 base64、captchaId。 - 提交校验:POST /api/captcha/verify 请求入参:
captchaId、offset(拖动距离)、track(轨迹数组)。 - 后端三层校验规则
- 有效性校验:Redis 中是否存在该 captchaId,已过期直接返回失败;
- 位置校验:偏移量和目标缺口距离在误差阈值内,一般 4~6px;
- 行为风控(防脚本):
- 总拖动时长不能过短,小于 200ms 直接拦截;
- Y 轴坐标需要有微小抖动,完全水平直线是典型脚本特征;
- 速度、加速度不能是完美匀速;
校验通过后,立刻删除 Redis 里的 captchaId(一次性凭证,防重放),返回成功标识 + 业务 token;校验失败保留 ID,允许有限重试,多次失败后自动失效。
3.2 Python Pillow 抠图简易示例
from PIL import Image
import random
def make_puzzle(img_path):
img = Image.open(img_path)
w, h = img.size
bw, bh = 40, 40
# 随机缺口坐标,预留安全边距
gap_x = random.randint(80, w - bw - 20)
gap_y = random.randint(20, h - bh - 20)
# 裁剪拼图碎片
puzzle = img.crop((gap_x, gap_y, gap_x+bw, gap_y+bh))
# 在原图绘制半透明缺口阴影
mask = Image.new("RGBA", (bw,bh), (0,0,0,100))
img.paste(mask, (gap_x, gap_y), mask)
return img, puzzle, gap_x
四、开源组件与商用方案
不想从零自研,可直接选用成熟轮子:
五、踩坑清单(高频问题)
- ❌ 只在前端做位置判断:F12 调试面板修改 JS 变量就能直接绕过;
- ❌ 不校验拖动轨迹:脚本匀速滑动可以轻松通关;
- ❌ captchaId 不做一次性销毁:请求可以被抓包重放,无限通过验证;
- 移动端坑:touchmove 不加
e.preventDefault(),拖动时会触发页面滚动,体验很差; - 误差阈值不要设置过大,阈值越大越容易被脚本暴力遍历。
六、进阶优化方向
- 轨迹数据 AES 加密上传,防止前端篡改轨迹数组;
- 升级为旋转拼图,不再是横向拖拽对齐;
- 使用 OpenCV 做图片缺口自动识别,脱离固定模板图片;
- 增加失败次数限流,连续多次验证失败直接刷新验证码;
- 接入轻量 CNN 模型,识别 AI 打码请求。
结语
拖拽验证码的难点不在前端拖拽交互,而在后端的风控体系。单纯实现滑动效果非常简单,但是缺少轨迹校验、一次性凭证、过期控制的验证码,在爬虫、AI 打码面前几乎没有防护能力。对安全要求高的业务,优先选择成熟的第三方验证码服务;内部后台、低风险场景,才考虑自研这套方案。
如果你想要扩展,我可以继续补充:
- Node.js + Redis 完整后端接口代码
- Vue3 / React 封装好的滑块组件
- OpenCV 版本自动生成缺口的 Python 服务
正文完
可以使用微信扫码关注公众号(ID:xzluomor)