驗證碼是防禦暴力破解、機器人註冊、刷單刷票的第一道防線。本文提供一套基於 Java AWT 繪圖 + Servlet/JSP + Spring MVC + jQuery AJAX 的完整驗證碼解決方案,涵蓋圖片生成、Session 存儲、前端刷新/驗證、無障礙優化、安全加固與現代化替代方案。
@Controller
@RequestMapping("/captcha")
public class VerifyCodeController {
// 配置參數
private int width = 120;
private int height = 40;
private int codeCount = 4;
private int lineCount = 20;
private String sessionKey = "validateCode";
@GetMapping("/image")
public void generateImage(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// 1. 禁止緩存
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
// 2. 創建畫布
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 3. 背景填充(淺色隨機噪點)
g.setColor(getRandomColor(200, 250));
g.fillRect(0, 0, width, height);
drawNoise(g, 0.02f); // 2% 噪點
// 4. 干擾線
g.setColor(getRandomColor(160, 200));
for (int i = 0; i < lineCount; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(15);
int yl = random.nextInt(15);
g.drawLine(x, y, x + xl, y + yl);
}
// 5. 繪製驗證碼字符
StringBuilder code = new StringBuilder();
int fontHeight = height - 6;
int x = (width - fontHeight * codeCount) / (codeCount + 1);
g.setFont(new Font("Fixedsys", Font.BOLD, fontHeight));
for (int i = 0; i < codeCount; i++) {
char c = randomChar();
code.append(c);
g.setColor(new Color(random.nextInt(100), random.nextInt(100), random.nextInt(100)));
g.drawString(String.valueOf(c), (i + 1) * x, height - 6);
}
// 6. 存入 Session(大小寫不敏感驗證建議統一轉大寫)
HttpSession session = request.getSession();
session.setAttribute(sessionKey, code.toString().toUpperCase());
// 7. 輸出圖片
ImageIO.write(image, "JPEG", response.getOutputStream());
g.dispose();
}
@PostMapping("/validate")
@ResponseBody
public Result validate(@RequestParam String code, HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) return Result.fail("會話過期");
String realCode = (String) session.getAttribute(sessionKey);
if (realCode == null) return Result.fail("驗證碼已失效,請刷新");
// 移除防止重放攻擊
session.removeAttribute(sessionKey);
if (code == null || code.trim().isEmpty()) {
return Result.fail("請輸入驗證碼");
}
if (realCode.equalsIgnoreCase(code.trim())) {
return Result.ok("驗證通過");
} else {
return Result.fail("驗證碼錯誤");
}
}
// --- 工具方法 ---
private final Random random = new Random();
private final char[] chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray(); // 排除易混淆字符
private char randomChar() { return chars[random.nextInt(chars.length)]; }
private Color getRandomColor(int min, int max) {
return new Color(random.nextInt(max - min) + min,
random.nextInt(max - min) + min,
random.nextInt(max - min) + min);
}
private void drawNoise(Graphics2D g, float density) {
int area = width * height;
int count = (int) (area * density);
for (int i = 0; i < count; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
g.drawRect(x, y, 1, 1);
}
}
// 統一返回格式
static class Result { boolean success; String msg; ... }
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登錄驗證碼演示</title>
<style>
.captcha-img { cursor: pointer; vertical-align: middle; border: 1px solid #ddd; }
.captcha-input { width: 100px; padding: 5px; }
.msg { margin-left: 10px; color: #666; }
.success { color: green; }
.error { color: red; }
</style>
</head>
<body>
<h3>用戶登錄</h3>
<form id="loginForm">
<div>
用戶名: <input name="username" required>
</div>
<div>
密碼: <input name="password" type="password" required>
</div>
<div>
驗證碼:
<input id="captchaInput" name="captcha" class="captcha-input" maxlength="4" required autocomplete="off">
<img id="captchaImg" class="captcha-img" src="/captcha/image" alt="點擊刷新驗證碼" title="點擊刷新">
<span class="msg" id="captchaMsg"></span>
</div>
<button type="submit">登錄</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script>
$(function() {
// 1. 點擊圖片刷新
$('#captchaImg').on('click', function() {
refreshCaptcha();
});
// 2. 輸入框失焦自動驗證(可選)
$('#captchaInput').on('blur', function() {
var code = $(this).val().trim();
if (code.length === 4) validateCaptcha(code);
});
// 3. 表單提交前驗證
$('#loginForm').on('submit', function(e) {
var code = $('#captchaInput').val().trim();
if (!code) {
showMsg('請輸入驗證碼', 'error');
e.preventDefault();
return;
}
// 同步驗證(生產建議後端統一驗證)
$.ajax({
url: '/captcha/validate',
type: 'POST',
data: { code: code },
async: false, // 同步阻塞
success: function(res) {
if (!res.success) {
showMsg(res.msg, 'error');
refreshCaptcha();
e.preventDefault();
}
}
});
});
function refreshCaptcha() {
// 加時間戳防止瀏覽器緩存
$('#captchaImg').attr('src', '/captcha/image?t=' + Date.now());
$('#captchaMsg').text('').removeClass('success error');
$('#captchaInput').val('').focus();
}
function validateCaptcha(code) {
$.post('/captcha/validate', { code: code }, function(res) {
showMsg(res.msg, res.success ? 'success' : 'error');
if (!res.success) refreshCaptcha();
});
}
function showMsg(text, type) {
$('#captchaMsg').text(text).removeClass('success error').addClass(type);
}
});
</script>
</body>
</html>
| 風險 | 對策 |
|---|---|
| 驗證碼可重複使用 (Replay Attack) | 驗證成功後立即 session.removeAttribute(key);或用 Redis 存儲並設置 TTL + 單次驗證刪除 |
| 暴力破解 (枚舉 4 位數字 10000 次) | 同 IP/Session 限制驗證次數(如 5 次/分鐘),超限鎖定/要求圖形驗證/短信驗證 |
| OCR 識別破解 | 增加干擾線、噪點、字符扭曲、隨機字體/大小/顏色/旋轉;或遷移至行為驗證(滑塊、點選) |
| Session Fixation / CSRF | 登錄成功重新生成 Session ID;表單包含 CSRF Token |
| 繞過前端直接調用驗證接口 | 後端驗證邏輯不可依賴前端攔截;驗證接口同樣需限流 |
| 分布式環境 Session 不共享 | 使用 Redis 集中存儲驗證碼,Key: captcha:{sessionId},Value: code,TTL 5 分鐘 |
@Service
public class CaptchaService {
@Autowired StringRedisTemplate redisTemplate;
public void save(String sessionId, String code) {
redisTemplate.opsForValue().set("captcha:" + sessionId, code.toUpperCase(), 5, TimeUnit.MINUTES);
}
public boolean validateAndRemove(String sessionId, String input) {
String key = "captcha:" + sessionId;
String real = redisTemplate.opsForValue().getAndDelete(key); // 獲取並刪除,原子操作
return real != null && real.equalsIgnoreCase(input.trim());
}
}
為視障用戶提供語音驗證碼:
@GetMapping("/audio")
public void generateAudio(HttpServletRequest request, HttpServletResponse response) {
String code = (String) request.getSession().getAttribute("validateCode");
if (code == null) return;
// 使用 FreeTTS 或 Google TTS API 生成語音流
// response.setContentType("audio/wav");
// audioSystem.write(audioData, AudioFileFormat.Type.WAVE, response.getOutputStream());
}
前端提供「聽取驗證碼」按鈕播放 /captcha/audio。
| 方案 | 原理 | 優勢 | 代表服務 |
|---|---|---|---|
| 滑塊拼圖 | 軌跡分析、設備指紋 | 用戶體驗好、機器成本高 | Geetest、NetEase Captcha |
| 點選文字/圖標 | 點擊座標隨機性分析 | 移動端友好 | Tencent TCaptcha |
| 無感驗證 (Invisible) | 行為建模、風控引擎 | 零交互、精準攔截 | Google reCAPTCHA v3、阿里云風控 |
| 設備指紋 + 風控 | Canvas/電池/字體/聲音指紋 | 靜默識別模擬器/腳本 | 自研/第三方 SDK |
集成方式:引入 JS SDK → 後端驗證 Token → 根據風險等級決定放行/二次驗證/拒絕。
CompletableFuture 預生成圖片池,請求時直接取用。BufferedImage、Graphics2D、Random 減少 GC。傳統圖片驗證碼雖簡單,但「生成、存儲、驗證、刷新、無障礙、分布式、安全加固」每一環都藏著坑。本文提供的 Spring MVC + Redis + jQuery 方案,代碼結構清晰、安全策略完備、易於擴展。對於新系統,強烈建議優先評估行為驗證/無感驗證服務,將安全風控外包給專業廠商,專注業務創新。無論選哪種方案,後端驗證邏輯不可信任前端、單次使用即銷毀、限流防暴力是鐵律。