自行编码实现滑块验证码,意味着你要完全掌控拼图生成、前端交互、轨迹采集、后端验证这一整套流程。这通常适用于对隐私、定制化有极高要求,或不希望引入第三方服务的场景。
以下是一套完整的技术实现流程,分为前端、后端和核心算法三部分。
一、 整体流程概览
- 请求获取:前端请求后端,获取一张带缺口的背景图和对应的完整滑块图,以及一个唯一会话ID。
- 用户滑动:前端渲染图片,用户拖动滑块到缺口位置,前端记录整个拖动轨迹数据。
- 提交验证:前端将轨迹数据(加密后)和会话ID提交给后端。
- 后端校验:后端从轨迹数据还原出用户拖动的最终X轴坐标,并与真实的缺口X坐标进行比对。同时,对轨迹本身进行人机特征分析(速度、停顿、轨迹形状),判断是否为机器脚本。
- 返回结果:验证通过,签发一次性票据;失败则要求重试。
二、 后端实现(核心:图片生成与坐标存储)
后端需要完成:准备底图、随机切割缺口、生成滑块图、存储缺口坐标。
技术选型: 常用Java + OpenCV / Python + PIL / Node.js + Sharp。这里以 Java 伪代码为例,展示逻辑。
1. 准备素材
- 一张背景大图 (如
bg_1.jpg) - 需要在其上挖去一个特定形状的拼图块。
2. 生成缺口与滑块图
java
// 生成验证码的接口
public CaptchaVO createCaptcha() {
// 1. 随机选择一张背景图
BufferedImage bgImage = ImageIO.read(new File("bg_1.jpg"));
// 2. 随机生成缺口位置 (Y轴通常固定,X轴在一定范围内随机)
int x = random(50, bgImage.getWidth() - 100);
int y = random(50, bgImage.getHeight() - 100);
// 3. 定义拼图形状(最常见的是带凹凸边缘的方形)
// 使用Path2D绘制自定义形状:一个矩形+左凸起+右凹陷等
Shape puzzleShape = createPuzzleShape();
// 4. 生成滑块图片 (小图)
// 从原图(x,y)处裁剪出拼图形状的图片,可选增加描边、阴影
BufferedImage sliderImage = cutImageByShape(bgImage, puzzleShape, x, y);
// 5. 生成带缺口的背景图
// 在原图(x,y)处,用半透明灰色填充拼图形状,产生阴影感
BufferedImage bgWithGapImage = drawGapOnImage(bgImage, puzzleShape, x, y);
// 6. 将图片转为Base64返回,并记录坐标到Redis
String sessionId = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("SLIDER_CAP:" + sessionId, x, Duration.ofMinutes(5));
CaptchaVO vo = new CaptchaVO();
vo.setSessionId(sessionId);
vo.setBgImage(base64Encode(bgWithGapImage));
vo.setSliderImage(base64Encode(sliderImage));
// 可选:返回滑块的Y坐标,以及背景图宽度,供前端计算比例
vo.setSliderY(y);
return vo;
}
自定义形状示例 (createPuzzleShape):
- 通常是正方形或圆形,加上随机生成的多边形凹凸,防止简单模板匹配。
三、 前端实现(核心:交互与轨迹采集)
前端需要绘制、处理拖拽事件,并精确记录轨迹。
1. 渲染结构
html
<div id="captcha-container">
<div class="bg-image"><!-- 缺口背景图 --></div>
<div class="slider-track">
<div class="slider-btn" id="sliderBtn">⟫</div>
</div>
<div class="slider-clip" id="sliderClip"><!-- 滑块图,初始隐藏 --></div>
</div>
2. 核心交互逻辑 (JavaScript)
javascript
const bgImage = document.querySelector('.bg-image');
const sliderBtn = document.getElementById('sliderBtn');
const sliderClip = document.getElementById('sliderClip');
let startX, startY; // 鼠标按下时的起始坐标
let track = []; // 轨迹数组:[{x, y, t}],时间戳t相对于开始时间
// 绑定滑块图片,使其初始位置与Y坐标对齐
sliderClip.style.backgroundImage = `url(${sliderImageBase64})`;
sliderClip.style.top = gapY + 'px'; // gapY由后端返回
sliderClip.style.left = '0px';
// 鼠标按下
sliderBtn.addEventListener('mousedown', (e) => {
startX = e.clientX;
startY = e.clientY;
track = [{x: 0, y: 0, t: Date.now()}]; // 起始点(0,0)
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
function onMove(e) {
let moveX = e.clientX - startX;
// 边界限制
if (moveX < 0) moveX = 0;
let maxMove = bgImage.offsetWidth - sliderBtn.offsetWidth;
if (moveX > maxMove) moveX = maxMove;
// 记录轨迹 (相对起点的位移)
track.push({
x: moveX,
y: e.clientY - startY, // Y轴偏移可用来分析是否为真人
t: Date.now()
});
// 移动滑块按钮和滑块拼图
sliderBtn.style.transform = `translateX(${moveX}px)`;
sliderClip.style.transform = `translateX(${moveX}px)`;
}
function onUp(e) {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
// 记录最终点
let finalX = e.clientX - startX;
track.push({x: finalX, y: e.clientY - startY, t: Date.now()});
// 提交验证
submitValidation(finalX, track);
}
移动端适配 :同样使用 touchstart, touchmove, touchend。
四、 后端验证逻辑(核心:轨迹分析)
这是判断"人还是机器"的关键,也是自研最难的部分。
java
public boolean verify(CaptchaVerifyReq req) {
String sessionId = req.getSessionId();
Integer realGapX = redisTemplate.opsForValue().get("SLIDER_CAP:" + sessionId);
if (realGapX == null) return false;
// 使用后立即删除,防止重用
redisTemplate.delete("SLIDER_CAP:" + sessionId);
// 1. 坐标比对 (允许一定误差,如3-5像素)
int submittedX = req.getFinalX();
// 注意:前端图片可能被缩放,需要按比例换算!
double scale = req.getImageWidth() / bgOriginalWidth;
int calculatedX = (int) (submittedX / scale);
if (Math.abs(calculatedX - realGapX) > 5) {
return false; // 位置不对
}
// 2. 轨迹分析 (防御机器脚本)
List<TrackPoint> track = req.getTrack(); // 解密后
if (!isHumanTrack(track)) {
return false; // 轨迹像机器
}
return true; // 验证通过
}
轨迹人机检测核心函数 isHumanTrack 实现思路:
java
private boolean isHumanTrack(List<TrackPoint> track) {
if (track.size() < 5) return false;
// 1. 总耗时检查:太快 (<0.3s) 或太慢 (>10s) 都可能是机器
long totalTime = track.get(track.size()-1).t - track.get(0).t;
if (totalTime < 300 || totalTime > 10000) return false;
// 2. 瞬时速度检查:是否存在瞬间大距离跳动 (脚本通常匀速或瞬移)
for (int i=1; i<track.size(); i++) {
long dt = track.get(i).t - track.get(i-1).t;
if (dt == 0) continue;
double dx = track.get(i).x - track.get(i-1).x;
double instantSpeed = dx / dt; // 像素/毫秒
if (instantSpeed > 2.0) { // 移动过快,疑似程序跳跃
return false;
}
}
// 3. 轨迹形状分析:真人有加速减速过程,会有"回拖"或"停顿"
// 检查是否有反向移动 (回拖) ------ 很多真人会在快到终点时回拉一点点
boolean hasReverse = false;
for (int i=2; i<track.size(); i++) {
if (track.get(i).x < track.get(i-1).x) {
hasReverse = true;
break;
}
}
// 高级机器可以模拟回拖,但通过大量数据训练的模型可以分辨细微差别。
// 对于简易自研,只做基础判断。
if (!hasReverse) {
// 无回拖特征,但可能真人直接精准到位,可提高容错或降低权重
// 这里简单处理:若总耗时>1.5秒且无回拖,认为是高风险
if (totalTime > 1500) return false;
}
// 4. Y轴波动检查:真人在水平拖动时Y轴会有微小抖动,脚本常是直线
double yVariance = calculateYVariance(track);
if (yVariance < 0.5) return false; // 几乎没有抖动
return true;
}
轨迹分析的本质是机器学习问题。真正的生产级方案需要采集大量正负样本训练模型(如逻辑回归、CNN、RNN等),判断轨迹是真人还是机器。自研的规则方式只能对抗最简单的脚本。
五、 前端发送前的数据安全处理
绝对不要把原始轨迹数组明文传给后端!需要混淆和签名。
- 加密传输 :对
track数组、finalX等用AES加密,密钥在服务端生成并动态注入到前端(或通过key-exchange)。 - 环境指纹:收集浏览器指纹、时间差等,一起加密传到后端,后端验证指纹一致性。
- 防重放:会话ID一次性,后端一旦验证(无论成功失败)就销毁。
示例加密:
javascript
// 前端使用 CryptoJS
const payload = JSON.stringify({finalX, track, timestamp, nonce});
const encrypted = CryptoJS.AES.encrypt(payload, sessionKey).toString();
// 提交到后端
后端解密后获取原始数据进行验证。
六、 自制方案的利弊总结
| 优点 | 缺点与风险 |
|---|---|
| 完全掌控代码和数据,无隐私泄露风险 | 安全性依赖算法强度:简单的轨迹规则极易被脚本针对破解 |
| 高度可定制UI和交互 | 需要持续对抗升级:攻击者会分析代码,模拟真人轨迹 |
| 无第三方服务成本 | 开发与维护成本高:图片处理、机器学习模型训练需要专业团队 |
| 无网络依赖,运行环境可控 | 不同平台兼容性:移动端、不同浏览器处理复杂 |
建议:
- 初期验证 :可以用此自研流程快速上线,但轨迹分析部分必须预留机器学习模型接口。
- 生产环境:如果用户量上来,强烈建议接入成熟的第三方验证码服务(如极验、网易易盾),它们已经把轨迹的深度学习模型做到了极高防御水平,自研很难超越。
- 混合使用:自研图片生成和交互,把轨迹数据作为参数传给第三方的纯API校验(如果服务商提供),兼顾定制与安全。
按照上面的流程,你可以搭建出一个基础可用的滑块验证码系统。重点在于后端轨迹分析算法的不断优化,这是自研的核心护城河。