From 6f94095bb0d0cbd64066d5ea1b15a5072e1eaa6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Wed, 14 Jan 2026 18:28:37 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=20=E8=87=AA?= =?UTF-8?q?=E8=A1=8C=E5=AE=9E=E7=8E=B0=E6=9B=B4=E6=BC=82=E4=BA=AE=E7=9A=84?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E5=9B=BE=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/CaptchaController.java | 21 +- .../src/main/resources/application.yml | 4 +- .../common/web/config/CaptchaConfig.java | 49 ----- .../config/properties/CaptchaProperties.java | 9 +- .../common/web/core/WaveAndCircleCaptcha.java | 197 ++++++++++++++++++ .../common/web/enums/CaptchaCategory.java | 35 ---- .../dromara/common/web/enums/CaptchaType.java | 29 --- 7 files changed, 211 insertions(+), 133 deletions(-) create mode 100644 ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/WaveAndCircleCaptcha.java delete mode 100644 ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaCategory.java delete mode 100644 ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java diff --git a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java index dcd04c5e1..2586addab 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java @@ -1,8 +1,9 @@ package org.dromara.web.controller; import cn.dev33.satoken.annotation.SaIgnore; -import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.RandomUtil; import jakarta.validation.constraints.NotBlank; @@ -14,14 +15,13 @@ import org.dromara.common.core.domain.R; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StringUtils; -import org.dromara.common.core.utils.reflect.ReflectUtils; import org.dromara.common.mail.config.properties.MailProperties; import org.dromara.common.mail.utils.MailUtils; import org.dromara.common.ratelimiter.annotation.RateLimiter; import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.redis.utils.RedisUtils; +import org.dromara.common.web.core.WaveAndCircleCaptcha; import org.dromara.common.web.config.properties.CaptchaProperties; -import org.dromara.common.web.enums.CaptchaType; import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.core.factory.SmsFactory; @@ -33,6 +33,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import java.awt.*; import java.time.Duration; import java.util.LinkedHashMap; @@ -130,19 +131,21 @@ public class CaptchaController { String uuid = IdUtil.simpleUUID(); String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; // 生成验证码 - CaptchaType captchaType = captchaProperties.getType(); + String captchaType = captchaProperties.getType(); CodeGenerator codeGenerator; - if (CaptchaType.MATH == captchaType) { - codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false); + if ("math".equals(captchaType)) { + codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false); } else { - codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength()); + codeGenerator = new RandomGenerator(captchaProperties.getCharLength()); } - AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); + WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60); + // captcha.setBackground(Color.WHITE); // 不设置就是透明底 + captcha.setFont(new Font("Arial", Font.BOLD, 45)); captcha.setGenerator(codeGenerator); captcha.createCode(); // 如果是数学验证码,使用SpEL表达式处理验证码结果 String code = captcha.getCode(); - if (CaptchaType.MATH == captchaType) { + if ("math".equals(captchaType)) { ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(StringUtils.remove(code, "=")); code = exp.getValue(String.class); diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 3fc344e0b..d11d9f0ea 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -24,9 +24,7 @@ captcha: # 是否启用验证码校验 enable: true # 验证码类型 math 数组计算 char 字符验证 - type: MATH - # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 - category: SHEAR + type: math # 数字验证码位数 numberLength: 1 # 字符验证码长度 diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java index b7172e9b6..7140db947 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java @@ -1,16 +1,8 @@ package org.dromara.common.web.config; -import cn.hutool.captcha.CaptchaUtil; -import cn.hutool.captcha.CircleCaptcha; -import cn.hutool.captcha.LineCaptcha; -import cn.hutool.captcha.ShearCaptcha; import org.dromara.common.web.config.properties.CaptchaProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; - -import java.awt.*; /** * 验证码配置 @@ -21,45 +13,4 @@ import java.awt.*; @EnableConfigurationProperties(CaptchaProperties.class) public class CaptchaConfig { - private static final int WIDTH = 160; - private static final int HEIGHT = 60; - private static final Color BACKGROUND = Color.WHITE; - private static final Font FONT = new Font("Arial", Font.BOLD, 48); - - /** - * 圆圈干扰验证码 - */ - @Lazy - @Bean - public CircleCaptcha circleCaptcha() { - CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT); - captcha.setBackground(BACKGROUND); - captcha.setFont(FONT); - return captcha; - } - - /** - * 线段干扰的验证码 - */ - @Lazy - @Bean - public LineCaptcha lineCaptcha() { - LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT); - captcha.setBackground(BACKGROUND); - captcha.setFont(FONT); - return captcha; - } - - /** - * 扭曲干扰验证码 - */ - @Lazy - @Bean - public ShearCaptcha shearCaptcha() { - ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT); - captcha.setBackground(BACKGROUND); - captcha.setFont(FONT); - return captcha; - } - } diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java index bfc52f450..6dcfe64bb 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java @@ -1,7 +1,5 @@ package org.dromara.common.web.config.properties; -import org.dromara.common.web.enums.CaptchaCategory; -import org.dromara.common.web.enums.CaptchaType; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -19,12 +17,7 @@ public class CaptchaProperties { /** * 验证码类型 */ - private CaptchaType type; - - /** - * 验证码类别 - */ - private CaptchaCategory category; + private String type; /** * 数字验证码位数 diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/WaveAndCircleCaptcha.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/WaveAndCircleCaptcha.java new file mode 100644 index 000000000..8b37be471 --- /dev/null +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/core/WaveAndCircleCaptcha.java @@ -0,0 +1,197 @@ +package org.dromara.common.web.core; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.img.GraphicsUtil; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.Serial; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 带干扰线、波浪、圆的验证码 + * + * @author Lion Li + */ +public class WaveAndCircleCaptcha extends AbstractCaptcha { + + @Serial + private static final long serialVersionUID = 1L; + + // 构造方法(略,与之前一致) + public WaveAndCircleCaptcha(int width, int height) { + this(width, height, 4); + } + + public WaveAndCircleCaptcha(int width, int height, int codeCount) { + this(width, height, codeCount, 6); + } + + public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount) { + this(width, height, new RandomGenerator(codeCount), interfereCount); + } + + public WaveAndCircleCaptcha(int width, int height, CodeGenerator generator, int interfereCount) { + super(width, height, generator, interfereCount); + } + + public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount, float size) { + super(width, height, new RandomGenerator(codeCount), interfereCount, size); + } + + @Override + public Image createImage(String code) { + final BufferedImage image = new BufferedImage( + width, + height, + (null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB + ); + final Graphics2D g = ImgUtil.createGraphics(image, this.background); + + try { + drawString(g, code); + // 扭曲 + shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + drawInterfere(g); + } finally { + g.dispose(); + } + + return image; + } + + private void drawString(Graphics2D g, String code) { + // 设置抗锯齿(让字体渲染更清晰) + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + if (this.textAlpha != null) { + g.setComposite(this.textAlpha); + } + + GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height); + } + + protected void drawInterfere(Graphics2D g) { + ThreadLocalRandom random = RandomUtil.getRandom(); + int circleCount = Math.max(0, this.interfereCount - 1); + + // 圈圈 + for (int i = 0; i < circleCount; i++) { + g.setColor(ImgUtil.randomColor(random)); + int x = random.nextInt(width); + int y = random.nextInt(height); + int w = random.nextInt(height >> 1); + int h = random.nextInt(height >> 1); + g.drawOval(x, y, w, h); + } + + // 仅 1 条平滑波浪线 + if (this.interfereCount >= 1) { + g.setColor(getRandomColor(120, 230, random)); + drawSmoothWave(g, random); + } + } + + private void drawSmoothWave(Graphics2D g, ThreadLocalRandom random) { + int amplitude = random.nextInt(8) + 5; // 波动幅度 + int wavelength = random.nextInt(40) + 30; // 波长 + double phase = random.nextDouble() * Math.PI * 2; + + // ✅ 关键:限制 baseY 在中间区域 + int centerY = height / 2; + int verticalJitter = Math.max(5, height / 6); // 至少偏移5像素 + int baseY = centerY - verticalJitter + random.nextInt(verticalJitter * 2); + + g.setStroke(new BasicStroke(2.5f)); // 线宽 + + int[] xPoints = new int[width]; + int[] yPoints = new int[width]; + for (int x = 0; x < width; x++) { + int y = baseY + (int) (amplitude * Math.sin((double) x / wavelength * 2 * Math.PI + phase)); + // 限制 y 不要超出图像边界(可选) + y = Math.max(amplitude, Math.min(y, height - amplitude)); + xPoints[x] = x; + yPoints[x] = y; + } + g.drawPolyline(xPoints, yPoints, width); + } + + private Color getRandomColor(int min, int max, ThreadLocalRandom random) { + int range = max - min; + return new Color( + min + random.nextInt(range), + min + random.nextInt(range), + min + random.nextInt(range) + ); + } + + /** + * 扭曲 + * + * @param g {@link Graphics} + * @param w1 w1 + * @param h1 h1 + * @param color 颜色 + */ + private void shear(Graphics g, int w1, int h1, Color color) { + shearX(g, w1, h1, color); + shearY(g, w1, h1, color); + } + + /** + * X坐标扭曲 + * + * @param g {@link Graphics} + * @param w1 宽 + * @param h1 高 + * @param color 颜色 + */ + private void shearX(Graphics g, int w1, int h1, Color color) { + + int period = RandomUtil.randomInt(this.width); + + int frames = 1; + int phase = RandomUtil.randomInt(2); + + for (int i = 0; i < h1; i++) { + double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(0, i, w1, 1, (int) d, 0); + g.setColor(color); + g.drawLine((int) d, i, 0, i); + g.drawLine((int) d + w1, i, w1, i); + } + + } + + /** + * Y坐标扭曲 + * + * @param g {@link Graphics} + * @param w1 宽 + * @param h1 高 + * @param color 颜色 + */ + private void shearY(Graphics g, int w1, int h1, Color color) { + + int period = RandomUtil.randomInt(this.height >> 1); + + int frames = 20; + int phase = 7; + for (int i = 0; i < w1; i++) { + double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(i, 0, 1, h1, 0, (int) d); + g.setColor(color); + // 擦除原位置的痕迹 + g.drawLine(i, (int) d, i, 0); + g.drawLine(i, (int) d + h1, i, h1); + } + + } + +} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaCategory.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaCategory.java deleted file mode 100644 index ecf26583c..000000000 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaCategory.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.dromara.common.web.enums; - -import cn.hutool.captcha.AbstractCaptcha; -import cn.hutool.captcha.CircleCaptcha; -import cn.hutool.captcha.LineCaptcha; -import cn.hutool.captcha.ShearCaptcha; -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 验证码类别 - * - * @author Lion Li - */ -@Getter -@AllArgsConstructor -public enum CaptchaCategory { - - /** - * 线段干扰 - */ - LINE(LineCaptcha.class), - - /** - * 圆圈干扰 - */ - CIRCLE(CircleCaptcha.class), - - /** - * 扭曲干扰 - */ - SHEAR(ShearCaptcha.class); - - private final Class clazz; -} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java deleted file mode 100644 index d0b6ca3eb..000000000 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.dromara.common.web.enums; - -import cn.hutool.captcha.generator.CodeGenerator; -import cn.hutool.captcha.generator.MathGenerator; -import cn.hutool.captcha.generator.RandomGenerator; -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 验证码类型 - * - * @author Lion Li - */ -@Getter -@AllArgsConstructor -public enum CaptchaType { - - /** - * 数字 - */ - MATH(MathGenerator.class), - - /** - * 字符 - */ - CHAR(RandomGenerator.class); - - private final Class clazz; -}