BufferedImage 內存圖片畫布Graphics2D 繪製文字、線條、形狀ImageIO.write() 輸出 PNG/JPEG 流public class CaptchaUtil {
private static final Random RANDOM = new Random();
private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 排除易混淆字符
private static final int WIDTH = 120, HEIGHT = 40;
private static final int CODE_LENGTH = 4;
public static BufferedImage generate(StringBuilder codeHolder) {
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 1. 背景
g.setColor(randomColor(200, 250));
g.fillRect(0, 0, WIDTH, HEIGHT);
// 2. 干擾線
g.setColor(randomColor(160, 200));
for (int i = 0; i < 20; i++) {
int x1 = RANDOM.nextInt(WIDTH);
int y1 = RANDOM.nextInt(HEIGHT);
int x2 = RANDOM.nextInt(WIDTH);
int y2 = RANDOM.nextInt(HEIGHT);
g.drawLine(x1, y1, x2, y2);
}
// 3. 驗證碼文字
g.setFont(new Font("微軟雅黑", Font.BOLD, 28));
StringBuilder code = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
char c = CHARS.charAt(RANDOM.nextInt(CHARS.length()));
code.append(c);
g.setColor(randomColor(20, 130));
int x = 15 + i * 25;
int y = HEIGHT - 8;
g.drawString(String.valueOf(c), x, y);
}
// 4. 干擾噪點
g.setColor(randomColor(160, 200));
for (int i = 0; i < 30; i++) {
g.drawRect(RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT), 1, 1);
}
g.dispose();
codeHolder.setLength(0);
codeHolder.append(code);
return image;
}
private static Color randomColor(int min, int max) {
return new Color(RANDOM.nextInt(max - min) + min,
RANDOM.nextInt(max - min) + min,
RANDOM.nextInt(max - min) + min);
}
// 輸出到 OutputStream (Servlet 直接寫響應)
public static void writeToStream(OutputStream os, StringBuilder codeHolder) throws IOException {
BufferedImage img = generate(codeHolder);
ImageIO.write(img, "PNG", os);
}
}
@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
StringBuilder code = new StringBuilder();
BufferedImage img = CaptchaUtil.generate(code);
// 存入 Session
req.getSession().setAttribute("captcha_code", code.toString().toLowerCase());
// 禁止快取
resp.setHeader("Cache-Control", "no-store, no-cache");
resp.setContentType("image/png");
ImageIO.write(img, "PNG", resp.getOutputStream());
}
}
使用 Unicode CJK 統一表意文字基本區(0x4E00-0x9FA5)隨機選取常用字,並加入旋轉變形。
public class ChineseCaptchaUtil {
private static final Random RANDOM = new Random();
private static final int WIDTH = 130, HEIGHT = 40;
private static final int CODE_LENGTH = 4;
// 3500 常用字子集(實際可從文件加載)
private static final String COMMON_CHARS = "的一是不了人我在有他這為之大來以個中們上..."; // 省略,需補全
public static BufferedImage generate(StringBuilder codeHolder) {
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 背景漸變
GradientPaint gp = new GradientPaint(0, 0, Color.WHITE, 0, HEIGHT, new Color(230, 240, 250));
g.setPaint(gp);
g.fillRect(0, 0, WIDTH, HEIGHT);
// 邊框
g.setColor(Color.LIGHT_GRAY);
g.drawRect(0, 0, WIDTH-1, HEIGHT-1);
// 干擾曲線
g.setColor(new Color(180, 180, 200));
for (int i = 0; i < 5; i++) {
drawRandomCurve(g);
}
// 繪製漢字
g.setFont(new Font("微軟雅黑", Font.BOLD, 30));
StringBuilder code = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
char ch = COMMON_CHARS.charAt(RANDOM.nextInt(COMMON_CHARS.length()));
code.append(ch);
// 隨機旋轉 ±25 度
double angle = (RANDOM.nextDouble() - 0.5) * Math.PI / 3.6; // ±25°
int x = 20 + i * 28;
int y = 30;
AffineTransform original = g.getTransform();
g.rotate(angle, x, y);
g.setColor(randomDarkColor());
g.drawString(String.valueOf(ch), x, y);
g.setTransform(original); // 恢復變換
}
g.dispose();
codeHolder.setLength(0);
codeHolder.append(code);
return image;
}
private static void drawRandomCurve(Graphics2D g) {
int x1 = RANDOM.nextInt(WIDTH);
int y1 = RANDOM.nextInt(HEIGHT);
int x2 = RANDOM.nextInt(WIDTH);
int y2 = RANDOM.nextInt(HEIGHT);
int ctrlX = RANDOM.nextInt(WIDTH);
int ctrlY = RANDOM.nextInt(HEIGHT);
QuadCurve2D curve = new QuadCurve2D.Float(x1, y1, ctrlX, ctrlY, x2, y2);
g.draw(curve);
}
private static Color randomDarkColor() {
return new Color(RANDOM.nextInt(100), RANDOM.nextInt(100), RANDOM.nextInt(100));
}
}
<!-- HTML -->
<img id="captchaImg" src="/captcha" onclick="this.src='/captcha?t='+Date.now()" style="cursor:pointer">
<input type="text" id="captchaInput" placeholder="輸入驗證碼">
<script>
$('#captchaInput').on('blur', function() {
$.post('/verify-captcha', {code: this.value}, function(res) {
if (!res.valid) alert('驗證碼錯誤');
});
});
</script>
@PostMapping("/verify-captcha")
@ResponseBody
public Map verify(@RequestParam String code, HttpSession session) {
String real = (String) session.getAttribute("captcha_code");
boolean valid = real != null && real.equalsIgnoreCase(code.trim());
if (valid) session.removeAttribute("captcha_code"); // 一次性使用
return Map.of("valid", valid);
}