⚠️ 本文是设计篇,不落地代码。 滑块验证码的完整实现(前端滑块 + 后端校验)推迟到「开始有前端」阶段再做。本篇只把后端必须自写的部分设计清楚、把契约定下来,避免到时候返工。原因见第六节「为什么代码推迟」。

3.3~3.5 做的是短信验证码(防脚本批量注册)。短信验证码有个软肋:它是"知识因子"——6 位数字,真能拦住人肉,但拦不住"养一批号 + 接码平台"的专业黄牛。滑块验证码补的是另一层:行为因子——你拖动滑块的轨迹,机器人很难逼真模仿。这一篇讲它的设计。

一、为什么是滑块:人机区分的本质

短信验证码回答"你有没有这个手机号",滑块回答"你是不是真人在操作"。两者维度不同:

  • 短信 = 拥有因子(你有这个号,接得到码)
  • 滑块 = 行为因子(你的拖动轨迹像人)

黄牛可以批量为号接码,但很难批量为每个号生成"像人"的拖动轨迹。所以滑块是挡自动化脚本的更前沿一道,通常放在"发短信/登录/下单"之前做前置人机校验。

二、前端有成熟框架,后端没有

前端滑块不要自己造轮子——GitHub 上有成熟开源实现:

框架 特点
vue3-slide-verify Vue3 滑块,交互顺滑,社区活跃
slider-verify-v3 轻量,Vue3 适用
AJ-Captcha 行为验证码(滑块/点选),自带前后端契约,Java 生态成熟,C++ 后端需自写校验侧

但 C++ 后端没有现成实现——前端的"拼图/滑块组件"只是交互,真正的校验逻辑(challenge 生成、答案存储、容差、轨迹行为分析)必须后端自写,否则前端传来的"验证通过"标志可以被脚本直接伪造。

三、后端必须自写的部分

不管前端用哪个框架,后端要兜底四件事:

  1. challenge 生成:返回一张带缺口的背景图 + 缺口位置(或一段 challenge token),前端据此渲染滑块。答案(缺口 x 坐标)只在后端,前端永远拿不到真答案。
  2. 答案存 Rediscaptcha:challenge:{cid} → {answerX, expire, tryCount},有效期短(如 2 分钟),一次性消费(同短信验证码思路)。
  3. 容差:用户提交的 x 与真答案差 ≤ 阈值(如 ±5px)即算通过——滑块本就是"大致对齐",不能要精确像素。
  4. 轨迹行为分析:前端上报拖动过程的 track(每个时间点的 x/y/时间),后端判断:① 速度是否过于匀速(机器人常匀速);② 是否有"先快后慢"的人手特征;③ 总时长是否合理(太短=脚本秒过)。这是滑块相比纯"对坐标"多出来的抗脚本层。

四、后端契约设计

定两套接口,前端框架照此对接:

1
2
3
GET  /api/captcha/challenge   → 返回 {cid, bgImg, sliderImg, token}
(bgImg/sliderImg 可由后端用现成拼图库生成,或预生成图床)
POST /api/captcha/verify → body {cid, x, track} → {code:0} 通过 / {code:1,msg:...}

校验侧与短信同理,必须原子:比对 + 消费进一个 Lua(防并发同 challenge 被用两次)。通过后把 cid 标记为"已验证",注册/登录/下单流程在拿业务 token 前先校验这个 cid 已通过——等价于把滑块变成"前置人机闸门"。

1
2
3
4
5
6
7
8
9
10
-- 校验脚本(KEYS: challengeKey;ARGV: submitX/maxDiff/maxTry)
local ans = redis.call('GET', KEYS[1])
if not ans then return {-1, 0} end -- 不存在/过期
local tries = tonumber(redis.call('GET', KEYS[1]..':try') or '0')
if tries >= tonumber(ARGV[3]) then redis.call('DEL', KEYS[1]); return {-3, 0} end
local diff = math.abs(tonumber(ans) - tonumber(ARGV[1]))
if diff <= tonumber(ARGV[2]) then
redis.call('DEL', KEYS[1]); redis.call('DEL', KEYS[1]..':try'); return {0, 0} -- 通过即删
end
redis.call('INCR', KEYS[1]..':try'); return {-2, tries + 1}

五、安全模型:轨迹分析 + 容差,抗脚本

维度 纯坐标比对 坐标 + 轨迹分析(本文)
抗"直接发对坐标" 弱(脚本可抓包重放) 中(需真滑块交互)
抗"模拟坐标" 强(轨迹不像人被拒)
容差 需精确 ±阈值,贴合人手

注意:滑块验证码不是银弹。它把"自动化脚本"成本抬高到需要真模拟轨迹/养号,但对抗"人工打码平台"仍有限。正确定位是多层防护中的行为层,与短信验证码、登录锁定(3.6)、应用层在途闸门(4.8)叠加,而非单独扛全部防刷。

图 1:滑块验证码前后端分工(后端自写校验,前端用成熟框架) 前端(成熟框架)vue3-slide-verify渲染滑块 + 采轨迹上报 {cid,x,track} 后端(C++ 自写)challenge 生成Redis 存答案 + 容差轨迹行为分析 + Lua 校验 前端永远拿不到真答案;"通过"由后端说了算

六、为什么代码推迟到前端阶段

项目决策(2026-09-02,见 README「项目边界:前端页面」):

  1. 验收硬指标零贡献:阶段一验收是"压测 QPS 提升一个量级 + 不超卖不重复下单",前端/滑块对这条指标无贡献。
  2. 分散主线:阶段一主线是"事务化 DB 直扣 → 缓存 → MQ → Lua 原子"的吞吐演进,重前端会拖主线。
  3. 引入成本:Vue/Vite 会把"两条 cmake 命令"变成"先装 Node 再 npm i",读者直接劝退;且当前 /api/seckill 走 body 双通道、不校验 token,滑块的前置闸门没有挂载点。

已定的落地形态(到时候直接照做):单文件 www/index.html 原生 JS、无框架无 CDN;config.jsondocument_root: "./www" 走 Drogon 静态服务(同源零 CORS);前端引 vue3-slide-verify 等成熟框架,后端校验(challenge/Redis/Lua/轨迹)自写,挂载到 3.x 路由与 4.1/4.2 商品接口、/api/seckillAuthorization: Bearer

功能抉择(本篇核心权衡)

① 为什么前端用成熟框架、后端自写校验?
滑块的"交互组件"是前端成熟领域,重复造轮子既丑又易出交互 bug;但"答案生成 + 校验"是安全边界,绝不能完全信任前端传来的"通过"标志——后端必须掌握真答案与校验逻辑,否则滑块形同虚设。

② 为什么校验也要 Lua 原子 + 一次性消费?
与短信验证码(3.5)同一理由:并发下同一 challenge 被用两次、试错不计数等竞态,只有 Lua 单线程原子能根除。

③ 为什么现在不写、留到前端阶段?
滑块的挂载点(/api/seckill 接 Bearer、前端页面)当前都不存在;现在写后端校验没有前端联调、也没有压测收益,纯属透支注意力。把契约(本节四)定清楚,等前端阶段一次落地,反而更省。

小结

  • 滑块验证码补的是行为因子层,与短信(拥有因子)正交,专门挡自动化脚本。
  • 前端用成熟框架(vue3-slide-verify / slider-verify-v3 / AJ-Captcha),后端校验必须自写:challenge 生成 + Redis 存答案 + 容差 + 轨迹行为分析。
  • 后端契约:GET /api/captcha/challenge + POST /api/captcha/verify,校验 Lua 原子 + 一次性消费,通过即前置人机闸门。
  • 本文只设计、不落地代码——实现推迟到「开始有前端」阶段,落地形态已定(原生 JS 单页 + Drogon 静态服务 + 后端校验自写)。🐾