Browse Source

!1393 feat.新增输入字母数字类型的验证码spi
Merge pull request !1393 from DanielTsui/master-jdk17-pictureWordCaptcha

芋道源码 11 months ago
parent
commit
3102fc1f9c

+ 227 - 0
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java

@@ -0,0 +1,227 @@
+package cn.iocoder.yudao.module.system.framework.captcha.core;
+
+import com.anji.captcha.model.common.CaptchaTypeEnum;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.impl.AbstractCaptchaService;
+import com.anji.captcha.service.impl.CaptchaServiceFactory;
+import com.anji.captcha.util.AESUtil;
+import com.anji.captcha.util.ImageUtils;
+import com.anji.captcha.util.RandomUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BufferedImage;
+import java.util.Properties;
+import java.util.Random;
+
+/**
+ * 图片文字验证码
+ *
+ * @author Tsui
+ * @since 2025/7/23 20:44
+ */
+public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService {
+
+    private static final int WIDTH = 120;
+    private static final int HEIGHT = 40;
+    private static final int LINES = 10;
+    private static final Random RANDOM = new Random();
+    private static final String CHARACTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+
+    @Override
+    public void init(Properties config) {
+        super.init(config);
+    }
+
+    @Override
+    public void destroy(Properties config) {
+        logger.info("start-clear-history-data-{}", captchaType());
+    }
+
+    @Override
+    public String captchaType() {
+        return "pictureWord";
+//        return CaptchaTypeEnum.PICTURE_WORD.getCodeValue();
+    }
+
+    @Override
+    public ResponseModel get(CaptchaVO captchaVO) {
+        String text = generateRandomText(4);
+
+        CaptchaVO imageData = getImageData(text);
+
+        // pointJson不传到前端,只做后端校验,测试时放开
+//        imageData.setPointJson(text);
+
+        return ResponseModel.successData(imageData);
+    }
+
+    @Override
+    public ResponseModel check(CaptchaVO captchaVO) {
+        ResponseModel r = super.check(captchaVO);
+        if (!validatedReq(r)) {
+            return r;
+        }
+
+        // 取出验证码
+        String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken());
+        if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+        }
+        // 正确的验证码
+        String codeValue = CaptchaServiceFactory.getCache(cacheType).get(codeKey);
+        String code = getCodeByCodeValue(codeValue);
+        String secretKey = getSecretKeyByCodeValue(codeValue);
+        //验证码只用一次,即刻失效
+        CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+
+        // 用户输入的验证码(CaptchaVO中没有预留字段,暂时用pointJson,无需加解密)
+        String userCode = captchaVO.getPointJson();
+        if (!StringUtils.equalsIgnoreCase(code, userCode)) {
+            afterValidateFail(captchaVO);
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
+        }
+
+        //校验成功,将信息存入缓存
+        String value;
+        try {
+            value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(userCode), secretKey);
+        } catch (Exception e) {
+            logger.error("AES加密失败", e);
+            afterValidateFail(captchaVO);
+            return ResponseModel.errorMsg(e.getMessage());
+        }
+        String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value);
+        CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE);
+        captchaVO.setResult(true);
+        captchaVO.resetClientFlag();
+        return ResponseModel.successData(captchaVO);
+    }
+
+    @Override
+    public ResponseModel verification(CaptchaVO captchaVO) {
+        ResponseModel r = super.verification(captchaVO);
+        if (!validatedReq(r)) {
+            return r;
+        }
+        try {
+            String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
+            if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+            }
+            //二次校验取值后,即刻失效
+            CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+        } catch (Exception e) {
+            logger.error("验证码解析失败", e);
+            return ResponseModel.errorMsg(e.getMessage());
+        }
+        return ResponseModel.success();
+    }
+
+
+    private CaptchaVO getImageData(String text) {
+        CaptchaVO dataVO = new CaptchaVO();
+        BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g = image.createGraphics();
+
+        // 设置背景色
+        g.setColor(getRandomColor(200, 250));
+        g.fillRect(0, 0, WIDTH, HEIGHT);
+
+        // 绘制干扰线
+        for (int i = 0; i < LINES; i++) {
+            g.setColor(getRandomColor(100, 200));
+            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);
+        }
+
+        // 设置字体
+        g.setFont(new Font("Arial", Font.BOLD, 24));
+
+        // 绘制验证码文本
+        for (int i = 0; i < text.length(); i++) {
+            g.setColor(getRandomColor(20, 130));
+
+            // 文字旋转
+            AffineTransform affineTransform = new AffineTransform();
+            int x = 20 + i * 20;
+            int y = 24 + RANDOM.nextInt(8);
+            // 旋转范围 -45 ~ 45
+            affineTransform.setToRotation(Math.toRadians(RandomUtils.getRandomInt(-45, 45)), x, y);
+            g.setTransform(affineTransform);
+
+            g.drawString(text.charAt(i) + "", x, y);
+        }
+
+        // 添加噪点
+        for (int i = 0; i < 100; i++) {
+            int x = RANDOM.nextInt(WIDTH);
+            int y = RANDOM.nextInt(HEIGHT);
+            image.setRGB(x, y, getRandomColor(0, 255).getRGB());
+        }
+
+        g.dispose();
+
+        String secretKey = null;
+        if (captchaAesStatus) {
+            secretKey = AESUtil.getKey();
+        }
+        dataVO.setSecretKey(secretKey);
+
+        dataVO.setOriginalImageBase64(ImageUtils.getImageToBase64Str(image).replaceAll("\r|\n", ""));
+        dataVO.setToken(RandomUtils.getUUID());
+//        dataVO.setSecretKey(secretKey);
+        //将坐标信息存入redis中
+        String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken());
+        CaptchaServiceFactory.getCache(cacheType).set(codeKey, getCodeValue(text, secretKey), EXPIRESIN_SECONDS);
+        return dataVO;
+    }
+
+    private String getCodeValue(String text, String secretKey) {
+        return text + "," + secretKey;
+    }
+
+    private String getCodeByCodeValue(String codeValue) {
+        return codeValue.split(",")[0];
+    }
+
+    private String getSecretKeyByCodeValue(String codeValue) {
+        return codeValue.split(",")[1];
+    }
+
+    private Color getRandomColor(int min, int max) {
+        if (min > max) {
+            int temp = min;
+            min = max;
+            max = temp;
+        }
+        int r = min + RANDOM.nextInt(max - min);
+        int g = min + RANDOM.nextInt(max - min);
+        int b = min + RANDOM.nextInt(max - min);
+        return new Color(r, g, b);
+    }
+
+    /**
+     * 生成指定长度的随机字符串
+     *
+     * @param length 长度
+     * @return {@link String}
+     * @author Rex
+     * @since 2025/6/26 15:20
+     */
+    public static String generateRandomText(int length) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < length; i++) {
+            int index = RANDOM.nextInt(CHARACTERS.length());
+            sb.append(CHARACTERS.charAt(index));
+        }
+        return sb.toString();
+    }
+
+}

+ 1 - 0
yudao-module-system/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService

@@ -0,0 +1 @@
+cn.iocoder.yudao.module.system.framework.captcha.core.PictureWordCaptchaServiceImpl