驗證碼是 Web 應用防機器人核心機制。本文提供生產級驗證碼生成工具類,支援字元混合、隨機字體、顏色干擾、干擾線、Session 儲存,並示範 Servlet/JSP/JS 整合部署。
public class ValidCodeUtils {
// 尺寸配置
public static int WIDTH = 60;
public static int HEIGHT = 20;
// 顏色範圍 (RGB 0-255) - 背景淺、干擾中、前景深
public static int COLOR_FC_BG = 200, COLOR_BC_BG = 250; // 背景
public static int COLOR_FC_LINE = 160, COLOR_BC_LINE = 200; // 干擾線
public static int COLOR_FC_CODE = 20, COLOR_BC_CODE = 170; // 驗證碼文字
// 隨機顏色生成器
private static Color getRandColor(int fc, int bc) {
Random random = new Random();
fc = Math.max(0, Math.min(255, fc));
bc = Math.max(0, Math.min(255, bc));
if (bc == fc) bc += 10;
if (bc < fc) { int t = fc; fc = bc; bc = t; }
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
// 隨機字元: 大寫/小寫/數字 各 1/3 機率
public static String getRandomChar() {
int type = (int) Math.round(Math.random() * 2); // 0,1,2
switch (type) {
case 0: return String.valueOf((char) (Math.round(Math.random() * 25) + 65)); // A-Z
case 1: return String.valueOf((char) (Math.round(Math.random() * 25) + 97)); // a-z
default: return String.valueOf(Math.round(Math.random() * 9)); // 0-9
}
}
// 隨機字體: 字體/大小雙重隨機
public static Font getRandomFont() {
String[] fonts = {"Georgia", "Verdana", "Arial", "Tahoma", "Times New Roman",
"Courier New", "Arial Black", "Quantzite"};
int fontIndex = (int) Math.round(Math.random() * (fonts.length - 1));
int fontSize = (int) Math.round(Math.random() * 4 + 16); // 16-20px
return new Font(fonts[fontIndex], Font.PLAIN, fontSize);
}
// 核心生成方法: 回傳是否成功寫入輸出流
public static boolean getImage(HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.reset();
response.setContentType("image/jpeg");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
// 1. 填充背景
g.setColor(getRandColor(COLOR_FC_BG, COLOR_BC_BG));
g.fillRect(0, 0, WIDTH, HEIGHT);
// 2. 繪製干擾線 (155 條)
g.setColor(getRandColor(COLOR_FC_LINE, COLOR_BC_LINE));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(WIDTH);
int y = random.nextInt(HEIGHT);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 3. 生成並繪製 4 位驗證碼
StringBuilder codeValue = new StringBuilder();
for (int i = 0; i < 4; i++) {
String rand = getRandomChar();
codeValue.append(rand);
g.setFont(getRandomFont()); // 每字隨機字體
g.setColor(getRandColor(COLOR_FC_CODE, COLOR_BC_CODE));
g.drawString(rand, 13 * i + 6, 16); // 位置微調
}
// 4. 存入 Session
request.getSession().setAttribute("codeValue", codeValue.toString());
// 5. 輸出圖片
g.dispose();
return ImageIO.write(image, "JPEG", response.getOutputStream());
}
}
@WebServlet("/validCodeServlet")
public class ValidCodeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
try {
ValidCodeUtils.getImage(req, resp);
} catch (Exception e) {
e.printStackTrace();
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
<servlet>
<servlet-name>validCodeServlet</servlet-name>
<servlet-class>com.hoo.servlet.ValidCodeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>validCodeServlet</servlet-name>
<url-pattern>/validCodeServlet</url-pattern>
</servlet-mapping>
<img src="${pageContext.request.contextPath}/validCodeServlet"
title="看不清,點擊刷新"
onclick="reloadValidCode(this)" />
<script>
function reloadValidCode(img) {
// 加時間戳防止 IE 快取
img.src = "${pageContext.request.contextPath}/validCodeServlet?t=" + new Date().getTime();
}
</script>
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String userInput = req.getParameter("captcha");
String sessionCode = (String) req.getSession().getAttribute("codeValue");
if (sessionCode != null && sessionCode.equalsIgnoreCase(userInput)) {
// 驗證通過
req.getSession().removeAttribute("codeValue"); // 用完即銷毀
// 後續業務邏輯...
} else {
// 驗證失敗
req.setAttribute("error", "驗證碼錯誤");
req.getRequestDispatcher("/login.jsp").forward(req, resp);
}
}
}
| 項目 | 建議 |
|---|---|
| 字元集 | 排除易混淆字元 (0/O, 1/I/l, 2/Z, 5/S, 8/B) → 定義 String CHARS = "23456789ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz" |
| 長度 | 4-6 位平衡安全性與使用者體驗 |
| 干擾強度 | 線條/噪點密度需測試 OCR 識別率,避免過度影響人類辨識 |
| 儲存安全 | Session 側邊,避免 URL 參數、Cookie、隱藏欄位傳遞 |
| 一次性 | 驗證後立即從 Session 移除,防重放攻擊 |
| 無障礙 | 提供語音驗證碼選項、或替代驗證方式 (簡訊、Email) |
| 現代替代 | 考慮 reCAPTCHA v3 (行為分析)、hCaptcha、Turnstile 等無感驗證 |
Random、Font 陣列可設為 static final 避免重複建立response.getOutputStream()此實作提供完整可部署的驗證碼解決方案,可直接整合至傳統 JSP/Servlet 或 Spring MVC 專案中。