图形验证码是最常见的人机校验手段之一,几乎所有涉及登录、注册、找回密码的页面都能看到它的身影。对于Java技术栈的项目来说,Kaptcha是一个非常成熟的验证码生成库,它基于SimpleCaptcha扩展而来,配置灵活、生成的图片干扰性强,与Spring Boot整合也非常顺手。本文将从依赖引入、核心配置、接口开发到前端展示,完整演示一次整合过程,并补充Session存储、防重放校验等实际项目中必须注意的细节。

一、Kaptcha的工作原理与依赖引入
Kaptcha的核心思路是在服务端随机生成一串字符文本,然后通过Java的Graphics2D把这段文本绘制成一张带有噪点、干扰线、扭曲效果的图片,最后把图片以二进制流的形式写回给浏览器。与此同时,正确答案会保存在服务端(通常是Session),等用户提交表单时再与用户输入的值比对。
整个过程对客户端来说只暴露了图片,验证逻辑全部在服务端完成,安全性有基本保障。Kaptcha支持定制的内容相当丰富,包括验证码字符集合、图片边框、字体大小、干扰线数量、背景颜色渐变等,这些都可以通过配置项或代码方式控制。
在Spring Boot项目中引入依赖非常简单,如果项目使用Spring Boot 3.x,需要注意选择Kaptcha 2.3.2及以上版本,并且该依赖托管在谷歌仓库,建议直接使用下面这份坐标:
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>这份依赖是社区维护的版本,兼容性较好, javax.servlet 和 jakarta.servlet 环境都能通过适配解决。如果你的项目还是Spring Boot 2.x,引入后基本开箱即用,不需要额外处理。
二、配置验证码生成器并暴露图片接口
Kaptcha提供了一个Producer接口,我们只需要在配置类中声明一个Bean,Spring容器启动时就会自动创建验证码生成器。下面是常见的配置方式,把各项参数都做了中文注释:
@Configuration
public class KaptchaConfig {
@Bean
public Producer captchaProducer() {
Properties props = new Properties();
// 验证码字符范围:去掉了容易混淆的字母和数字
props.setProperty("kaptcha.textproducer.char.string", "23456789ABCDEFGHJKMNPQRSTUVWXYZ");
// 验证码长度
props.setProperty("kaptcha.textproducer.char.length", "4");
// 图片宽度
props.setProperty("kaptcha.image.width", "120");
// 图片高度
props.setProperty("kaptcha.image.height", "40");
// 字体大小
props.setProperty("kaptcha.textproducer.font.size", "32");
// 干扰线颜色
props.setProperty("kaptcha.noise.color", "blue");
// 干扰实现类
props.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
// 背景渐变起止色
props.setProperty("kaptcha.background.clear.from", "240,240,240");
props.setProperty("kaptcha.background.clear.to", "255,255,255");
Config config = new Config(props);
return new DefaultKaptcha(config);
}
}配置里的字符集合特意去掉了数字1、字母I、字母O和数字0这类肉眼难分的字符,这是提升用户体验的小技巧,能明显降低用户输错的概率。
接下来编写生成图片的Controller接口。这里有一个关键点:生成验证码后必须立刻调用createText拿到文本并存入Session,然后再用createImage生成图片,两次调用的文本是同一个值,千万不要先建图再取文本。
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private Producer captchaProducer;
@GetMapping("/image")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 先生成文本,存入Session
String text = captchaProducer.createText();
request.getSession().setAttribute("captchaCode", text);
// 设置响应头,禁止浏览器缓存
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
// 生成图片并写回
BufferedImage image = captchaProducer.createImage(text);
try (ServletOutputStream out = response.getOutputStream()) {
ImageIO.write(image, "jpg", out);
}
}
}接口没有返回JSON,而是直接把图片流写回响应体,前端只需把img标签的src指向这个接口地址即可。响应头里一定要加上禁止缓存的设置,否则部分浏览器会复用旧图片,导致用户看到的图和Session里的答案对不上。
三、前端展示与校验接口封装
前端部分几乎不需要额外代码,用一个img标签加载验证码,再绑定点击事件刷新即可。刷新时给URL拼上一个随机参数,强制浏览器重新请求:
<div class="captcha-box">
<input type="text" id="captchaInput" placeholder="请输入验证码">
<img id="captchaImg" src="/captcha/image" onclick="refreshCaptcha()" title="点击刷新">
</div>
<script>
function refreshCaptcha() {
document.getElementById('captchaImg').src = '/captcha/image?t=' + new Date().getTime();
}
</script>服务端的校验接口建议单独封装,登录请求进来后先校验验证码,通过再走账号密码逻辑。校验时注意三点:忽略大小写、校验后立即清除Session中的值、验证码为空时也返回失败。下面是一个可复用的工具方法:
@Service
public class CaptchaService {
public boolean verify(HttpServletRequest request, String input) {
HttpSession session = request.getSession();
String expected = (String) session.getAttribute("captchaCode");
// 无论校验成功与否,一次性使用,防止重放
session.removeAttribute("captchaCode");
if (expected == null || input == null || input.trim().isEmpty()) {
return false;
}
return expected.equalsIgnoreCase(input.trim());
}
}一次性消费是这段代码最重要的设计。如果校验失败后不清除Session里的值,攻击者就可以拿到一个正确验证码后反复请求接口,验证码就形同虚设了。
四、集群环境下的进阶实践
上面的方案把验证码存在Session里,单机部署没有问题。但一旦项目部署多台机器并使用Nginx做负载均衡,用户请求生成验证码和提交登录可能落在不同节点上,Session不共享就会导致校验必然失败。
解决这个问题有两条路。一是引入Spring Session加Redis,让所有节点共享Session,改动成本最低;二是绕开Session,自己生成一个UUID作为凭证,把验证码文本和UUID一起写入Redis并设置过期时间,前端拿到UUID后随表单一起提交。第二种方式对前后端分离架构更友好,参考实现如下:
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/captcha/v2")
public Map<String, String> captchaV2(HttpServletResponse response) throws IOException {
String text = captchaProducer.createText();
String uuid = UUID.randomUUID().toString();
// 存入Redis,两分钟后自动过期
redisTemplate.opsForValue().set("captcha:" + uuid, text, 2, TimeUnit.MINUTES);
BufferedImage image = captchaProducer.createImage(text);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", bos);
String base64 = Base64.getEncoder().encodeToString(bos.toByteArray());
Map<String, String> result = new HashMap<>();
result.put("uuid", uuid);
result.put("image", "data:image/jpeg;base64," + base64);
return result;
}这个版本直接返回Base64编码的图片,前端用img标签的src直接展示返回的data URI即可,省去了静态资源请求。校验时通过Redis的get和delete原子操作取出并删除,同样保证一次性使用。设置2分钟过期时间也很有必要,既能给用户留够输入时间,又能缩短验证码被暴力尝试的窗口期。
另外从安全角度补充一点:图形验证码只能挡住最基础的脚本攻击,如果业务对安全要求更高,可以在此基础上叠加接口限流、错误次数锁定、行为验证码等手段,多层防御才能构建真正可靠的防护体系。
Spring BootKaptcha图形验证码修改时间:2026-09-03 09:39:01