本文目录导读:

Spring Boot实现图形验证码案例:从零到生产级的完整指南
目录导读
- 为什么还需要图形验证码? —— 安全背景与核心价值
- 技术选型对比 —— 自研 vs 第三方库(Hutool / Kaptcha / EasyCaptcha)
- Spring Boot整合EasyCaptcha实战 —— 接口设计、前端联调、Redis存储
- 高频问题与最佳实践 —— 并发、刷新、防重放攻击、分布式会话
- 常见面试问答 —— 校验失败、前端无法显示、Redis宕机等场景
为什么还需要图形验证码?
在OAuth2、JWT、短信验证码大行其道的今天,图形验证码依然是防机器刷接口的第一道防线,它的核心价值在于低成本拦截自动化脚本——比如登录接口的暴力破解、注册接口的垃圾数据灌入、以及秒杀系统的批量抢购。
但注意:图形验证码只能提高攻击成本,不能完全杜绝攻击,生产环境中通常配合行为验证(滑块、点选) 或频率限制(Rate Limiter) 一起使用,本文聚焦如何在Spring Boot中优雅地实现一个可扩展的图形验证码模块。
技术选型对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 自研(BufferedImage) | 完全可控,无额外依赖 | 需要处理干扰线、扭曲、字体库,代码量大 | 对样式有特殊要求 |
| Kaptcha | 老牌稳定,配置多 | 配置繁琐,API较老 | 遗留项目 |
| Hutool工具类 | 代码极简,中文文档好 | 验证码样式相对单一 | 快速原型 |
| EasyCaptcha | 支持GIF、中文、算术,API友好 | 较新,社区稍小 | 推荐,兼顾效果与效率 |
本案例采用EasyCaptcha + Redis存储 + Base64前端直出,不依赖Session,天然支持分布式。
Spring Boot整合EasyCaptcha实战
1 引入依赖(pom.xml)
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2 生成验证码接口(Controller)
@RestController
@RequestMapping("/api/captcha")
public class CaptchaController {
@Autowired
private StringRedisTemplate redisTemplate;
// 生成验证码:返回Base64图片 + UUID
@GetMapping("/generate")
public Result generate() {
// 1. 生成算术验证码(或 SpecCaptcha 图片验证码)
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48);
captcha.setLen(2); // 两位运算
// 2. 获取运算结果("10")
String code = captcha.text();
// 3. 存入Redis,5分钟有效,一次性使用
String sessionId = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("captcha:" + sessionId, code, 5, TimeUnit.MINUTES);
// 4. 返回前端:Base64前缀 + UUID
Map<String, Object> data = new HashMap<>();
data.put("uuid", sessionId);
data.put("img", captcha.toBase64()); // 直接返回 data:image/png;base64,...
return Result.success(data);
}
// 校验验证码(在登录接口内调用)
public boolean validate(String uuid, String userInput) {
String key = "captcha:" + uuid;
String rightCode = redisTemplate.opsForValue().get(key);
redisTemplate.delete(key); // 立刻作废,防止重放
if (rightCode != null && rightCode.equalsIgnoreCase(userInput.trim())) {
return true;
}
return false;
}
}
3 前端集成(Vue/React示例)
// 页面加载时请求图片
axios.get('/api/captcha/generate').then(res => {
this.captchaImg = res.data.data.img;
this.captchaUuid = res.data.data.uuid;
});
// 刷新验证码(点击图片)
$('#captchaImg').click(function() {
axios.get('/api/captcha/generate').then(...);
});
4 登录接口校验逻辑
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
// 1. 先校验验证码
if (!captchaService.validate(dto.getUuid(), dto.getCode())) {
return Result.fail(400, "验证码错误或已过期");
}
// 2. 再进行账号密码校验...
}
高频问题与最佳实践
1 并发场景防重复使用
上面的代码中,validate方法先get再delete非原子性。高并发下可能出现同一验证码被多次校验通过,改进方案:
// 使用Redis Lua脚本保证原子性(等价于 get + delete)
DefaultRedisScript<Long> script = new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
Long.class
);
2 前端图片显示异常(显示裂图)
常见原因:Base64字符串过长被网关截断,解决方案:
- 确认网关(Nginx)对
proxy_buffering的配置 - 前端
<img src="data:image/png;base64,...">不要用引号包裹
3 验证码永远校验失败排查链路
- Redis是否宕机:此时接口应抛出降级异常,返回
503 - Key是否拼错:注意
"captcha:" + uuid和生成时一致 - 大小写问题:校验时用
equalsIgnoreCase - 时间戳问题:服务器时间与Redis过期时间不匹配(少用)
4 分布式会话替代方案
如果不使用Redis,用Session存储验证码,在多实例部署时会失败,因此生产环境强制要求Redis/本地缓存(Caffeine)+ 分布式锁。
常见面试问答
Q1:图形验证码如何防止OCR识别? 答:通过增加干扰线、随机扭曲、背景噪点,以及采用算术验证码(如"1+2=?")让OCR难以区分数值与运算符,更高级的可采用行为验证,但此处不展开。
Q2:验证码过期时间设置多久合适?
答:推荐3~5分钟,过长会被攻击者利用,过短会降低用户体验,可配置化,用@Value("${captcha.expire}")动态读取。
Q3:为什么我刷新验证码,但Redis里的值还在?
答:刷新验证码时没有主动删除旧Key,建议在generate接口中,按用户维度删除旧的uuid,但更通用的做法是让前端每次刷新携带旧uuid,后端先删除再生成。
Q4:验证码在前后端分离项目里怎么传递用户状态?
答:不要用Session,而是把uuid放在Redis的Key中,前端每次请求带上uuid即可,这也是本案例采用的方式。
总结与扩展
本案例展示了使用EasyCaptcha + Redis实现图形验证码的最小闭环,核心亮点:
- 无Session,适配分布式架构
- 一次性使用,防止重放攻击
- Base64直出,减少一次HTTP请求
如果想进一步升级,可以考虑:
- 集成
spring-boot-starter-validation对输入非空校验 - 使用
Logout时主动清理所有验证码Key - 结合
Sentinel做验证码发送频率限制
希望这篇Spring Boot图形验证码案例能帮你快速落地,同时理解背后的安全设计逻辑,如果你在集成过程中遇到问题,欢迎按上述排查链路逐项定位。