首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 驗證碼生成實戰:支持中英數混合、隨機字體、干擾線、Session 綁定的完整方案

     編輯:Java 程式設計入門

前言

驗證碼是 Web 應用防暴力破解、機器註冊、刷單的第一道防線。本文提供一個開箱即用的 ValidCodeUtils 工具類,涵蓋:可配置寬高/顏色範圍、中英數混合字符池、每字符隨機字體/大小/傾斜、貝塞爾曲線/直線干擾、Servlet 輸出流輸出、Session 綁定驗證、前端點擊刷新防緩存機制,並給出生產環境加固建議。

一、核心工具類設計

package com.example.captcha;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

public final class ValidCodeUtils {

    // ========== 可配置常量 ==========
    public static int WIDTH = 130;
    public static int HEIGHT = 48;
    public static int CODE_LENGTH = 4;

    // 顏色範圍:背景淡、干擾中、前景深
    private static final int[] BG_RANGE = {200, 250};
    private static final int[] LINE_RANGE = {160, 200};
    private static final int[] CODE_RANGE = {20, 130};

    // 字符池:去除易混淆字符 (0/O, 1/I/l, 2/Z, 5/S, 8/B)
    private static final String CHAR_POOL =
        "ABCDEFGHJKLMNPQRSTUVWXYZ" + // 大寫 24
        "abcdefghjkmnpqrstuvwxyz" +   // 小寫 24
        "23456789";                   // 數字 8

    private static final String[] FONTS = {
        "Arial", "Verdana", "Tahoma", "Georgia", "Courier New",
        "Microsoft YaHei", "SimHei", "KaiTi"
    };

    private static final Random RANDOM = new Random();

    // ========== 核心生成方法 ==========
    public static boolean generate(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.reset();
        resp.setContentType("image/jpeg");
        resp.setHeader("Pragma", "No-cache");
        resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        resp.setDateHeader("Expires", 0);

        BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();

        // 1. 背景填充
        g.setColor(randomColor(BG_RANGE));
        g.fillRect(0, 0, WIDTH, HEIGHT);

        // 2. 干擾線(直線 + 貝塞爾曲線)
        g.setColor(randomColor(LINE_RANGE));
        for (int i = 0; i < 20; i++) {
            drawRandomLine(g);
        }
        for (int i = 0; i < 5; i++) {
            drawBezierCurve(g);
        }

        // 3. 干擾噪點
        g.setColor(randomColor(LINE_RANGE));
        for (int i = 0; i < 30; i++) {
            int x = RANDOM.nextInt(WIDTH);
            int y = RANDOM.nextInt(HEIGHT);
            g.drawOval(x, y, 1, 1);
        }

        // 4. 繪製驗證碼字符
        StringBuilder code = new StringBuilder();
        FontMetrics fm = g.getFontMetrics();
        int charWidth = (WIDTH - 20) / CODE_LENGTH;
        for (int i = 0; i < CODE_LENGTH; i++) {
            char ch = randomChar();
            code.append(ch);

            Font font = randomFont();
            g.setFont(font);
            g.setColor(randomColor(CODE_RANGE));

            // 隨機旋轉角度 ±25°
            AffineTransform orig = g.getTransform();
            g.rotate(Math.toRadians(RANDOM.nextInt(50) - 25),
                     15 + i * charWidth + charWidth / 2, HEIGHT / 2);
            g.drawString(String.valueOf(ch), 15 + i * charWidth, HEIGHT / 2 + fm.getAscent() / 2 - 2);
            g.setTransform(orig);
        }

        // 5. 存入 Session(建議加上時間戳防重放)
        req.getSession().setAttribute("captcha_code", code.toString().toLowerCase());
        req.getSession().setAttribute("captcha_time", System.currentTimeMillis());

        g.dispose();
        return ImageIO.write(image, "JPEG", resp.getOutputStream());
    }

    // ========== 輔助方法 ==========
    private static Color randomColor(int[] range) {
        int fc = Math.max(0, range[0]);
        int bc = Math.min(255, range[1]);
        if (fc > bc) { int t = fc; fc = bc; bc = t; }
        int r = fc + RANDOM.nextInt(bc - fc + 1);
        int g = fc + RANDOM.nextInt(bc - fc + 1);
        int b = fc + RANDOM.nextInt(bc - fc + 1);
        return new Color(r, g, b);
    }

    private static char randomChar() {
        return CHAR_POOL.charAt(RANDOM.nextInt(CHAR_POOL.length()));
    }

    private static Font randomFont() {
        String name = FONTS[RANDOM.nextInt(FONTS.length)];
        int style = RANDOM.nextBoolean() ? Font.BOLD : Font.PLAIN;
        int size = 28 + RANDOM.nextInt(8); // 28~35
        return new Font(name, style, size);
    }

    private static void drawRandomLine(Graphics2D g) {
        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);
    }

    private static void drawBezierCurve(Graphics2D g) {
        int x1 = RANDOM.nextInt(WIDTH);
        int y1 = RANDOM.nextInt(HEIGHT);
        int x2 = RANDOM.nextInt(WIDTH);
        int y2 = RANDOM.nextInt(HEIGHT);
        int ctrlX = RANDOM.nextInt(WIDTH);
        int ctrlY = RANDOM.nextInt(HEIGHT);
        // 簡易二次貝塞爾:分段畫線
        for (double t = 0; t <= 1; t += 0.05) {
            double u = 1 - t;
            int x = (int) (u * u * x1 + 2 * u * t * ctrlX + t * t * x2);
            int y = (int) (u * u * y1 + 2 * u * t * ctrlY + t * t * y2);
            if (t > 0) g.drawLine(prevX, prevY, x, y);
            prevX = x; prevY = y;
        }
    }
    private static int prevX, prevY; // 用於貝塞爾連線
}

二、Servlet 入口

@WebServlet("/captcha.jpg")
public class CaptchaServlet extends HttpServlet {
    @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        ValidCodeUtils.generate(req, resp);
    }
    @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doGet(req, resp);
    }
}

三、前端整合與防緩存

<!-- JSP/Thymeleaf/Vue 通用 -->
<img id="captchaImg" src="${ctx}/captcha.jpg" style="cursor:pointer;vertical-align:middle" 
     onclick="refreshCaptcha()" title="點擊刷新" />
<input type="text" name="captcha" placeholder="輸入驗證碼" />

<script>
function refreshCaptcha() {
    var img = document.getElementById('captchaImg');
    img.src = '${ctx}/captcha.jpg?t=' + Date.now(); // 時間戳防瀏覽器緩存
}
</script>

四、後端驗證邏輯

@PostMapping("/login")
public Result login(@RequestParam String username,
                    @RequestParam String password,
                    @RequestParam String captcha,
                    HttpSession session) {

    String realCode = (String) session.getAttribute("captcha_code");
    Long genTime = (Long) session.getAttribute("captcha_time");

    // 1. 空值檢查
    if (realCode == null || captcha == null) {
        return Result.error("驗證碼已失效,請刷新");
    }
    // 2. 時效性檢查(如 5 分鐘)
    if (System.currentTimeMillis() - genTime > 5 * 60 * 1000) {
        session.removeAttribute("captcha_code");
        return Result.error("驗證碼已過期,請刷新");
    }
    // 3. 大小寫不敏感比對
    if (!realCode.equalsIgnoreCase(captcha.trim())) {
        return Result.error("驗證碼錯誤");
    }
    // 4. 驗證通過後立即清除,防重放
    session.removeAttribute("captcha_code");
    session.removeAttribute("captcha_time");

    // 後續業務邏輯...
    return Result.ok();
}

五、生產環境加固建議

風險點對策
驗證碼被 OCR 識別增加貝塞爾曲線、扭曲、噪點、字符粘連;或接入 kaptcha / EasyCaptcha 成熟庫
Session 洩漏/共享導致驗證碼被盜用分佈式 Session (Redis) + 短過期時間 + 一次性驗證即銷毀
暴力破解驗證碼接口同 IP 限流(如 10 次/分鐘)、接入行為驗證(滑塊/點選/無感)
高併發下圖片生成 CPU 瓶頸預先生成圖片池、或使用純前端 Canvas 生成(需防逆向)
字體在 Linux 容器缺失導致方框基礎鏡像安裝 fontconfig dejavu-sans-fonts wqy-zenhei;或打包 .ttf 字體文件隨應用加載

六、進階替代方案

  • 行為驗證(無感/滑塊/點選):Geetest、NetEase Captcha、阿里云驗證碼,體驗更好、安全性更高。
  • hCaptcha / reCAPTCHA v3:零交互風險評分,適合全球化業務。
  • JWT 攜帶驗證碼簽名:無狀態驗證,減輕 Session 壓力。

七、總結

自研驗證碼適合中小型項目、內網系統、學習理解原理。核心要點:字符池去混淆、顏色範圍分層、每字符隨機字體/旋轉、干擾線曲線化、Session 綁定時效性、前端時間戳防緩存、驗證即銷毀。生產環境高安全場景強烈建議接入成熟的行為驗證服務,將安全對抗交給專業團隊。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved