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

Java 驗證碼生成與驗證完整實現:Servlet、Spring MVC、前端交互一體化方案

     編輯:Java 程式設計入門

前言:驗證碼在 Web 安全中的定位

驗證碼是防禦暴力破解、機器人註冊、刷單刷票的第一道防線。本文提供一套基於 Java AWT 繪圖 + Servlet/JSP + Spring MVC + jQuery AJAX 的完整驗證碼解決方案,涵蓋圖片生成、Session 存儲、前端刷新/驗證、無障礙優化、安全加固與現代化替代方案。

一、後端核心:VerifyCodeController (Spring MVC 版)


@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; ... }
}

二、前端頁面:HTML + jQuery 交互


<!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 分鐘

四、分布式環境 Redis 存儲實現


@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 → 根據風險等級決定放行/二次驗證/拒絕。

七、效能優化建議

  • 圖片緩存: 相同驗證碼內容可緩存 BufferedImage(Key: code,Value: byte[]),避免重複繪製。
  • 異步生成: 高併發時用 CompletableFuture 預生成圖片池,請求時直接取用。
  • 對象池: 復用 BufferedImage、Graphics2D、Random 減少 GC。
  • CDN 分發: 靜態資源(字體、背景圖)走 CDN;動態驗證碼圖片不可緩存。

結語

傳統圖片驗證碼雖簡單,但「生成、存儲、驗證、刷新、無障礙、分布式、安全加固」每一環都藏著坑。本文提供的 Spring MVC + Redis + jQuery 方案,代碼結構清晰、安全策略完備、易於擴展。對於新系統,強烈建議優先評估行為驗證/無感驗證服務,將安全風控外包給專業廠商,專注業務創新。無論選哪種方案,後端驗證邏輯不可信任前端、單次使用即銷毀、限流防暴力是鐵律。

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