使用 java.awt.Graphics2D 繪製干擾線、噪點、隨機字符,輸出 PNG 流。關鍵參數可配置化。
public class CaptchaUtil {
private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 排除易混淆字符
private static final Random RANDOM = new Random();
// 生成驗證碼圖片
public static BufferedImage generateImage(int width, int height, String code, int lineCount, double noiseRate) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 1. 背景
g.setColor(randomColor(200, 255));
g.fillRect(0, 0, width, height);
// 2. 干擾線
for (int i = 0; i < lineCount; i++) {
g.setColor(randomColor(100, 200));
g.drawLine(RANDOM.nextInt(width), RANDOM.nextInt(height),
RANDOM.nextInt(width), RANDOM.nextInt(height));
}
// 3. 噪點
int noiseArea = (int) (width * height * noiseRate);
for (int i = 0; i < noiseArea; i++) {
image.setRGB(RANDOM.nextInt(width), RANDOM.nextInt(height), randomColor(0, 255).getRGB());
}
// 4. 字符繪製
int fontSize = Math.min(width * 4 / 5 / code.length(), height * 7 / 10);
g.setFont(new Font("Arial", Font.BOLD, fontSize));
FontMetrics fm = g.getFontMetrics();
int x = (width - fm.stringWidth(code)) / 2;
int y = (height - fm.getDescent()) / 2 + fm.getAscent();
for (int i = 0; i < code.length(); i++) {
g.setColor(randomColor(20, 130));
g.drawString(code.substring(i, i+1), x + i * fm.charWidth(code.charAt(i)), y);
}
g.dispose();
return image;
}
// 生成隨機驗證碼字符串
public static String generateCode(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length())));
}
return sb.toString();
}
private static Color randomColor(int min, int max) {
int r = clamp(RANDOM.nextInt(max - min + 1) + min);
int g = clamp(RANDOM.nextInt(max - min + 1) + min);
int b = clamp(RANDOM.nextInt(max - min + 1) + min);
return new Color(r, g, b);
}
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
// 圖片轉 byte[]
public static byte[] toBytes(BufferedImage image) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return baos.toByteArray();
}
}
@WebServlet("/captcha/image")
public class CaptchaImageServlet extends HttpServlet {
private static final int WIDTH = 120, HEIGHT = 40, LENGTH = 4;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 生成驗證碼
String code = CaptchaUtil.generateCode(LENGTH);
// 2. 存入 Session (大小寫不敏感驗證時統一轉大寫)
req.getSession().setAttribute("CAPTCHA_CODE", code.toUpperCase());
// 3. 生成圖片
BufferedImage image = CaptchaUtil.generateImage(WIDTH, HEIGHT, code, 8, 0.02);
// 4. 輸出響應
resp.setContentType("image/png");
resp.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
resp.setHeader("Pragma", "no-cache");
resp.setDateHeader("Expires", 0);
try (OutputStream out = resp.getOutputStream()) {
out.write(CaptchaUtil.toBytes(image));
}
}
}
<form action="/captcha/verify" method="post">
驗證碼: <input type="text" name="captchaInput" maxlength="4" required>
<img id="captchaImg" src="/captcha/image" alt="點擊刷新" title="看不清?點擊刷新" style="cursor:pointer; vertical-align:middle;">
<br>
<input type="submit" value="提交">
</form>
<script>
document.getElementById('captchaImg').onclick = function() {
// 加隨機參數防瀏覽器緩存
this.src = '/captcha/image?' + Date.now();
};
</script>
@WebServlet("/captcha/verify")
public class CaptchaVerifyServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String sessionCode = (String) req.getSession().getAttribute("CAPTCHA_CODE");
String inputCode = req.getParameter("captchaInput");
resp.setContentType("application/json;charset=UTF-8");
PrintWriter out = resp.getWriter();
boolean valid = sessionCode != null && inputCode != null
&& sessionCode.equals(inputCode.toUpperCase());
// 驗證後立即移除,防止重放攻擊
req.getSession().removeAttribute("CAPTCHA_CODE");
out.write("{\"success\":" + valid + ",\"message\":\"" + (valid ? "驗證通過" : "驗證碼錯誤") + "\"}");
}
}
@RestController
@RequestMapping("/api/captcha")
public class CaptchaController {
@GetMapping("/image")
public Map getCaptcha(HttpSession session) {
String code = CaptchaUtil.generateCode(4);
session.setAttribute("CAPTCHA_CODE", code.toUpperCase());
BufferedImage image = CaptchaUtil.generateImage(120, 40, code, 8, 0.02);
String base64 = Base64.getEncoder().encodeToString(CaptchaUtil.toBytes(image));
return Map.of("imageBase64", "data:image/png;base64," + base64);
}
@PostMapping("/verify")
public Map verify(@RequestParam String code, HttpSession session) {
String sessionCode = (String) session.getAttribute("CAPTCHA_CODE");
session.removeAttribute("CAPTCHA_CODE");
boolean valid = sessionCode != null && sessionCode.equals(code.toUpperCase());
return Map.of("success", valid, "message", valid ? "驗證通過" : "驗證碼錯誤");
}
}
前端直接 `` 即可,無緩存煩惱。