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

Java 圖形驗證碼生成完整實作:從基礎到生產級最佳實踐

     編輯:Java 相關知識

前言

圖形驗證碼是 Web 應用防止機器人惡意註冊、暴力破解的第一道防線。本文提供完整的 Java 實作,涵蓋基礎版本、進階抗識別強化、Spring Boot 整合、效能優化等生產環境所需的完整方案。

核心設計原則

原則說明實作方式
隨機性字符、顏色、位置、干擾線全隨機SecureRandom + 字符集配置
不可重放一次性驗證,驗證後即失效Session/Redis 儲存 + 刪除機制
抗 OCR干擾線、噪點、扭曲、字體變化多層干擾 + 仿射變換
高效能低延遲、低記憶體、支援高併發物件池、流式輸出、快取字體
易擴展支援算術、中文、點選、滑塊等類型策略模式 + SPI 擴展

基礎版:標準字符驗證碼

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Base64;
import javax.imageio.ImageIO;

public class CaptchaGenerator {

    // 配置常數
    private static final String CHAR_SET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; // 排除易混淆字符
    private static final int DEFAULT_WIDTH = 130;
    private static final int DEFAULT_HEIGHT = 48;
    private static final int DEFAULT_LENGTH = 4;
    private static final int DEFAULT_LINE_COUNT = 6;
    private static final int DEFAULT_NOISE_COUNT = 30;

    private final SecureRandom random = new SecureRandom();
    private final Font[] fonts = {
        new Font("Arial", Font.BOLD | Font.ITALIC, 32),
        new Font("Courier New", Font.BOLD, 30),
        new Font("Verdana", Font.BOLD | Font.ITALIC, 34),
        new Font("Tahoma", Font.BOLD, 32)
    };

    /**
     * 生成驗證碼
     * @return CaptchaResult 包含 code、image(Base64)、imageBytes
     */
    public CaptchaResult generate() {
        return generate(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_LENGTH, DEFAULT_LINE_COUNT, DEFAULT_NOISE_COUNT);
    }

    public CaptchaResult generate(int width, int height, int length, int lineCount, int noiseCount) {
        // 1. 生成隨機字符
        StringBuilder code = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            code.append(CHAR_SET.charAt(random.nextInt(CHAR_SET.length())));
        }

        // 2. 建立圖片緩衝區
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = image.createGraphics();

        try {
            // 3. 繪製背景
            drawBackground(g2d, width, height);

            // 4. 繪製干擾線
            drawLines(g2d, width, height, lineCount);

            // 5. 繪製噪點
            drawNoise(g2d, width, height, noiseCount);

            // 6. 繪製字符(核心)
            drawCharacters(g2d, code.toString(), width, height);

            // 7. 套用扭曲變換(進階抗 OCR)
            applyShearTransform(g2d, width, height);

            // 8. 輸出
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(image, "PNG", baos);
            byte[] imageBytes = baos.toByteArray();
            String base64 = Base64.getEncoder().encodeToString(imageBytes);

            return new CaptchaResult(code.toString(), base64, imageBytes);

        } catch (IOException e) {
            throw new RuntimeException("驗證碼生成失敗", e);
        } finally {
            g2d.dispose();
        }
    }

    // === 繪製細節方法 ===

    private void drawBackground(Graphics2D g, int w, int h) {
        g.setColor(new Color(245, 245, 250));
        g.fillRect(0, 0, w, h);
        // 漸層背景(可選)
        GradientPaint gp = new GradientPaint(0, 0, new Color(255, 255, 255), w, h, new Color(230, 235, 245));
        g.setPaint(gp);
        g.fillRect(0, 0, w, h);
    }

    private void drawLines(Graphics2D g, int w, int h, int count) {
        g.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        for (int i = 0; i < count; i++) {
            g.setColor(randomColor(160, 220));
            int x1 = random.nextInt(w);
            int y1 = random.nextInt(h);
            int x2 = random.nextInt(w);
            int y2 = random.nextInt(h);
            g.drawLine(x1, y1, x2, y2);
        }
    }

    private void drawNoise(Graphics2D g, int w, int h, int count) {
        for (int i = 0; i < count; i++) {
            g.setColor(randomColor(180, 230));
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int w2 = random.nextInt(3) + 1;
            int h2 = random.nextInt(3) + 1;
            g.fillOval(x, y, w2, h2);
        }
    }

    private void drawCharacters(Graphics2D g, String code, int w, int h) {
        int charWidth = w / code.length();
        for (int i = 0; i < code.length(); i++) {
            char c = code.charAt(i);
            g.setFont(fonts[random.nextInt(fonts.length)]);
            g.setColor(randomColor(20, 130));
            
            // 隨機旋轉角度 (-25° ~ +25°)
            double theta = (random.nextDouble() - 0.5) * Math.PI / 3.6;
            
            // 字符位置微調
            int x = i * charWidth + random.nextInt(charWidth / 4) + 5;
            int y = h / 2 + random.nextInt(h / 8) + 10;

            // 套用旋轉
            AffineTransform original = g.getTransform();
            g.rotate(theta, x, y);
            g.drawString(String.valueOf(c), x, y);
            g.setTransform(original);
        }
    }

    private void applyShearTransform(Graphics2D g, int w, int h) {
        // 水平剪切變換,增加辨識難度
        double shearX = (random.nextDouble() - 0.5) * 0.3;
        AffineTransform transform = AffineTransform.getShearInstance(shearX, 0);
        g.transform(transform);
    }

    private Color randomColor(int min, int max) {
        int r = random.nextInt(max - min) + min;
        int g = random.nextInt(max - min) + min;
        int b = random.nextInt(max - min) + min;
        return new Color(r, g, b);
    }

    // === 結果封裝 ===
    public static class CaptchaResult {
        private final String code;
        private final String base64;
        private final byte[] imageBytes;

        public CaptchaResult(String code, String base64, byte[] imageBytes) {
            this.code = code;
            this.base64 = base64;
            this.imageBytes = imageBytes;
        }

        public String getCode() { return code; }
        public String getBase64() { return base64; }
        public byte[] getImageBytes() { return imageBytes; }
        public String getDataUri() { return "data:image/png;base64," + base64; }
    }
}

Spring Boot 整合:REST API + Redis 儲存

@RestController
@RequestMapping("/api/captcha")
public class CaptchaController {

    private final CaptchaGenerator generator = new CaptchaGenerator();
    private final StringRedisTemplate redisTemplate;

    public CaptchaController(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @GetMapping("/generate")
    public Result<CaptchaVO> generate() {
        // 1. 生成驗證碼
        CaptchaGenerator.CaptchaResult result = generator.generate();

        // 2. 生成唯一標識
        String captchaId = UUID.randomUUID().toString().replace("-", "");
        
        // 3. 存入 Redis,5 分鐘過期
        String key = "captcha:" + captchaId;
        redisTemplate.opsForValue().set(key, result.getCode().toLowerCase(), Duration.ofMinutes(5));

        // 4. 返回前端所需資料
        CaptchaVO vo = new CaptchaVO(captchaId, result.getDataUri());
        return Result.success(vo);
    }

    @PostMapping("/verify")
    public Result<Boolean> verify(@RequestBody VerifyRequest request) {
        String key = "captcha:" + request.getCaptchaId();
        String storedCode = redisTemplate.opsForValue().getAndDelete(key); // 驗證即銷毀
        
        if (storedCode == null) {
            return Result.error("驗證碼已失效或不存在");
        }
        
        boolean match = storedCode.equals(request.getCode().toLowerCase());
        return match ? Result.success(true) : Result.error("驗證碼錯誤");
    }

    static class CaptchaVO {
        private final String captchaId;
        private final String imageBase64;
        public CaptchaVO(String id, String img) { this.captchaId = id; this.imageBase64 = img; }
        public String getCaptchaId() { return captchaId; }
        public String getImageBase64() { return imageBase64; }
    }

    static class VerifyRequest {
        private String captchaId;
        private String code;
        public String getCaptchaId() { return captchaId; }
        public void setCaptchaId(String captchaId) { this.captchaId = captchaId; }
        public String getCode() { return code; }
        public void setCode(String code) { this.code = code; }
    }
}

前端整合範例

<!-- Vue 3 Composition API 範例 -->
<script setup>
import { ref } from 'vue'

const captchaId = ref('')
const captchaImage = ref('')
const inputCode = ref('')

async function refreshCaptcha() {
  const res = await fetch('/api/captcha/generate')
  const data = await res.json()
  captchaId.value = data.data.captchaId
  captchaImage.value = data.data.imageBase64
}

async function verify() {
  const res = await fetch('/api/captcha/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ captchaId: captchaId.value, code: inputCode.value })
  })
  const result = await res.json()
  if (result.success) {
    alert('驗證通過')
  } else {
    alert(result.message)
    refreshCaptcha()
  }
}

onMounted(refreshCaptcha)
</script>

<template>
  <div class="captcha-container">
    <img :src="captchaImage" @click="refreshCaptcha" title="點擊刷新" />
    <input v-model="inputCode" placeholder="輸入驗證碼" maxlength="4" />
    <button @click="verify">驗證</button>
  </div>
</template>

進階強化:抗 AI 識別技術

技術原理實作要點
字符扭曲/彎曲貝塞爾曲線路徑繪製字符GeneralPath + 隨機控制點
波紋/水波變形正弦函數像素位移像素級操作或 Shader
字體隨機化多字體、字重、斜體混合預載 10+ 字體檔案
動態干擾線曲線、虛線、寬度變化QuadCurve2D + BasicStroke
背景紋理噪點、網格、漸層、圖片TexturePaint + BufferedImage
字符重疊/連接破壞字符分割算法負間距、隨機偏移

效能優化建議

  1. 字體快取:啟動時載入字體到記憶體,避免重複 new Font()
  2. 圖片物件池:重用 BufferedImage 和 Graphics2D(需線程安全)
  3. 流式輸出:直接寫入 ServletOutputStream,避免 Base64 編碼開銷
  4. 異步生成:高併發下使用 CompletableFuture 預生成緩衝池
  5. 關閉抗鋸齒:g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF) 可提速 20%+

替代方案對比

方案優點缺點適用場景
自建圖形驗證碼完全可控、無依賴、免費需自行對抗 AI、維護成本高中小型專案、內網系統
Google reCAPTCHA v3無感驗證、Google 維護、安全性高需網路、隱私合規、中國大陸不可用面向國際用戶的公開網站
騰訊/網易/阿里驗證碼國內可用、滑塊/點選體驗好需申請、有額度限制、vendor lock-in國內商業項目
hCaptcha隱私友好、全球可用、有收益需網路、體驗稍差重視隱私的專案

安全檢查清單

  • [ ] 驗證碼一次性使用,驗證後立即刪除
  • [ ] 設定合理過期時間(建議 3-5 分鐘)
  • [ ] 限制單 IP 請求頻率(如 10 次/分鐘)
  • [ ] 失敗次數超限鎖定帳號/IP(如 5 次鎖定 15 分鐘)
  • [ ] 使用 HTTPS 傳輸,防止中間人竊取
  • [ ] 前端不暴露驗證碼答案(僅傳 ID)
  • [ ] 記錄驗證日誌(IP、時間、成功/失敗)供審計
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved