驗證碼是 Web 安全的第一道防線,用於區分人機、防暴力破解、刷票、灌水。雖然單次通過率約 90%,但其必要性不容置疑。本文回顧 Java 實現驗證碼的四種方案演進,從最原始的圖片硬編碼到 Servlet 動態繪製,剖析各方案優劣與核心代碼。
流程:PS 預製圖片 → 前端顯示 → 後端 Map 維護「圖片檔名 ↔ 驗證碼字串」 → 提交時比對。
// 伪代码:極其低效且不安全
Map codeMap = new HashMap<>();
codeMap.put("captcha_1.jpg", "A3F9");
codeMap.put("captcha_2.jpg", "7K2M");
// ...
// 驗證時:codeMap.get(filename).equals(userInput)
致命缺陷:圖片有限、易被識別破解、維護成本極高、無法動態生成、安全性極低。僅供理解概念,嚴禁生產使用。
核心思想:「用代碼畫圖」。利用 BufferedImage 作畫布,Graphics2D 當畫筆,隨機生成字元、干擾線、噪點,最後輸出 JPEG 流。
BufferedImage bi = new BufferedImage(width, height, TYPE_INT_RGB);Graphics2D g = bi.createGraphics();g.fillRect(...)、g.drawRect(...)g.drawLine(x1,y1,x2,y2)g.drawString(ch, x, y)session.setAttribute("captcha", codeStr);ImageIO.write(bi, "JPEG", response.getOutputStream());@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
BufferedImage bi = new BufferedImage(120, 30, BufferedImage.TYPE_INT_RGB);
Graphics2D g = bi.createGraphics();
// 背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, 120, 30);
// 干擾線
g.setColor(Color.GREEN);
for (int i = 0; i < 5; i++) {
g.drawLine(random.nextInt(120), random.nextInt(30), random.nextInt(120), random.nextInt(30));
}
// 驗證碼字元
String chars = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"; // 去除易混淆字元
StringBuilder sb = new StringBuilder();
g.setFont(new Font("微軟雅黑", Font.BOLD, 24));
for (int i = 0; i < 4; i++) {
char ch = chars.charAt(random.nextInt(chars.length()));
sb.append(ch);
g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
// 隨機旋轉增加識別難度
AffineTransform orig = g.getTransform();
g.rotate(Math.toRadians(random.nextInt(30) - 15), 20 + i * 25, 15);
g.drawString(String.valueOf(ch), 20 + i * 25, 22);
g.setTransform(orig);
}
// 存 Session(忽略大小寫驗證時轉大寫)
req.getSession().setAttribute("captcha", sb.toString().toUpperCase());
// 防快取 + 輸出
resp.setHeader("Cache-Control", "no-store, no-cache");
resp.setContentType("image/jpeg");
ImageIO.write(bi, "JPEG", resp.getOutputStream());
g.dispose();
}
}
<img id="captchaImg" src="/captcha" onclick="this.src='/captcha?t=' + Date.now()" />
<script>
function refreshCaptcha() {
document.getElementById('captchaImg').src = '/captcha?t=' + Date.now();
}
</script>
關鍵點:Date.now() 或 Math.random() 作為查詢參數,強制瀏覽器繞過快取重新請求 Servlet。
手寫 Servlet 易出 Bug(字體缺失、干擾不足、Session 同步)。成熟庫提供:
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
# application.yml
kaptcha:
border: no
textproducer:
char-string: "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"
char-length: 4
font-size: 28
利用 Canvas/WebGL 在瀏覽器端生成驗證碼(如滑塊拼圖、點選文字),後端僅驗證軌跡/座標。優點:減輕伺服器渲染壓力、體驗更好;缺點:前端代碼可被逆向分析,需配合行為分析、設備指紋等風控體系。
sessionCode.equalsIgnoreCase(userInput)session.removeAttribute("captcha"),防重放。| 方案 | 適用階段 | 優點 | 缺點 |
|---|---|---|---|
| 預製圖片 | 教學/原型 | 零代碼繪圖 | 極不安全、不可維護 |
| Servlet 手繪 | 學習/簡單項目 | 可控、無依賴 | 易出 Bug、功能弱、字體跨平台問題 |
| 成熟庫 | 企業級生產 | 功能強、穩定、易配置 | 引入依賴、需學習配置 |
| 前端生成 | 高併發/極致體驗 | 零後端渲染、交互豐富 | 前端可逆向、需風控體系配合 |
建議:學習階段手寫 Servlet 理解原理;生產環境直接接入 Kaptcha 或 Hutool 的 CaptchaUtil;高併發/高安全場景評估前端行為驗證方案。