4 位隨機數字 + 干擾線 + 隨機顏色 + 隨機字體大小/位置,點擊「看不清」即時刷新無緩存。
@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
private static final int WIDTH = 120;
private static final int HEIGHT = 40;
private static final int CODE_COUNT = 4;
private static final int LINE_COUNT = 8;
private static final String CHARS = "0123456789"; // 可擴展字母
@Override
protected void service(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. 創建畫布
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 3. 繪製背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, WIDTH, HEIGHT);
// 4. 繪製邊框 (可選)
g.setColor(Color.LIGHT_GRAY);
g.drawRect(0, 0, WIDTH - 1, HEIGHT - 1);
// 5. 生成驗證碼並繪製文字
Random random = new Random();
StringBuilder code = new StringBuilder();
Font font = new Font("Consolas", Font.BOLD, 24);
g.setFont(font);
for (int i = 0; i < CODE_COUNT; i++) {
char c = CHARS.charAt(random.nextInt(CHARS.length()));
code.append(c);
// 隨機顏色
g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200)));
// 隨機位置 (x 間距固定,y 隨機)
int x = i * (WIDTH / CODE_COUNT) + 5;
int y = 20 + random.nextInt(15);
g.drawString(String.valueOf(c), x, y);
}
// 6. 繪製干擾線
for (int i = 0; i < LINE_COUNT; i++) {
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
int x1 = random.nextInt(WIDTH);
int y1 = random.nextInt(HEIGHT);
int x2 = random.nextInt(WIDTH);
int y2 = random.nextInt(HEIGHT);
g.drawLine(x1, y1, x2, y2);
}
// 7. 釋放資源
g.dispose();
// 8. 輸出圖片
ImageIO.write(image, "JPEG", resp.getOutputStream());
// 9. 存入 Session 供驗證
req.getSession().setAttribute("captcha_code", code.toString());
}
}
<form action="/login" method="post">
用戶名: <input name="username" /><br/>
密碼: <input type="password" name="password" /><br/>
驗證碼: <input name="captcha" maxlength="4" />
<img id="captchaImg" src="/captcha" style="vertical-align:middle; cursor:pointer;" title="點擊刷新" />
<a href="javascript:void(0)" onclick="refreshCaptcha()">看不清</a><br/>
<button type="submit">登錄</button>
</form>
<script>
function refreshCaptcha() {
var img = document.getElementById('captchaImg');
// 關鍵:加時間戳參數強制瀏覽器重新請求,繞過緩存
img.src = '/captcha?' + Date.now();
}
$('#captchaImg').click(refreshCaptcha); // 點擊圖片也可刷新
</script>
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
@RequestParam String captcha,
HttpSession session,
Model model) {
String realCode = (String) session.getAttribute("captcha_code");
if (realCode == null || !realCode.equalsIgnoreCase(captcha)) {
model.addAttribute("error", "驗證碼錯誤");
return "login";
}
// 清除驗證碼,防止重放
session.removeAttribute("captcha_code");
// 後續業務驗證...
return "redirect:/home";
}
| 需求 | 實現要點 |
|---|---|
| 字母+數字混合 | CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" (去除易混淆字符) |
| 中文驗證碼 | 字庫取常用 3000 字,g.drawString(String.valueOf(ch), x, y) 需確保字體支持中文 |
| 幹擾曲線/噪點 | g.drawArc() 畫貝塞爾曲線;g.fillRect(x,y,1,1) 畫噪點 |
| 扭曲變形 | AffineTransform 仿射變換扭曲文字 |
| GIF 動態碼 | 用 AnimatedGifEncoder 生成多幀 GIF,每幀字符位置微調 |
| 分佈式 Session | 驗證碼存 Redis,Key: captcha:{sessionId},TTL 5 分鐘 |
| 防暴力破解 | 同 IP 5 分鐘錯誤 > 5 次封禁 15 分鐘,需配合限流框架 |
implementation 'com.github.penggle:kaptcha:2.3.2',一行配置即可手寫驗證碼 Servlet 只有 50 行核心代碼,涵蓋 BufferedImage、Graphics2D、ImageIO、Session 管理、Cache-Control 等核心知識點。但生產環境強烈建議接入成熟第三方驗證服務,自研僅適合學習、內部低風險系統或無法聯網環境。