Passport 登录滑块验证安全方案(产品级设计)
好的——把“滑块挑战(challenge)我把设计里所有相关点都改成基于关系库实现,并补充了会用到的并发/幂等/过期清理策略、性能注意项与实现建议(都不含具体代码)。
下面是完整、可落地的设计更新。
要点总结(一句话)
滑块挑战与校验数据全部写入 MySQL(新增 passport_slider_challenge 表),后端用事务与原子更新保证“一次一用”,通过 expire_time + 定期清理任务代替 Redis TTL(可选);
同时保留 passport_slider_verify_log 日志表用于审计。
一、总体流程(MySQL 版)
- 前端请求
/passport/captcha/slider/init - 后端生成挑战(图片/正确 X、token 等)并写入
passport_slider_challenge表,返回captcha_token + 图片给前端。 - 前端渲染、用户滑动后提交轨迹到
/passport/captcha/slider/verify。 - 后端在单个事务内读取 challenge(by token)并做校验:误差、时间、轨迹质量等;若校验通过则 原子标记为已使用(更新 consumed_flag / status);同时写
passport_slider_verify_log。 - 若验证成功,允许继续后续登录流程;失败则更新失败计数并记录日志。
- 后端定期(例如每 5 分钟)异步任务:删除或归档
expire_time早于当前时间且未消费的 challenge 行,或把其状态置为过期。
关键点:一切校验与“销毁/标记已用”动作都要在数据库事务/原子操作中完成,防止重放或并发竞态。
二、数据库表设计(必须包含你要求的基础字段)
1) 滑块挑战表(用于存放挑战,替代 Redis)
表名:passport_slider_challenge
CREATE TABLE passport_slider_challenge (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
captcha_token VARCHAR(64) NOT NULL UNIQUE COMMENT '挑战 token (外部返回给前端)',
correct_x INT NOT NULL COMMENT '服务端正确 X',
correct_y INT NOT NULL DEFAULT 0 COMMENT '服务端正确 Y(如需要)',
background_image MEDIUMBLOB NULL COMMENT '背景图二进制或 base64(可选,若图片走 CDN 可不放)',
slider_image MEDIUMBLOB NULL COMMENT '滑块图像二进制(可选)',
biz_scene VARCHAR(32) NOT NULL DEFAULT 'LOGIN' COMMENT '业务场景',
expire_time DATETIME(3) NOT NULL COMMENT '过期时间',
consumed_flag TINYINT(4) NOT NULL DEFAULT 0 COMMENT '是否已消费(0未消费 1已消费)',
client_ip VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
creator_id BIGINT NULL,
updater_id BIGINT NULL,
status TINYINT(4) NOT NULL DEFAULT 1,
delete_flag TINYINT(4) NOT NULL DEFAULT 0,
INDEX idx_token (captcha_token),
INDEX idx_expire (expire_time),
INDEX idx_consume (consumed_flag)
);
说明:
captcha_token唯一且短期有效(例如 2 分钟)。consume_flag或者status用于标记一次性使用,校验时需以事务保证从 0 -> 1 原子更新成功才判定为成功。- 图片字段可选:若系统走静态图 CDN/文件存储,表里只需保存路径或 id。
2) 滑块验证日志表(审计)
表名:passport_slider_verify_log(保留并略微补充)
CREATE TABLE passport_slider_verify_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NULL,
captcha_token VARCHAR(64) NOT NULL,
client_ip VARCHAR(64) NOT NULL,
user_agent VARCHAR(255) NULL,
slider_x INT NULL,
correct_x INT NULL,
deviation INT NULL,
verify_result TINYINT NOT NULL COMMENT '1成功 0失败',
spend_time INT NULL COMMENT '滑动耗时ms',
track_points INT NULL COMMENT '轨迹点数量',
biz_scene VARCHAR(32) NOT NULL,
fail_reason VARCHAR(255) NULL,
status TINYINT(4) NOT NULL DEFAULT 1,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
creator_id BIGINT NULL,
updater_id BIGINT NULL,
delete_flag TINYINT(4) NOT NULL DEFAULT 0,
INDEX idx_user (user_id),
INDEX idx_result (verify_result),
INDEX idx_ip (client_ip)
);
三、后端校验 + 幂等性与并发控制(核心设计,不是代码)
1) 原子消费(一次一用)
-
在单个数据库事务内完成以下操作:
SELECT ... FOR UPDATE(或使用乐观锁/WHERE consumed_flag = 0 AND captcha_token = ? 的 UPDATE 返回受影响行数)读取 challenge 行并上锁(或进行原子更新)。- 检验
expire_time、consumed_flag是否允许校验。 - 对轨迹、时间、误差等做业务校验。
- 如果校验通过,执行
UPDATE passport_slider_challenge SET consumed_flag = 1, update_time = now() WHERE id = ? AND consumed_flag = 0,并检查受影响行数是否为 1(保证原子性)。 - 无论成功与否,都写入
passport_slider_verify_log(可在同一事务或另一个事务里写日志;为保证可观测性,建议尽量在同一事务后异步写入日志或事务内写入以防丢失)。
-
若在更新时发现
consumed_flag已被改为 1,则视为重放或并发,直接拒绝。
2) 防止重放攻击
- 校验成功后立即把
captcha_token标记为已消费(上面一步),后续重复提交因consumed_flag被置位而被拒绝。 - 若对安全非常敏感,可在
passport_slider_challenge中加入used_by_session_id或used_by_user_id字段记录消费者。
3) 并发场景与锁策略
-
推荐使用 乐观更新 + WHERE consumed_flag = 0 的单语句 UPDATE 判断受影响行数,避免长事务锁表。流程更稳健:
- 先读取行(非锁),做判断(例如 expire_time),再执行
UPDATE ... SET consumed_flag=1 WHERE captcha_token=? AND consumed_flag=0 AND expire_time >= now();若影响 1 行则继续校验(或先校验再 update,取决实现);常见做法是把校验也放在事务里并用 FOR UPDATE。
- 先读取行(非锁),做判断(例如 expire_time),再执行
-
如果系统压力大且并发高,避免使用
SELECT FOR UPDATE长时间锁行,改用短事务的 compare-and-swap 模式。
四、过期清理与归档策略(替代 Redis TTL)–可选
MySQL 无 TTL,因此需要后台任务清理:
-
定时任务(Cron / Spring
@Scheduled)每 1~5 分钟:DELETE FROM passport_slider_challenge WHERE expire_time < now() AND consumed_flag = 0(或将其改状态为EXPIRED归档到历史表)- 若担心直接删除,可先移动到归档表(
passport_slider_challenge_history)再删除
-
对
passport_slider_verify_log做周期归档与分区(按月分区)以便查询性能。
五、性能与扩展注意事项
- 索引:确保对
captcha_token、expire_time、consumed_flag索引合理组合(如复合索引)以提高查询/删除速度。 - 图片存储:尽量不要把图片二进制频繁读写 DB(若并发高),可把图片上传到文件存储或 CDN,表里只保存路径/ID。
-
按量级考虑:如果并发很高(比如每秒数千/万次),MySQL 可能成为瓶颈。此时的演进路径:
- 用 MySQL 做主存储 + 本地内存/短期缓存(仅缓存 token 校验状态)
- 或将挑战数据迁回 Redis(混合架构)——但你当前要求全 MySQL,则应做写入/清理优化与分表分区。
- 分区:对
passport_slider_challenge按日期分区(按日或按小时)可以让过期清理更高效。
六、API 与 Controller 拆分(同前,强调 MySQL 行为)
-
SliderCaptchaController(用户侧)POST /passport/captcha/slider/init→ 生成 token,写passport_slider_challenge,返回 token + 图POST /passport/captcha/slider/verify→ 提交轨迹;后端在事务中做一次性消费、验证,写passport_slider_verify_log
-
SliderCaptchaAdminController(管理侧)GET /admin/passport/slider/log/page→ 查询passport_slider_verify_logGET /admin/passport/slider/challenge/list→ 查询passport_slider_challenge(管理查看未消费、过期、统计)
七、管理/用户视角功能调整(MySQL 存储无差异)
-
管理员
- 查看挑战库存(例如:未消费挑战数、过期数)
- 配置滑块策略(误差阈值、最短滑动时间、最大失败次数)
- 查询错误 IP / 设备、聚合失败趋势
-
普通用户
- 无需特别视角(滑块验证为登录的客户端体验),但用户操作失败日志会出现在
passport_slider_verify_log,用于自助与客服排查
- 无需特别视角(滑块验证为登录的客户端体验),但用户操作失败日志会出现在
八、开发规范(沿用你给的约束)
- 尽量参考 1-2 个旧代码风格(例如你现有的验证码模块或 token 模块),复用工具类(时间工具、IP 工具、DB util)。
- 后端:高内聚、适当内耦合;禁止使用 lombok + beanCopy。
- mybatis XML:对
<>等比较符号做转义处理;时间比较在 SQL 中用expire_time >= #{now}可行,但注意参数格式与预编译,避免注入。 - 前端:Vue3 组件化,滑块组件可复用、遵循现有风格。
九、安全与一致性补充要点
- 校验时间窗口:挑战生成时记录
create_time与expire_time,后端拒绝过期 token。 - 防止机器人重放:一次性消费 + 鼓励在数据库写入
client_fingerprint(设备指纹)与client_ip以便联动风控。 - 日志完整性:
passport_slider_verify_log必须尽量写入(若事务失败,考虑异步补写策略并上报失败率)。 - 失败阈值:设置 IP 或 Token 失败阈值,超过阈值后对 IP 限流或黑名单。
十、清理/运维建议
- 编写 DB 维护脚本(归档/压缩历史表)并和监控告警联动(例如
passport_slider_challenge行数异常增长触警)。 - 定期检查未消费挑战增长是否异常(可能是前端未正确回收或攻击)。
- 对
passport_slider_challenge建议按时间分区并定期 DROP 过期分区以优化删除性能。
十一、为什么用 MySQL 也能做到稳定?
- MySQL 能保证强一致性(事务),适合“一次性消费”场景:通过事务可确保 challenge 被准确标记为已消费,防止重放。
- 代价是性能与运维成本(必须做好索引、分区、归档、批量删除)。在并发不是极端高的场景下,MySQL 完全能提供稳定和可审计的实现。
已有的方案
有的,而且还不少 —— 如果你的目标是 不想自己造轮子,直接稳定落地滑块验证码,下面这几款在 Java / SpringBoot 场景里都算“比较成熟”的方案,我帮你按「靠谱程度 + 易用性」排个序。
✅ 强烈推荐方案(生产可控 + 可私有部署)
1. Anji Captcha(安吉滑块) ⭐⭐⭐⭐⭐
最接近你需求的:Java + SpringBoot + 数据库存储可控 + 企业级使用多
- GitHub:anji-plus/captcha
-
特点:
- 专门为后端系统设计的滑块验证码
- 原生支持 Spring Boot Starter
- 支持:滑块 + 图片验证码 + 点选验证码
- 支持 MySQL / Redis 存储(你可以只用 MySQL)
- 支持轨迹校验、防机器刷、异常识别
- Maven 引入:
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>captcha-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
适合场景:
- 自建 Passport / SSO
- 要可控、可审计、可二次开发的企业系统
✅ 综合评价:
企业级、可控、安全、易扩展,几乎是 Java 滑块验证码的“标准答案”。
2. Tianai Captcha(天爱安全验证码) ⭐⭐⭐⭐☆
另一款国产开源滑块方案,技术成熟度也不错。
- GitHub:tianai-captcha
-
特点:
- 专注滑块验证码
- 支持行为轨迹分析
- 提供 SpringBoot Starter
- 代码成熟,社区活跃
Maven 示例:
<dependency>
<groupId>com.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.4.2</version>
</dependency>
适合:
- 不想绑定商业服务
- 希望有一定防刷能力,但不想过度复杂
✅ 商业成熟方案(更强风控能力)
如果你愿意接入第三方平台:
3. 极验 Geetest
国内非常成熟,滑块验证码鼻祖之一
- 安全策略强
- 反外挂能力强
- 但:需要注册 + 商业收费(免费额度有限)
4. 腾讯云 TCaptcha
腾讯的人机验证方案:
- 稳定性强
- 微信生态友好
- 对接相对规范
❌ 不太建议的
| 类型 | 原因 |
|---|---|
| 自己写 Canvas + JS 滑块 | 容易被脚本模拟,安全等级低 |
| GitHub 上无人维护项目 | 后期升级难,风险高 |
| 只前端校验的方案 | 形同虚设,极易绕过 |
推荐结论(结合你的系统场景)
你当前场景是:
- Passport 登录系统
- SpringBoot
- 要可管控、可审计、支持 MySQL
- 不想重新实现
✅ 最优方案建议:
| 场景 | 推荐 |
|---|---|
| 私有部署 + 可控 + 可扩展 | Anji Captcha |
| 轻量但稳定 | Tianai Captcha |
| 高安全 + 商业级 | Geetest / 腾讯 CAPTCHA |
👉 综合结论:
如果是你的 Passport 系统,我会强烈建议: 直接选 Anji Captcha 做底座,然后按你现有规范进行二次封装。
建议架构融合方式
不是完全“裸用”,而是这种模式:
Passport登录
↓
滑块验证组件(Anji)
↓
包装成你的 SliderService
↓
写入 passport_slider_* 表
↓
统一认证链路
也就是说:
