驗證碼核心需求
- **機器不可識別**:干擾線、噪點、字體扭曲、顏色隨機
- **人類可識別**:字符清晰、對比度高、尺寸適中
- **一次性使用**:生成 → Session 存儲 → 驗證即銷毀
- **高性能**:無磁盤 I/O,直接輸出流
核心工具類:VerifyCodeUtils
public class VerifyCodeUtils {
// 字符集:去除易混淆 0/O、1/I/l、2/Z、8/B 等
private static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static final Random RANDOM = new Random();
// 1. 生成隨機字符串
public static String generateVerifyCode(int length) {
return generateVerifyCode(length, VERIFY_CODES);
}
public static String generateVerifyCode(int length, String sources) {
if (sources == null || sources.isEmpty()) sources = VERIFY_CODES;
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(sources.charAt(RANDOM.nextInt(sources.length())));
}
return sb.toString();
}
// 2. 輸出圖片到 OutputStream (核心方法)
public static void outputImage(int width, int height, OutputStream os, String code)
throws IOException {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = image.createGraphics();
// 抗鋸齒
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 隨機背景色
Color[] colors = randomColors(5);
float[] fractions = randomFractions(5);
Arrays.sort(fractions);
// 邊框
g2.setColor(Color.GRAY);
g2.fillRect(0, 0, width, height);
// 背景漸變
Color bg = getRandomColor(200, 255);
g2.setColor(bg);
g2.fillRect(0, 2, width, height - 4);
// 3. 干擾線
g2.setColor(getRandomColor(50, 255));
for (int i = 0; i < 20; i++) {
int x = RANDOM.nextInt(width - 1);
int y = RANDOM.nextInt(height - 1);
int xl = RANDOM.nextInt(6) + 1;
int yl = RANDOM.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 4. 噪點
float yawpRate = 0.05f;
int area = (int) (yawpRate * width * height);
for (int i = 0; i < area; i++) {
int x = RANDOM.nextInt(width);
int y = RANDOM.nextInt(height);
image.setRGB(x, y, getRandomIntColor());
}
// 5. 扭曲變換 (核心反識別)
shear(g2, width, height, bg);
// 6. 繪制字符
int fontSize = height - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize); // 需系統安裝
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < chars.length; i++) {
AffineTransform affine = new AffineTransform();
// 隨機旋轉 ±45°
double angle = Math.PI / 4 * RANDOM.nextDouble() * (RANDOM.nextBoolean() ? 1 : -1);
affine.setToRotation(angle,
(width / chars.length) * i + fontSize / 2, height / 2);
g2.setTransform(affine);
g2.setColor(getRandomColor(100, 160));
g2.drawChars(chars, i, 1,
((width - 10) / chars.length) * i + 5,
height - (height - (int)(fontSize * 0.75)) / 2);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
// 輔助方法:隨機顏色、剪切變換等 (見完整代碼)
private static Color getRandomColor(int fc, int bc) { /* ... */ }
private static int getRandomIntColor() { /* ... */ }
private static void shear(Graphics g, int w, int h, Color color) { /* ... */ }
private static void shearX(Graphics g, int w, int h, Color color) { /* ... */ }
private static void shearY(Graphics g, int w, int h, Color color) { /* ... */ }
}
關鍵反識別技術解析
| 技術 | 實現 | 效果 |
| 字符集過濾 | 移除 0/O、1/I/l、2/Z、5/S、8/B、U/V | 消除視覺歧義 |
| 隨機字體 | Algerian 等藝術字體 + 斜體 | 破壞標準字符特徵 |
| 仿射變換 | `AffineTransform` 隨機旋轉 ±45° | 字符傾斜、位移,OCR 難分割 |
| 剪切扭曲 | `shearX/shearY` 正弦波形平移 | 整體圖像波浪扭曲,連通域破壞 |
| 干擾線 | 隨機顏色、位置、長度直線 | 干擾字符邊緣檢測 |
| 噪點 | 5% 密度隨機像素 | 增加背景複雜度 |
| 顏色隨機 | 字符、背景、干擾線顏色均隨機 | 無固定顏色特徵可利用 |
Spring MVC Controller 整合
@Controller
public class VerifyCodeController {
@GetMapping("/verifyCode")
public void verifyCode(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. 生成驗證碼
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
// 3. 存入 Session (轉小寫不區分大小寫)
HttpSession session = request.getSession(true);
session.setAttribute("verifyCode", verifyCode.toLowerCase());
// 4. 輸出圖片流
VerifyCodeUtils.outputImage(200, 80, response.getOutputStream(), verifyCode);
}
}
前端使用
<img id="verifyCodeImg" src="/verifyCode" alt="點擊刷新"
onclick="this.src='/verifyCode?t=' + Date.now()" style="cursor:pointer;" />
<input type="text" name="verifyCode" placeholder="請輸入驗證碼" />
驗證邏輯 (登錄/表單提交)
@PostMapping("/login")
public Result login(@RequestParam String username,
@RequestParam String password,
@RequestParam String verifyCode,
HttpSession session) {
String savedCode = (String) session.getAttribute("verifyCode");
if (savedCode == null || !savedCode.equalsIgnoreCase(verifyCode)) {
return Result.error("驗證碼錯誤或已過期");
}
// 驗證通過,立即移除防止重用
session.removeAttribute("verifyCode");
// 後續業務邏輯...
return Result.success();
}
生產環境優化建議
- 字體備選:系統無 Algerian 時回退 `Arial`、`Verdana`,或項目打包 `.ttf` 字體文件動態加載
- 性能優化:`BufferedImage` 對象池化、異步生成、CDN 緩存靜態資源
- 安全加固:驗證碼加密存儲 (AES)、限制驗證次數 (防暴力)、圖片加水印標識來源
- 無障礙支持:提供語音驗證碼選項、高對比度模式
- 現代替代:行為驗證 (滑塊、點選)、設備指紋、風控引擎 (Google reCAPTCHA v3、阿里云驗證碼)