From 2382c3d844cc4e5bca1c61086fb75bcdef44a373 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 4 Aug 2025 13:01:02 +0800 Subject: [PATCH] =?UTF-8?q?reactor=EF=BC=9A=E3=80=90system=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8A=9F=E8=83=BD=E3=80=91=E4=BC=98=E5=8C=96=E2=80=9C?= =?UTF-8?q?=E6=96=87=E5=AD=97=E9=AA=8C=E8=AF=81=E7=A0=81=E2=80=9D=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/PictureWordCaptchaServiceImpl.java | 212 ++++++++++++++++++ .../com.anji.captcha.service.CaptchaService | 1 + .../src/main/resources/application.yaml | 2 +- .../src/main/resources/application.yaml | 2 +- 4 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java create mode 100644 yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java new file mode 100644 index 000000000..a9e3d919b --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java @@ -0,0 +1,212 @@ +package cn.iocoder.yudao.module.system.framework.captcha.core; + +import cn.hutool.core.util.RandomUtil; +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; + +/** + * 图片文字验证码 + * + * @author Tsui + * @since 2025/7/23 20:44 + */ +public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService { + + /** + * 验证码的基础字符 + */ + private static final String CHARACTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + /** + * 验证码长度 + */ + private static final Integer LENGTH = 4; + + private static final int WIDTH = 120; + private static final int HEIGHT = 40; + private static final int LINES = 10; + + @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"; + } + + @Override + public ResponseModel get(CaptchaVO captchaVO) { + String text = generateRandomText(LENGTH); + 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 = RandomUtil.randomInt(WIDTH); + int y1 = RandomUtil.randomInt(HEIGHT); + int x2 = RandomUtil.randomInt(WIDTH); + int y2 = RandomUtil.randomInt(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 + RandomUtil.randomInt(8); + // 旋转范围 -45 ~ 45 + affineTransform.setToRotation(Math.toRadians(RandomUtil.randomInt(-45, 45)), x, y); + g.setTransform(affineTransform); + g.drawString(text.charAt(i) + "", x, y); + } + // 添加噪点 + for (int i = 0; i < 100; i++) { + int x = RandomUtil.randomInt(WIDTH); + int y = RandomUtil.randomInt(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) { + int minVal = Math.min(min, max); + int maxVal = Math.max(min, max); + int r = RandomUtil.randomInt(minVal, maxVal); + int g = RandomUtil.randomInt(minVal, maxVal); + int b = RandomUtil.randomInt(minVal, maxVal); + return new Color(r, g, b); + } + + /** + * 生成指定长度的随机字符串 + * + * @param length 长度 + * @return {@link String} + */ + public static String generateRandomText(int length) { + return RandomUtil.randomString(CHARACTERS, length); + } + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService b/yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService new file mode 100644 index 000000000..80adf6ddb --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService @@ -0,0 +1 @@ +cn.iocoder.yudao.module.system.framework.captcha.core.PictureWordCaptchaServiceImpl \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml b/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml index 396447ff9..2d90ccfac 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml @@ -135,7 +135,7 @@ aj: cache-type: redis # 缓存 local/redis... cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 - type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 + type: blockPuzzle # 验证码类型 default 三种都实例化。blockPuzzle 滑块拼图、clickWord 文字点选、pictureWord 文本输入 water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode interference-options: 0 # 滑动干扰项(0/1/2) req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 4eeb96f9d..e7e87bd06 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -131,7 +131,7 @@ aj: cache-type: redis # 缓存 local/redis... cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 - type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 + type: blockPuzzle # 验证码类型 default 三种都实例化。blockPuzzle 滑块拼图、clickWord 文字点选、pictureWord 文本输入 water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode interference-options: 0 # 滑动干扰项(0/1/2) req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false