核心 API:java.awt 繪圖
BufferedImage:記憶體中圖片緩衝區
Graphics2D:繪圖上下文,支援抗鋸齒、旋轉、縮放
ImageIO:編碼輸出(JPEG/PNG/GIF)
階段一:最簡單驗證碼(固定文字輸出檔案)
BufferedImage img = new BufferedImage(60, 30, BufferedImage.TYPE_INT_RGB);
Graphics g = img.getGraphics();
g.drawString("Hello", 10, 20);
g.dispose();
ImageIO.write(img, "JPG", new FileOutputStream("a.jpg"));
階段二:加入干擾線、隨機顏色、隨機位置
int w = 80, h = 40;
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics g = img.getGraphics();
// 背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, w, h);
// 字體
g.setFont(new Font("微軟雅黑", Font.BOLD, 18));
Random r = new Random();
for (int i = 0; i < 4; i++) {
int digit = r.nextInt(10);
int y = 10 + r.nextInt(20);
g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
g.drawString(String.valueOf(digit), 5 + i * w / 4, y);
}
// 干擾線
for (int i = 0; i < 10; i++) {
g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
g.drawLine(r.nextInt(w), r.nextInt(h), r.nextInt(w), r.nextInt(h));
}
g.dispose();
ImageIO.write(img, "JPG", new FileOutputStream("b.jpg"));
階段三:旋轉與縮放(更強抗識別)
Graphics2D g2d = (Graphics2D) img.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (int i = 0; i < 4; i++) {
String ch = String.valueOf(r.nextInt(10));
AffineTransform tx = new AffineTransform();
tx.rotate(r.nextDouble() * 0.5 - 0.25, 5 + i * 15, h - 5); // ±14° 隨機旋轉
// tx.scale(0.8 + r.nextDouble() * 0.4, 0.8 + r.nextDouble() * 0.4); // 隨機縮放
g2d.setTransform(tx);
g2d.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
g2d.drawString(ch, 2 + i * w / 4, h - 13);
}
階段四:Servlet 輸出圖片流(核心)
@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
private static final int W = 100, H = 40, LEN = 4;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("image/jpeg");
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Cache-Control", "no-store, no-cache");
resp.setDateHeader("Expires", 0);
BufferedImage img = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = img.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 背景漸變(可選)
g2d.setPaint(new GradientPaint(0, 0, Color.WHITE, W, H, new Color(230, 240, 255)));
g2d.fillRect(0, 0, W, H);
Random r = new SecureRandom(); // 加密級隨機
StringBuilder code = new StringBuilder();
for (int i = 0; i < LEN; i++) {
int idx = r.nextInt(36); // 0-9, a-z
char ch = (idx < 10) ? (char)('0' + idx) : (char)('a' + idx - 10);
code.append(ch);
AffineTransform tx = new AffineTransform();
tx.rotate(r.nextDouble() * 0.6 - 0.3, 10 + i * 22, H - 8);
g2d.setTransform(tx);
g2d.setFont(new Font("Consolas", Font.BOLD, 24));
g2d.setColor(new Color(r.nextInt(100), r.nextInt(100), r.nextInt(100)));
g2d.drawString(String.valueOf(ch), 10 + i * 22, H - 8);
}
// 干擾線
for (int i = 0; i < 8; i++) {
g2d.setColor(new Color(r.nextInt(200), r.nextInt(200), r.nextInt(200)));
g2d.drawLine(r.nextInt(W), r.nextInt(H), r.nextInt(W), r.nextInt(H));
}
g2d.dispose();
// 存入 Session(驗證時取出比對,忽略大小寫)
req.getSession().setAttribute("captcha_code", code.toString().toLowerCase());
ImageIO.write(img, "JPEG", resp.getOutputStream());
}
}
階段五:前端整合與「看不清換一張」
<img id="captchaImg" src="/captcha" alt="驗證碼" style="vertical-align: middle;" />
<a href="javascript:void(0)" onclick="refreshCaptcha()">看不清,換一張</a>
<input type="text" name="captcha" placeholder="請輸入驗證碼" />
<script>
function refreshCaptcha() {
var img = document.getElementById('captchaImg');
img.src = '/captcha?' + Date.now(); // 時間戳防瀏覽器緩存
}
</script>
階段六:後端驗證
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
@RequestParam String captcha,
HttpSession session) {
String realCode = (String) session.getAttribute("captcha_code");
if (realCode == null || !realCode.equals(captcha.toLowerCase())) {
return "redirect:/login?error=captcha";
}
// ... 正常登入邏輯
session.removeAttribute("captcha_code"); // 用一次即失效
return "redirect:/home";
}
進階強化方向
- 字符集擴充:排除易混淆字符(0/O、1/I/l、8/B),或支援中文成語驗證碼。
- 幹擾曲線:用
QuadCurve2D 畫貝茲曲線,比直線更難識別。
- 噪點:隨機畫 1-2 像素圓點。
- 點擊/滑動/拼圖驗證:前端 Canvas + 後端軌跡分析,體驗更好、安全性更高(如 Geetest、reCAPTCHA v3)。
- 分散式 Session:集群環境用 Redis 存驗證碼(Key:
captcha:{sessionId},TTL 5 分鐘)。
安全提醒
- 驗證碼必須在伺服端生成並驗證,前端不可信。
- 使用
SecureRandom 而非 Random。
- 設置
Cache-Control: no-store 防止代理緩存。
- 驗證後立即從 Session/Redis 刪除,防止重放攻擊。
- 登入介面需配合頻率限制(IP/賬號),單獨驗證碼無法防暴力破解。