圖片驗證碼是防止機器人暴力破解、垃圾註冊的第一道防線。本文基於 **Java Servlet + BufferedImage** 實作完整驗證碼生成與驗證流程,並提供進階干擾策略與現代替代方案。
@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
private static final int WIDTH = 120;
private static final int HEIGHT = 40;
private static final int CODE_LENGTH = 4;
private static final String CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; // 排除易混淆字元
private static final Random RANDOM = new Random();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 設定回應標頭:禁止快取、指定圖片類型
resp.setHeader("Pragma", "No-cache");
resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
resp.setDateHeader("Expires", 0);
resp.setContentType("image/jpeg");
// 2. 建立圖片緩衝區
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 3. 繪製背景 (隨機淺色)
g.setColor(getRandomColor(200, 250));
g.fillRect(0, 0, WIDTH, HEIGHT);
// 4. 繪製干擾線 (10-20 條)
for (int i = 0; i < RANDOM.nextInt(11) + 10; i++) {
g.setColor(getRandomColor(100, 200));
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);
}
// 5. 繪製干擾點 (100-200 個)
for (int i = 0; i < RANDOM.nextInt(101) + 100; i++) {
g.setColor(getRandomColor(100, 250));
int x = RANDOM.nextInt(WIDTH);
int y = RANDOM.nextInt(HEIGHT);
g.drawOval(x, y, 1, 1);
}
// 6. 產生隨機驗證碼並繪製文字
StringBuilder code = new StringBuilder();
Font font = new Font("Arial", Font.BOLD | Font.ITALIC, 28);
g.setFont(font);
FontMetrics fm = g.getFontMetrics();
int charWidth = (WIDTH - 10) / CODE_LENGTH;
for (int i = 0; i < CODE_LENGTH; i++) {
char c = CHARS.charAt(RANDOM.nextInt(CHARS.length()));
code.append(c);
g.setColor(getRandomColor(20, 100)); // 深色文字
int x = 5 + i * charWidth + RANDOM.nextInt(5);
int y = HEIGHT / 2 + fm.getAscent() / 2 - 2;
g.drawString(String.valueOf(c), x, y);
}
// 7. 釋放繪圖資源
g.dispose();
// 8. 存入 Session (key 建議唯一化,如 captcha_{sessionId})
req.getSession().setAttribute("captcha_code", code.toString().toLowerCase()); // 忽略大小寫
// 9. 輸出圖片
ImageIO.write(image, "JPEG", resp.getOutputStream());
}
private Color getRandomColor(int min, int max) {
int r = min + RANDOM.nextInt(max - min);
int g = min + RANDOM.nextInt(max - min);
int b = min + RANDOM.nextInt(max - min);
return new Color(r, g, b);
}
}
<form action="loginHandler" method="post">
<div>
<label>用戶名:</label>
<input name="username" required>
</div>
<div>
<label>密碼:</label>
<input type="password" name="password" required>
</div>
<div>
<label>驗證碼:</label>
<input name="captcha" maxlength="4" required>
<img id="captchaImg" src="/captcha" alt="點擊更換驗證碼" style="cursor:pointer; vertical-align:middle;" title="點擊更換">
</div>
<button type="submit">登入</button>
</form>
<script>
document.getElementById('captchaImg').onclick = function() {
this.src = '/captcha?' + Date.now(); // 加時間戳防快取
};
</script>
@WebServlet("/loginHandler")
public class LoginHandler extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
String inputCaptcha = req.getParameter("captcha");
// 1. 取得 Session 中驗證碼
String realCaptcha = (String) req.getSession().getAttribute("captcha_code");
// 2. 基本校驗
if (realCaptcha == null || inputCaptcha == null ||
!realCaptcha.equalsIgnoreCase(inputCaptcha.trim())) {
resp.sendRedirect("login.jsp?error=captcha");
return;
}
// 3. 驗證碼一次性使用,驗證後立即移除
req.getSession().removeAttribute("captcha_code");
// 4. 進行用戶名/密碼驗證 (略)
// boolean ok = userService.login(username, password);
// if (ok) { req.getSession().setAttribute("user", username); resp.sendRedirect("index.jsp"); }
// else { resp.sendRedirect("login.jsp?error=auth"); }
}
}
| 策略 | 實作方式 | 效果 |
|---|---|---|
| 字體隨機化 | 準備多種字體檔,隨機選擇 `new Font(fonts[random], Font.BOLD, size)` | 增加 OCR 識別難度 |
| 文字扭曲/旋轉 | `AffineTransform` 旋轉、切變每個字元 | 顯著降低機器識別率 |
| 中文驗證碼 | 字庫 `String CHARS = "的一是不了人..."`,字體需支援中文 (如微軟雅黑) | 針對中文環境更友善 |
| 算術/邏輯驗證碼 | 顯示 `12 + 5 = ?`、`選擇水果圖片` | 語義理解門檻高 |
| Session 分離存儲 | Redis 存儲 `captcha:{uuid}`,前端攜帶 UUID,無狀態擴展 | 叢集環境共享、支援分布式 |
| 速率限制 | 同一 IP/Session 分鐘請求上限 (Guava RateLimiter / Redis 計數) | 防暴力刷新 |
傳統圖片驗證碼以 **BufferedImage + Session** 實作簡單有效,但面對 AI OCR 已形同虛設。生產系統建議**疊加多層防護**:
1. 前端行為分析 (滑鼠軌跡、鍵盤節奏) → 2. 無感驗證 (reCAPTCHA v3) → 3. 高風險觸發圖形/滑塊驗證 → 4. 後端速率限制 + 帳號鎖定。
本文提供的 Servlet 實作適合教學、內部系統、低風險場景;對外開放系統請採用成熟風控服務。