圖形驗證碼(CAPTCHA)是 Web 應用防範惡意註冊、暴力破解、垃圾灌水的第一道防線。本文將從原理剖析、核心算法、完整 Servlet 實現、前端集成、安全加固五個維度,手把手教你構建一個生產可用的驗證碼組件。
java.awt.Graphics2D 在 BufferedImage 上繪製背景、干擾線、扭曲字符。HttpSession,鍵名如 validateCode。Pragma: no-cache, Cache-Control: no-cache, Expires: 0,確保每次請求獲取最新圖片。package com.servlet;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.http.*;
public class ValidateCodeServlet extends HttpServlet {
// 配置參數(可在 web.xml 覆蓋)
private int width = 120;
private int height = 40;
private int codeCount = 4;
private int fontHeight;
private int codeY;
private int x;
// 字符集:去除易混淆字符(0/O, 1/I, 8/B 等)
private static final char[] CODE_SEQUENCE = {
'A','B','C','D','E','F','G','H','J','K','M','N','P','Q','R','S','T','U','V','W','X','Y','Z',
'2','3','4','5','6','7','8','9'
};
@Override
public void init() throws ServletException {
// 從 web.xml 讀取初始化參數
String strWidth = getInitParameter("width");
String strHeight = getInitParameter("height");
String strCodeCount = getInitParameter("codeCount");
try {
if (strWidth != null && !strWidth.isEmpty()) width = Integer.parseInt(strWidth);
if (strHeight != null && !strHeight.isEmpty()) height = Integer.parseInt(strHeight);
if (strCodeCount != null && !strCodeCount.isEmpty()) codeCount = Integer.parseInt(strCodeCount);
} catch (NumberFormatException ignored) {}
// 計算佈局參數
x = width / (codeCount + 1);
fontHeight = height - 4;
codeY = height - 6;
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 創建畫布
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
Random random = new Random();
// 2. 填充白色背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 3. 設置字體(抗鋸齒)
Font font = new Font("Consolas", Font.BOLD, fontHeight);
g.setFont(font);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 4. 畫邊框
g.setColor(Color.DARK_GRAY);
g.drawRect(0, 0, width - 1, height - 1);
// 5. 繪製干擾線(數量可配置)
g.setColor(Color.LIGHT_GRAY);
for (int i = 0; i < 30; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(12);
int y2 = random.nextInt(12);
g.drawLine(x1, y1, x1 + x2, y1 + y2);
}
// 6. 生成並繪製驗證碼字符
StringBuilder randomCode = new StringBuilder();
for (int i = 0; i < codeCount; i++) {
char c = CODE_SEQUENCE[random.nextInt(CODE_SEQUENCE.length)];
randomCode.append(c);
// 隨機顏色(深色系,保證可讀性)
g.setColor(new Color(random.nextInt(100), random.nextInt(100), random.nextInt(100)));
// 隨機旋轉角度(-15° ~ +15°)增加識別難度
double theta = (random.nextDouble() - 0.5) * Math.PI / 6;
g.rotate(theta, (i + 1) * x, codeY);
g.drawString(String.valueOf(c), (i + 1) * x, codeY);
g.rotate(-theta, (i + 1) * x, codeY); // 恢復旋轉
}
// 7. 存入 Session
HttpSession session = req.getSession();
session.setAttribute("validateCode", randomCode.toString().toUpperCase());
// 8. 禁止緩存
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
resp.setDateHeader("Expires", 0);
resp.setContentType("image/jpeg");
// 9. 輸出圖片流
try (ServletOutputStream out = resp.getOutputStream()) {
ImageIO.write(image, "JPEG", out);
}
}
}
<servlet>
<servlet-name>ValidateCodeServlet</servlet-name>
<servlet-class>com.servlet.ValidateCodeServlet</servlet-class>
<init-param>
<param-name>width</param-name>
<param-value>130</param-value>
</init-param>
<init-param>
<param-name>height</param-name>
<param-value>45</param-value>
</init-param>
<init-param>
<param-name>codeCount</param-name>
<param-value>5</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>ValidateCodeServlet</servlet-name>
<url-pattern>/captcha.jpg</url-pattern>
</servlet-mapping>
<img id="captchaImg" src="/captcha.jpg" alt="驗證碼" style="cursor:pointer; vertical-align:middle;" />
<a href="javascript:void(0);" onclick="refreshCaptcha()">看不清?換一張</a>
<input type="text" name="captchaInput" maxlength="5" placeholder="請輸入驗證碼" />
<script>
function refreshCaptcha() {
var img = document.getElementById('captchaImg');
img.src = '/captcha.jpg?' + new Date().getTime(); // 附加時間戳防瀏覽器緩存
}
</script>
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
@RequestParam String captchaInput,
HttpSession session) {
String realCode = (String) session.getAttribute("validateCode");
if (realCode == null || !realCode.equalsIgnoreCase(captchaInput.trim())) {
model.addAttribute("error", "驗證碼錯誤或已過期");
return "login";
}
session.removeAttribute("validateCode"); // 一次性使用,防重放
// ... 後續業務邏輯
return "redirect:/dashboard";
}
| 加固點 | 實現方案 |
|---|---|
| 字符集優化 | 剔除 0/O, 1/I/l, 2/Z, 5/S, 8/B 等易混淆字符,提升用戶體驗 |
| 干擾噪點 | 在繪製干擾線基礎上,增加 g.fillRect(x, y, 1, 1) 隨機噪點 |
| 字符扭曲/彈性變形 | 使用 AffineTransform 實現字符隨機剪切、拉伸、波浪變形 |
| 中文驗證碼 | 字符集改為常用漢字 3000 字,字體設置為「微軟雅黑」或「思源黑體」 |
| 分布式 Session | 集群環境下需將驗證碼存入 Redis(Key: captcha:{sessionId}, TTL: 5min) |
| 限流防暴力 | 同一 IP 1 分鐘內請求驗證碼圖片超過 20 次觸發限流(429 Too Many Requests) |
通過 BufferedImage + Graphics2D 繪製圖形驗證碼是 Java Web 經典且輕量的方案。關鍵在於:字符隨機性、視覺干擾強度、Session 綁定生命週期、前端刷新機制、後端一次性校驗銷毀 五大環節缺一不可。生產環境建議結合 Redis 集中存儲、IP 限流、字符扭曲算法,構建多層次防禦體系。若業務對安全性要求極高,可考慮接入成熟的第三方驗證碼服務(如 Google reCAPTCHA v3、Geetest 極驗),實現無感驗證與行為分析結合。