Spring Boot实现图形验证码案例

wen java案例 2

本文目录导读:

Spring Boot实现图形验证码案例

  1. 目录导读
  2. 为什么还需要图形验证码?
  3. 技术选型对比
  4. Spring Boot整合EasyCaptcha实战
  5. 高频问题与最佳实践
  6. 常见面试问答
  7. 总结与扩展

Spring Boot实现图形验证码案例:从零到生产级的完整指南

目录导读

  1. 为什么还需要图形验证码? —— 安全背景与核心价值
  2. 技术选型对比 —— 自研 vs 第三方库(Hutool / Kaptcha / EasyCaptcha)
  3. Spring Boot整合EasyCaptcha实战 —— 接口设计、前端联调、Redis存储
  4. 高频问题与最佳实践 —— 并发、刷新、防重放攻击、分布式会话
  5. 常见面试问答 —— 校验失败、前端无法显示、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方法先getdelete非原子性。高并发下可能出现同一验证码被多次校验通过,改进方案:

// 使用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 验证码永远校验失败排查链路

  1. Redis是否宕机:此时接口应抛出降级异常,返回503
  2. Key是否拼错:注意"captcha:" + uuid和生成时一致
  3. 大小写问题:校验时用equalsIgnoreCase
  4. 时间戳问题:服务器时间与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图形验证码案例能帮你快速落地,同时理解背后的安全设计逻辑,如果你在集成过程中遇到问题,欢迎按上述排查链路逐项定位。

抱歉,评论功能暂时关闭!