驗證碼是Web表單防暴力破解、防機器註冊的第一道防線。本文提供基於Servlet生成圖片驗證碼、JSP頁面顯示與點擊刷新、服務端Session驗證的完整鏈路代碼(四位字母數字混合、干擾線、抗OCR設計)。
ValidateCodeServlet)package com.example.web;
import javax.servlet.*;
import javax.servlet.http.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import javax.imageio.ImageIO;
public class ValidateCodeServlet extends HttpServlet {
private static final int WIDTH = 60, HEIGHT = 20, CODE_LENGTH = 4;
private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 排除易混淆字符
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 關鍵響應頭:禁用緩存,確保每次請求獲取新圖片
resp.setContentType("image/jpeg");
resp.setHeader("Pragma", "No-cache");
resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
resp.setDateHeader("Expires", 0);
HttpSession session = req.getSession();
// 1. 創建內存圖片
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
// 2. 繪製背景
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, WIDTH, HEIGHT);
// 3. 設置字體
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
// 4. 繪製干擾線(155條)
g.setColor(getRandColor(160, 200));
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);
}
// 5. 生成並繪製驗證碼
StringBuilder code = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
char c = CHARS.charAt(random.nextInt(CHARS.length()));
code.append(c);
g.setColor(getRandColor(20, 130));
g.drawString(String.valueOf(c), 13 * i + 6, 16);
}
// 6. 存入Session供後續驗證
session.setAttribute("validateCode", code.toString().toUpperCase());
// 7. 輸出圖片
g.dispose();
ImageIO.write(image, "JPEG", resp.getOutputStream());
}
// 顏色工具方法
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) fc = 255;
if (bc > 255) bc = 255;
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);
}
}
<servlet>
<servlet-name>ValidateCodeServlet</servlet-name>
<servlet-class>com.example.web.ValidateCodeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ValidateCodeServlet</servlet-name>
<url-pattern>/validateCode.jpg</url-pattern> <!-- 偽裝為jpg,利於繞過某些攔截 -->
</servlet-mapping>
<%@ page contentType="text/html;charset=UTF-8" %>
<html><head>
<script>
// 點擊圖片/換一張鏈接刷新驗證碼
function refreshCaptcha() {
// 關鍵:添加隨機參數防止瀏覽器緩存圖片
document.getElementById('captchaImg').src = '/validateCode.jpg?' + Math.random();
}
</script>
</head><body>
<form action="submit.jsp" method="post">
用戶名:<input name="username"><br>
驗證碼:<input name="captcha" maxlength="4">
<img id="captchaImg" src="/validateCode.jpg" alt="點擊刷新" style="cursor:pointer; vertical-align:middle;" onclick="refreshCaptcha()">
<a href="javascript:refreshCaptcha()">看不清?換一張</a><br>
<input type="submit" value="提交">
</form>
</body></html>
submit.jsp或Controller)<%
String inputCode = request.getParameter("captcha");
String sessionCode = (String) session.getAttribute("validateCode");
// 驗證:忽略大小寫,驗證後立即移除防重放
boolean valid = (inputCode != null && sessionCode != null &&
inputCode.trim().toUpperCase().equals(sessionCode));
session.removeAttribute("validateCode"); // 單次使用
if (!valid) {
out.println("驗證碼錯誤!<a href='javascript:history.back()'>返回</a>");
return;
}
// 通過,執行業務邏輯...
%>
| 風險 | 對策 |
|---|---|
| 暴力破解(無限嘗試) | 同IP/同Session限制嘗試次數(如5次鎖定15分鐘),Redis計數器實現 |
| 驗證碼重放攻擊 | 驗證成功立即session.removeAttribute(),單次使用 |
| OCR識別破解 | 增加干擾線密度、字符扭曲、噪點、背景幹擾;或升級為行為驗證(滑塊/點選/無感) |
| CSRF繞過 | 表單攜帶CSRF Token,驗證碼驗證前先校驗Token |
| 圖片緩存導致不刷新 | URL添加隨機參數?t=timestamp,響應頭Cache-Control: no-store |