Java生成验证码案例怎么写?从零到生产级的完整指南(附核心代码)
目录导读
- 为什么验证码是Java Web应用的“安全守门员”?
- 验证码生成的底层原理:图形绘制与随机算法
- 手把手实战:基于Java原生API的验证码案例(纯Servlet+ImageIO)
- 进阶优化:如何加入干扰线、噪点、扭曲与字符粘连
- 生产级方案:集成Redis验证码存储与Spring Boot接口
- 高频面试问答:验证码防刷、识别与性能瓶颈
- Java验证码的最佳实践路线图
为什么验证码是Java Web应用的“安全守门员”?
在Web安全攻防中,验证码(CAPTCHA)的核心作用是区分人类用户与自动化程序,Java后端开发中,常见场景包括:登录防暴力破解、注册防批量刷号、评论防垃圾灌水、支付接口防重放,一个设计良好的验证码案例,不仅要能生成图片,还需要考虑时效性、一次性、服务端校验等安全维度。

根据搜索引擎聚合的技术文章(如CSDN、Stack Overflow、Baeldung),大多数教程只教“画图”,却忽略“校验逻辑”,本文将从图形渲染与服务端状态管理双维度,给你一套完整可落地的方案。
验证码生成的底层原理:图形绘制与随机算法
Java生成验证码的底层依赖以下核心类:
java.awt.BufferedImage:内存中的图像缓冲区,用于绘制像素。java.awt.Graphics2D:提供绘图方法,如画字符串、画线、填充椭圆。javax.imageio.ImageIO:将内存图像编码为JPEG/PNG输出流。
随机算法上,常见策略是从字符池(排除易混淆的0/O、1/l)随机选取N个字符,生成随机颜色、随机旋转角度、随机字体,为了防OCR(光学字符识别),还需加入:
- 干扰线(随机坐标连线)
- 噪点(随机像素点)
- 波浪形扭曲(通过Graphics2D的shear或transform方法)
关键点:验证码图片只负责“生成”,而校验值必须存放在服务端Session或Redis中,严禁存入Cookie(可被篡改)。
手把手实战:基于Java原生API的验证码案例(纯Servlet+ImageIO)
这是搜索热度最高的基础案例,适合初学者理解完整流程,以下代码经过“去伪存真”优化,删除了网上常见的无效逻辑(如未设置响应头导致的图片乱码)。
1 创建验证码生成工具类 CaptchaUtil.java
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.security.SecureRandom;
public class CaptchaUtil {
private static final int WIDTH = 120;
private static final int HEIGHT = 40;
private static final char[] CHARS = {
'A','B','C','D','E','F','H','J','K','M','N','P','Q','R','S','T','U','V','W','X','Y',
'2','3','4','5','6','7','8','9'
};
private static final SecureRandom RANDOM = new SecureRandom();
public static String generate(HttpServletResponse response) throws IOException {
// 1. 创建空白图像
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
// 2. 填充背景色(浅灰色渐变效果)
g2d.setColor(new Color(245, 245, 245));
g2d.fillRect(0, 0, WIDTH, HEIGHT);
// 3. 绘制随机干扰线(5条)
g2d.setStroke(new BasicStroke(1.5f));
for (int i = 0; i < 5; i++) {
g2d.setColor(new Color(RANDOM.nextInt(200), RANDOM.nextInt(200), RANDOM.nextInt(200)));
g2d.drawLine(RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT),
RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT));
}
// 4. 生成4位随机字符并绘制
StringBuilder code = new StringBuilder();
for (int i = 0; i < 4; i++) {
char c = CHARS[RANDOM.nextInt(CHARS.length)];
code.append(c);
// 随机字体、大小、颜色、旋转
Font font = new Font("Arial", Font.BOLD | Font.ITALIC, 24 + RANDOM.nextInt(8));
g2d.setFont(font);
g2d.setColor(new Color(30 + RANDOM.nextInt(100), 30 + RANDOM.nextInt(100), 30 + RANDOM.nextInt(100)));
// 轻微旋转(-30度到30度)
double angle = (RANDOM.nextInt(60) - 30) * Math.PI / 180;
g2d.rotate(angle, 20 + i * 25, HEIGHT / 2);
g2d.drawString(String.valueOf(c), 15 + i * 25, 28 + RANDOM.nextInt(6));
g2d.rotate(-angle, 20 + i * 25, HEIGHT / 2);
}
// 5. 添加噪点(随机小矩形)
for (int i = 0; i < 30; i++) {
g2d.setColor(new Color(RANDOM.nextInt(255), RANDOM.nextInt(255), RANDOM.nextInt(255)));
g2d.fillRect(RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT), 1, 2);
}
// 6. 释放资源并输出
g2d.dispose();
response.setContentType("image/jpeg");
response.setHeader("Cache-Control", "no-store");
ImageIO.write(image, "jpeg", response.getOutputStream());
return code.toString();
}
}
2 编写Servlet控制器 CaptchaServlet.java
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String code = CaptchaUtil.generate(resp);
// 将验证码存入Session(注意:生产环境建议用Redis)
req.getSession().setAttribute("captcha_code", code);
req.getSession().setMaxInactiveInterval(60); // 60秒过期
}
}
3 前端调用与校验逻辑
<!-- html片段 -->
<img src="/captcha" onclick="this.src='/captcha?t='+new Date().getTime()"/>
<!-- 提交时校验 -->
<p>输入验证码:</p>
<input name="captchaInput">
<button onclick="submitForm()">提交</button>
<script>
function submitForm() {
const input = document.querySelector('input[name="captchaInput"]').value;
fetch('/verifyCaptcha', {
method: 'POST',
body: new URLSearchParams({captchaInput: input})
}).then(res => res.text()).then(msg => alert(msg));
}
</script>
校验Servlet片段:
String sessionCode = (String) req.getSession().getAttribute("captcha_code");
String inputCode = req.getParameter("captchaInput");
if(sessionCode != null && sessionCode.equalsIgnoreCase(inputCode)) {
// 成功逻辑(立即移除,防复用)
req.getSession().removeAttribute("captcha_code");
}
注意:很多网帖在
equalsIgnoreCase后不删除Session属性,这是严重漏洞,会导致验证码可复用,被脚本无限提交。
进阶优化:如何加入干扰线、噪点、扭曲与字符粘连
若你的网站面临较高攻击风险,基础案例不够安全,以下优化点来自Baeldung和GitHub开源项目的精华:
| 优化手段 | 实现方式 | 防识别效果 |
|---|---|---|
| 字符粘连 | 使用等宽字体(如Courier New)加上字符间重叠绘制 |
防止基于字符分割的OCR |
| 贝塞尔曲线干扰 | 用g2d.drawPath绘制2-3条波浪曲线 |
显著增加图形复杂度 |
| 背景纹理 | 填充随机渐变或细密网格 | 干扰二值化处理 |
| 变色渐变 | 每个字符单独或连续渐变色 | 使颜色分割失效 |
| 弹性扭曲 | 使用AffineTransform对图像进行整体扭曲(如正弦波变换) |
极难还原 |
示例代码:添加贝塞尔曲线
// 在绘制字符之前,添加两条随机曲线
for (int i = 0; i < 2; i++) {
int x1 = RANDOM.nextInt(WIDTH/2);
int y1 = RANDOM.nextInt(HEIGHT);
int x2 = WIDTH/2 + RANDOM.nextInt(WIDTH/2);
int y2 = RANDOM.nextInt(HEIGHT);
int ctrlx1 = RANDOM.nextInt(WIDTH);
int ctrlx2 = RANDOM.nextInt(WIDTH);
g2d.setColor(new Color(100 + RANDOM.nextInt(100), 100 + RANDOM.nextInt(100), 100 + RANDOM.nextInt(100)));
g2d.drawPath(new Path2D.Float() {{
moveTo(x1, y1);
curveTo(ctrlx1, y1 + 20, ctrlx2, y2 - 20, x2, y2);
}});
}
生产级方案:集成Redis验证码存储与Spring Boot接口
在分布式或微服务架构中,Session共享是痛点,推荐使用Redis存储验证码,这是当前企业级项目的标准做法。
1 核心设计
- Key格式:
captcha:uuid:用户标识或客户端ID - Value:验证码字符串
- TTL:60~120秒
- 验证后立即删除:
DEL key(原子操作)
2 Spring Boot实现片段
@RestController
public class CaptchaController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/captcha/prod")
public void getCaptcha(@RequestParam String clientId, HttpServletResponse response) throws IOException {
String code = CaptchaUtil.generate(response);
stringRedisTemplate.opsForValue().set(
"captcha:" + clientId, code, 120, TimeUnit.SECONDS
);
}
@PostMapping("/verify")
public Result verify(@RequestBody VerifyDTO dto) {
String redisKey = "captcha:" + dto.getClientId();
String saved = stringRedisTemplate.opsForValue().get(redisKey);
if (saved != null && saved.equalsIgnoreCase(dto.getInput())) {
stringRedisTemplate.delete(redisKey); // 一次性使用
return Result.success("验证通过");
}
return Result.error("验证码错误或已失效");
}
}
3 为何不使用JWT或前端存储?
尽管有人尝试把验证码放入JWT,但这是不安全的,因为JWT可被解析且无法主动注销,Redis方案不仅支持过期,还能配合防刷限流(如同一clientId每分钟最多请求3次验证码)。
高频面试问答:验证码防刷、识别与性能瓶颈
Q1:如何防止验证码被OCR自动识别?
答案:多管齐下——使用干扰线+噪点+扭曲;限制字符长度为4~6;定时更换字符池;增加逻辑计算验证码(如“3+5=?”),高级方案可用行为验证码(如滑块/点选字),但Java后端生成逻辑更复杂。
Q2:验证码图片加载慢或占用内存大?
答案:用JPEG代替PNG可减小体积但牺牲清晰度;控制图片尺寸在150x50以内;使用BufferedImage.TYPE_INT_RGB压缩率更高,若并发巨大,可预生成一批图片存入内存缓存(如Caffeine),但需注意随机性。
Q3:为什么我设置了Cache-Control: no-store,图片还是被缓存?
答案:需同时设置Pragma: no-cache(兼容HTTP/1.0)和Expires: 0,部分浏览器还要求加上Last-Modified头,但最有效的是在URL后拼接随机参数(如时间戳t)。
Q4:验证码在集群环境下Session失效怎么办?
答案:采用第5章节的Redis方案,或使用Spring Session以Redis存储Session,避免使用自带的本机Session。
Q5:高频接口(如登录)验证码被秒刷?
答案:结合计数限流——同一IP或用户标识每60秒只生成1次验证码;若不通过则禁止刷新,同时可以通过Thread.sleep(50)微延迟降低自动化脚本速度。
Java验证码的最佳实践路线图
从“能画出来”到“生产可用”,Java验证码案例的关键演进路径:
- 基础版:原生Servlet + ImageIO + Session(适合学习)
- 进阶版:增加复杂干扰效果(干扰线、噪点、扭曲、粘连)
- 工程版:Spring Boot + Redis + 一次性校验 + 限流
- 商业版:接入第三方验证码服务(如极验、腾讯验证码)或内部行为识别引擎
最终建议:如果你的项目不是高安全等级,使用本文第3章源码即可;但涉及支付、后台管理、用户中心,请务必升级到第5章的Redis方案,并加上防刷逻辑。验证码只是第一道防线,还需配合IP黑名单、失败次数锁定等策略。
基于Java 8+技术栈,适配Spring Boot 2.x/3.x,代码可直接复制运行调试。)*