驗證碼是 Web 應用防暴力破解、機器註冊、刷單的第一道防線。本文提供一個開箱即用的 ValidCodeUtils 工具類,涵蓋:可配置寬高/顏色範圍、中英數混合字符池、每字符隨機字體/大小/傾斜、貝塞爾曲線/直線干擾、Servlet 輸出流輸出、Session 綁定驗證、前端點擊刷新防緩存機制,並給出生產環境加固建議。
package com.example.captcha;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
public final class ValidCodeUtils {
// ========== 可配置常量 ==========
public static int WIDTH = 130;
public static int HEIGHT = 48;
public static int CODE_LENGTH = 4;
// 顏色範圍:背景淡、干擾中、前景深
private static final int[] BG_RANGE = {200, 250};
private static final int[] LINE_RANGE = {160, 200};
private static final int[] CODE_RANGE = {20, 130};
// 字符池:去除易混淆字符 (0/O, 1/I/l, 2/Z, 5/S, 8/B)
private static final String CHAR_POOL =
"ABCDEFGHJKLMNPQRSTUVWXYZ" + // 大寫 24
"abcdefghjkmnpqrstuvwxyz" + // 小寫 24
"23456789"; // 數字 8
private static final String[] FONTS = {
"Arial", "Verdana", "Tahoma", "Georgia", "Courier New",
"Microsoft YaHei", "SimHei", "KaiTi"
};
private static final Random RANDOM = new Random();
// ========== 核心生成方法 ==========
public static boolean generate(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.reset();
resp.setContentType("image/jpeg");
resp.setHeader("Pragma", "No-cache");
resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
resp.setDateHeader("Expires", 0);
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 1. 背景填充
g.setColor(randomColor(BG_RANGE));
g.fillRect(0, 0, WIDTH, HEIGHT);
// 2. 干擾線(直線 + 貝塞爾曲線)
g.setColor(randomColor(LINE_RANGE));
for (int i = 0; i < 20; i++) {
drawRandomLine(g);
}
for (int i = 0; i < 5; i++) {
drawBezierCurve(g);
}
// 3. 干擾噪點
g.setColor(randomColor(LINE_RANGE));
for (int i = 0; i < 30; i++) {
int x = RANDOM.nextInt(WIDTH);
int y = RANDOM.nextInt(HEIGHT);
g.drawOval(x, y, 1, 1);
}
// 4. 繪製驗證碼字符
StringBuilder code = new StringBuilder();
FontMetrics fm = g.getFontMetrics();
int charWidth = (WIDTH - 20) / CODE_LENGTH;
for (int i = 0; i < CODE_LENGTH; i++) {
char ch = randomChar();
code.append(ch);
Font font = randomFont();
g.setFont(font);
g.setColor(randomColor(CODE_RANGE));
// 隨機旋轉角度 ±25°
AffineTransform orig = g.getTransform();
g.rotate(Math.toRadians(RANDOM.nextInt(50) - 25),
15 + i * charWidth + charWidth / 2, HEIGHT / 2);
g.drawString(String.valueOf(ch), 15 + i * charWidth, HEIGHT / 2 + fm.getAscent() / 2 - 2);
g.setTransform(orig);
}
// 5. 存入 Session(建議加上時間戳防重放)
req.getSession().setAttribute("captcha_code", code.toString().toLowerCase());
req.getSession().setAttribute("captcha_time", System.currentTimeMillis());
g.dispose();
return ImageIO.write(image, "JPEG", resp.getOutputStream());
}
// ========== 輔助方法 ==========
private static Color randomColor(int[] range) {
int fc = Math.max(0, range[0]);
int bc = Math.min(255, range[1]);
if (fc > bc) { int t = fc; fc = bc; bc = t; }
int r = fc + RANDOM.nextInt(bc - fc + 1);
int g = fc + RANDOM.nextInt(bc - fc + 1);
int b = fc + RANDOM.nextInt(bc - fc + 1);
return new Color(r, g, b);
}
private static char randomChar() {
return CHAR_POOL.charAt(RANDOM.nextInt(CHAR_POOL.length()));
}
private static Font randomFont() {
String name = FONTS[RANDOM.nextInt(FONTS.length)];
int style = RANDOM.nextBoolean() ? Font.BOLD : Font.PLAIN;
int size = 28 + RANDOM.nextInt(8); // 28~35
return new Font(name, style, size);
}
private static void drawRandomLine(Graphics2D g) {
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);
}
private static void drawBezierCurve(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);
// 簡易二次貝塞爾:分段畫線
for (double t = 0; t <= 1; t += 0.05) {
double u = 1 - t;
int x = (int) (u * u * x1 + 2 * u * t * ctrlX + t * t * x2);
int y = (int) (u * u * y1 + 2 * u * t * ctrlY + t * t * y2);
if (t > 0) g.drawLine(prevX, prevY, x, y);
prevX = x; prevY = y;
}
}
private static int prevX, prevY; // 用於貝塞爾連線
}
@WebServlet("/captcha.jpg")
public class CaptchaServlet extends HttpServlet {
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ValidCodeUtils.generate(req, resp);
}
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
<!-- JSP/Thymeleaf/Vue 通用 -->
<img id="captchaImg" src="${ctx}/captcha.jpg" style="cursor:pointer;vertical-align:middle"
onclick="refreshCaptcha()" title="點擊刷新" />
<input type="text" name="captcha" placeholder="輸入驗證碼" />
<script>
function refreshCaptcha() {
var img = document.getElementById('captchaImg');
img.src = '${ctx}/captcha.jpg?t=' + Date.now(); // 時間戳防瀏覽器緩存
}
</script>
@PostMapping("/login")
public Result login(@RequestParam String username,
@RequestParam String password,
@RequestParam String captcha,
HttpSession session) {
String realCode = (String) session.getAttribute("captcha_code");
Long genTime = (Long) session.getAttribute("captcha_time");
// 1. 空值檢查
if (realCode == null || captcha == null) {
return Result.error("驗證碼已失效,請刷新");
}
// 2. 時效性檢查(如 5 分鐘)
if (System.currentTimeMillis() - genTime > 5 * 60 * 1000) {
session.removeAttribute("captcha_code");
return Result.error("驗證碼已過期,請刷新");
}
// 3. 大小寫不敏感比對
if (!realCode.equalsIgnoreCase(captcha.trim())) {
return Result.error("驗證碼錯誤");
}
// 4. 驗證通過後立即清除,防重放
session.removeAttribute("captcha_code");
session.removeAttribute("captcha_time");
// 後續業務邏輯...
return Result.ok();
}
| 風險點 | 對策 |
|---|---|
| 驗證碼被 OCR 識別 | 增加貝塞爾曲線、扭曲、噪點、字符粘連;或接入 kaptcha / EasyCaptcha 成熟庫 |
| Session 洩漏/共享導致驗證碼被盜用 | 分佈式 Session (Redis) + 短過期時間 + 一次性驗證即銷毀 |
| 暴力破解驗證碼接口 | 同 IP 限流(如 10 次/分鐘)、接入行為驗證(滑塊/點選/無感) |
| 高併發下圖片生成 CPU 瓶頸 | 預先生成圖片池、或使用純前端 Canvas 生成(需防逆向) |
| 字體在 Linux 容器缺失導致方框 | 基礎鏡像安裝 fontconfig dejavu-sans-fonts wqy-zenhei;或打包 .ttf 字體文件隨應用加載 |
自研驗證碼適合中小型項目、內網系統、學習理解原理。核心要點:字符池去混淆、顏色範圍分層、每字符隨機字體/旋轉、干擾線曲線化、Session 綁定時效性、前端時間戳防緩存、驗證即銷毀。生產環境高安全場景強烈建議接入成熟的行為驗證服務,將安全對抗交給專業團隊。