驗證碼是 Web 應用防機器註冊、暴力破解、刷單的第一道防線。本文完整實現「圖片生成 → Servlet 輸出 → 前端展示 → 點擊刷新」全鏈路,涵蓋 BufferedImage 繪圖 API、Graphics 繪製幹擾線/文字、ImageIO 輸出流、Session 存儲驗證碼值、jQuery 無刷新刷新機制,提供生產可用代碼範本。
package com.util;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
public class ImageCodeUtil {
// 配置常量
private static final int WIDTH = 100;
private static final int HEIGHT = 35;
private static final int CODE_LENGTH = 4;
private static final int INTERFERE_LINES = 30;
private static final String CHAR_POOL = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; // 排除易混淆字符
public static BufferedImage generateImage(StringBuilder codeBuffer) {
// 1. 創建圖片緩衝區 (RGB 模式)
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// 2. 繪製背景
g.setColor(getRandomLightColor());
g.fillRect(0, 0, WIDTH, HEIGHT);
// 3. 繪製幹擾線
Random random = new Random();
for (int i = 0; i < INTERFERE_LINES; i++) {
g.setColor(getRandomColor(160, 200));
int x1 = random.nextInt(WIDTH);
int y1 = random.nextInt(HEIGHT);
int x2 = x1 + random.nextInt(15) - 5;
int y2 = y1 + random.nextInt(15) - 5;
g.drawLine(x1, y1, x2, y2);
}
// 4. 繪製驗證碼字符
g.setFont(new Font("Fixedsys", Font.BOLD, 24)); // 等寬字體,難識別
g.setColor(getRandomDarkColor());
StringBuilder code = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
char c = CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length()));
code.append(c);
// 字符隨機旋轉、位置微調,增加識別難度
int x = 20 * i + 10 + random.nextInt(5);
int y = 24 + random.nextInt(5);
g.drawString(String.valueOf(c), x, y);
}
// 5. 釋放資源
g.dispose();
// 6. 返回驗證碼值 (供 Session 存儲)
if (codeBuffer != null) {
codeBuffer.append(code);
}
return image;
}
// 輔助顏色方法
private static Color getRandomLightColor() {
Random r = new Random();
return new Color(200 + r.nextInt(55), 200 + r.nextInt(55), 200 + r.nextInt(55));
}
private static Color getRandomColor(int min, int max) {
Random r = new Random();
int rVal = min + r.nextInt(max - min);
int gVal = min + r.nextInt(max - min);
int bVal = min + r.nextInt(max - min);
return new Color(rVal, gVal, bVal);
}
private static Color getRandomDarkColor() {
Random r = new Random();
return new Color(r.nextInt(100), r.nextInt(100), r.nextInt(100));
}
}
package com.controller;
import com.util.ImageCodeUtil;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 設置響應頭:禁止緩存、指定圖片類型
resp.setHeader("Pragma", "No-cache");
resp.setHeader("Cache-Control", "no-cache");
resp.setDateHeader("Expires", 0);
resp.setContentType("image/jpeg");
// 2. 生成驗證碼圖片並獲取驗證碼值
StringBuilder codeBuffer = new StringBuilder();
BufferedImage image = ImageCodeUtil.generateImage(codeBuffer);
String captchaCode = codeBuffer.toString();
// 3. 存入 Session (大小寫不敏感驗證建議統一轉大寫)
HttpSession session = req.getSession();
session.setAttribute("captchaCode", captchaCode.toUpperCase());
// 4. 輸出圖片流
ImageIO.write(image, "JPEG", resp.getOutputStream());
}
}
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>登錄頁面</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<style>
.captcha-img { cursor: pointer; vertical-align: middle; border: 1px solid #ddd; }
.captcha-input { width: 80px; margin-left: 5px; vertical-align: middle; }
</style>
</head>
<body>
<form id="loginForm" action="login" method="post">
<div>
<label>用戶名:</label>
<input type="text" name="username" required/>
</div>
<div>
<label>密碼:</label>
<input type="password" name="password" required/>
</div>
<div>
<label>驗證碼:</label>
<input type="text" name="captcha" class="captcha-input" maxlength="4" required/>
<img id="captchaImg" class="captcha-img" src="captcha" alt="點擊刷新驗證碼" title="點擊刷新驗證碼"/>
</div>
<button type="submit">登錄</button>
</form>
<script>
// 點擊圖片刷新驗證碼 (加時間戳防瀏覽器緩存)
$("#captchaImg").click(function() {
$(this).attr("src", "captcha?t=" + new Date().getTime());
});
</script>
</body>
</html>
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
String captchaInput = req.getParameter("captcha");
// 1. 獲取 Session 中驗證碼
HttpSession session = req.getSession(false);
String captchaCode = (session != null) ? (String) session.getAttribute("captchaCode") : null;
// 2. 驗證碼校驗 (不區分大小寫)
if (captchaCode == null || !captchaCode.equalsIgnoreCase(captchaInput)) {
req.setAttribute("error", "驗證碼錯誤或已過期");
req.getRequestDispatcher("login.jsp").forward(req, resp);
return;
}
// 3. 驗證通過,移除驗證碼 (一次性使用)
session.removeAttribute("captchaCode");
// 4. 業務邏輯:用戶名密碼驗證 (此處省略,接入 UserService)
if ("admin".equals(username) && "123456".equals(password)) {
session.setAttribute("loginUser", username);
resp.sendRedirect("index.jsp");
} else {
req.setAttribute("error", "用戶名或密碼錯誤");
req.getRequestDispatcher("login.jsp").forward(req, resp);
}
}
}
| 維度 | 增強措施 |
|---|---|
| 字符池 | 排除 0/O、1/I/l、8/B、5/S 等易混淆字符 |
| 幹擾元素 | 幹擾線 + 幹擾圓點 + 幹擾弧線 + 前景幹擾字符 |
| 字體變形 | 隨機字體、隨機大小、隨機旋轉角度 (-30°~30°)、隨機平移 |
| 顏色空間 | 背景淺色系、前景深色系、幹擾中間色系,確保對比度 |
| 一次性失效 | 驗證後立即 session.removeAttribute(),防止重放攻擊 |
| 頻率限制 | 同 IP/Session 短時間請求驗證碼次數限制 (如 10次/分鐘) |
| 有效期控制 | Session 設置 maxInactiveInterval (如 5 分鐘) 或單獨存儲時間戳校驗 |
BufferedImage/Graphics 對象池化,減少 GC 壓力Content-Length、ETag、Cache-Control: no-store, must-revalidate

驗證碼生成雖屬基礎功能,但涉及「圖形學繪製、Web 流輸出、Session 狀態管理、前端交互、安全對抗」多維技能。掌握 BufferedImage + Graphics 核心繪圖鏈路,理解 ImageIO.write 輸出機制,構建完整「生成→輸出→驗證→失效」閉環,並針對機器識別攻擊持續迭代幹擾策略,是構建安全 Web 應用的必修課。建議團隊封裝通用 CaptchaService,支持多種驗證碼類型 (圖片/滑動/點選/無感),統一接入網關層,實現「業務零感知安全防護」。