项目结构(生产推荐)
captcha-service 是一个基于 Spring Boot 构建的验证码微服务生产骨架,它整合了滑块验证与行为验证,并采用 Redis 进行状态管理。
captcha-service
├── controller
│ └── CaptchaController.java
├── service
│ ├── SliderCaptchaService.java
│ ├── BehaviorCaptchaService.java
│ └── RiskDecisionService.java
├── model
│ ├── SliderTrack.java
│ ├── BehaviorEvent.java
│ ├── CaptchaResult.java
│ └── CaptchaDecision.java
├── util
│ ├── TokenUtil.java
│ └── RiskUtil.java
├── config
│ └── RedisConfig.java
└── CaptchaApplication.java
核心数据模型
1️⃣ 决策枚举
public enum CaptchaDecision {
PASS,
SLIDER,
REJECT
}
2️⃣ 返回结果
@Data
@AllArgsConstructor
public class CaptchaResult {
private CaptchaDecision decision;
private String captchaToken;
}
3️⃣ 滑块轨迹
@Data
public class SliderTrack {
private int x;
private int y;
private long t;
}
4️⃣ 行为事件
@Data
public class BehaviorEvent {
private String type; // move / click / key
private int x;
private int y;
private long timestamp;
}
Token 工具(一次性 + 防重放)
为每次验证生成一个一次性令牌,是防止重放攻击的基础。
public class TokenUtil {
private static final String SECRET = "captcha-secret";
public static String generate(String sessionId) {
String raw = sessionId + ":" + System.currentTimeMillis() + ":" + UUID.randomUUID();
return DigestUtils.sha256Hex(raw + SECRET);
}
}
滑块验证码 Service(重点)
滑块验证的核心逻辑包含两个维度:最终位置校验和拖拽轨迹分析。
@Service
public class SliderCaptchaService {
public boolean verifyPosition(int userX, int targetX) {
return Math.abs(userX - targetX) <= 5;
}
public boolean verifyTrack(List<SliderTrack> tracks) {
if (tracks == null || tracks.size() < 10) return false;
long duration = tracks.get(tracks.size() - 1).getT()
- tracks.get(0).getT();
if (duration < 300) return false;
boolean acc = false, dec = false;
for (int i = 2; i < tracks.size(); i++) {
int dx1 = tracks.get(i - 1).getX() - tracks.get(i - 2).getX();
int dx2 = tracks.get(i).getX() - tracks.get(i - 1).getX();
if (dx2 > dx1) acc = true;
if (dx2 < dx1) dec = true;
}
return acc && dec;
}
public boolean verify(int userX, int targetX, List<SliderTrack> tracks) {
return verifyPosition(userX, targetX) && verifyTrack(tracks);
}
}
⚠️ 注意:
前端轨迹只是“参考”,最终裁决一定在服务端
行为验证码 Service(风控核心)
无感验证码的核心在于通过收集用户在页面上的行为数据(鼠标移动、点击、键盘事件)进行风险评分。
@Service
public class BehaviorCaptchaService {
public double score(List<BehaviorEvent> events) {
double score = 0;
score += timeScore(events);
score += trajectoryScore(events);
score += entropyScore(events);
return score;
}
private double timeScore(List<BehaviorEvent> events) {
long duration = events.get(events.size() - 1).getTimestamp()
- events.get(0).getTimestamp();
if (duration < 500) return -50;
if (duration > 10000) return -10;
return 30;
}
private double trajectoryScore(List<BehaviorEvent> events) {
int straight = 0;
for (int i = 2; i < events.size(); i++) {
int dx1 = events.get(i - 1).getX() - events.get(i - 2).getX();
int dx2 = events.get(i).getX() - events.get(i - 1).getX();
if (dx1 == dx2) straight++;
}
return straight > 5 ? -30 : 30;
}
private double entropyScore(List<BehaviorEvent> events) {
Set<Integer> xs = events.stream().map(BehaviorEvent::getX).collect(Collectors.toSet());
return xs.size() < 5 ? -20 : 20;
}
}
风险决策 Service(非常关键)
基于行为验证的评分,进行风险分级决策。
@Service
public class RiskDecisionService {
public CaptchaDecision decide(double score) {
if (score >= 60) return CaptchaDecision.PASS;
if (score >= 20) return CaptchaDecision.SLIDER;
return CaptchaDecision.REJECT;
}
}
Controller(完整链路)
这是整合行为验证与决策的主入口。
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private BehaviorCaptchaService behaviorService;
@Autowired
private RiskDecisionService decisionService;
@PostMapping("/check")
public CaptchaResult check(@RequestBody List<BehaviorEvent> events,
HttpServletRequest request) {
double score = behaviorService.score(events);
CaptchaDecision decision = decisionService.decide(score);
if (decision == CaptchaDecision.PASS) {
return new CaptchaResult(decision, null);
}
String token = TokenUtil.generate(request.getSession().getId());
// TODO: token + 状态写入 Redis,TTL 5min
return new CaptchaResult(decision, token);
}
}
Redis Key 设计(上线必备)
验证码令牌在 Redis 中的存储结构。
captcha:token:{token}
{
"decision": "SLIDER",
"deviceFp": "...",
"expire": 300
}
TTL:5 分钟
✔ 一次使用
✔ 自动过期
这套代码已经具备什么能力?
✅ 抗脚本
✅ 抗重放
✅ 支持无感 → 滑块升级
✅ 支持后续点选 / 短信
✅ 能接入风控 / 黑名单
✅ 可灰度 / 可观测
Redis + Lua = 原子性 + 防重放 + 防并发绕
以下是可直接用在 Spring Boot 项目中的完整实现,核心是使用 Redis Lua 脚本保证校验的原子性。
✅ Redis Key 设计
✅ Lua 原子校验脚本
✅ Java 调用 Lua
✅ 防重放 / 防并发 / 防多次提交
✅ 验证码生命周期闭环
为什么必须用 Lua(而不是 Java if 判断)
很多项目在验证码校验时存在并发漏洞。
❌ 错误做法(99% 项目中都在犯)
if (redis.exists(key)) {
redis.delete(key);
return PASS;
}
👉 问题:
- 并发请求可同时
exists
- Token 可被多次使用
- 被脚本重放直接绕过
✅ 正确做法
校验 + 使用 + 删除 = 一个原子操作
Redis Lua 脚本天生满足原子性要求,可以一次性完成所有检查和状态更新。
Redis Key 设计(生产级)
1️⃣ 验证码 Token Key
Key: captcha:token:{token}
Type: Hash
TTL: 300s
{
"type": "SLIDER",
"status": "INIT",
"deviceFp": "fp_hash",
"risk": "0.42"
}
2️⃣ 使用状态约定
| status |
含义 |
| INIT |
未使用 |
| USED |
已验证 |
| EXPIRE |
过期 |
Lua 脚本:防重放核心(重点)
1️⃣ Lua 校验逻辑说明
一次 Lua 脚本执行 5 件事:
- key 是否存在
- 是否已使用
- 设备指纹是否一致
- 标记为 USED
- 返回结果
2️⃣ Lua 脚本(直接可用)
-- captcha_verify.lua
-- KEYS[1] = captcha key
-- ARGV[1] = device fingerprint
local key = KEYS[1]
local fp = ARGV[1]
if redis.call("EXISTS", key) == 0 then
return -1 -- token 不存在
end
local status = redis.call("HGET", key, "status")
if status ~= "INIT" then
return -2 -- 已使用 or 非法状态
end
local savedFp = redis.call("HGET", key, "deviceFp")
if savedFp ~= fp then
return -3 -- 设备不一致
end
redis.call("HSET", key, "status", "USED")
redis.call("EXPIRE", key, 60) -- 使用后保留一小段时间用于审计
return 1
3️⃣ Lua 返回码约定
| 返回值 |
含义 |
| 1 |
校验成功 |
| -1 |
Token 不存在 |
| -2 |
Token 已使用 |
| -3 |
设备指纹不匹配 |
Spring Boot Redis 配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
Java 调用 Lua(关键代码)
1️⃣ Lua Script Bean
@Component
public class CaptchaLuaScript {
private final DefaultRedisScript<Long> script;
public CaptchaLuaScript() {
script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setScriptText(load());
}
private String load() {
return """
local key = KEYS[1]
local fp = ARGV[1]
if redis.call("EXISTS", key) == 0 then
return -1
end
local status = redis.call("HGET", key, "status")
if status ~= "INIT" then
return -2
end
local savedFp = redis.call("HGET", key, "deviceFp")
if savedFp ~= fp then
return -3
end
redis.call("HSET", key, "status", "USED")
redis.call("EXPIRE", key, 60)
return 1
""";
}
public DefaultRedisScript<Long> getScript() {
return script;
}
}
2️⃣ Redis 校验 Service
@Service
public class CaptchaRedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private CaptchaLuaScript luaScript;
public boolean verifyOnce(String token, String deviceFp) {
String key = "captcha:token:" + token;
Long result = redisTemplate.execute(
luaScript.getScript(),
Collections.singletonList(key),
deviceFp
);
return result != null && result == 1;
}
}
验证码生成时写 Redis(完整)
public void saveCaptchaToken(String token, String deviceFp, double risk) {
String key = "captcha:token:" + token;
Map<String, String> data = new HashMap<>();
data.put("status", "INIT");
data.put("deviceFp", deviceFp);
data.put("risk", String.valueOf(risk));
redisTemplate.opsForHash().putAll(key, data);
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
}
Controller 中的最终校验(闭环)
@PostMapping("/verify")
public ResponseEntity<?> verify(@RequestParam String token,
@RequestParam String deviceFp) {
boolean ok = captchaRedisService.verifyOnce(token, deviceFp);
if (!ok) {
return ResponseEntity.status(403).body("CAPTCHA INVALID");
}
return ResponseEntity.ok("PASS");
}
这套 Redis + Lua 防住了什么?
| 攻击方式 |
是否可行 |
| 重放 Token |
❌ |
| 并发绕过 |
❌ |
| 跨设备使用 |
❌ |
| 多次提交 |
❌ |
| 脚本抢占 |
❌ |
生产级增强(你下一步一定要做)
🔥 Lua + 限流融合
-- 同 IP / 设备 5 秒最多 3 次
🔥 USED Token 行为审计
captcha:used:{date}
🔥 风险反哺模型
USED + 后续异常 → 封设备
架构师一句实话
验证码真正的安全,不在前端,也不在算法
而在:
“服务端原子性 + 一次性语义”
本文提供的代码框架旨在抛砖引玉,你可以根据自身业务需求,在 云栈社区 与更多开发者交流,进一步扩展和优化风控模型与校验逻辑。