org.dromara
diff --git a/ruoyi-common/ruoyi-common-captcha/pom.xml b/ruoyi-common/ruoyi-common-captcha/pom.xml
new file mode 100644
index 000000000..ea2c720ba
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-captcha/pom.xml
@@ -0,0 +1,71 @@
+
+
+
+ org.dromara
+ ruoyi-common
+ ${revision}
+
+ 4.0.0
+
+ ruoyi-common-captcha
+
+
+ ruoyi-common-code 验证码模块
+
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+ org.projectlombok
+ lombok
+
+
+ cn.hutool
+ hutool-core
+
+
+ org.dromara
+ ruoyi-common-core
+
+
+ org.dromara
+ ruoyi-common-redis
+
+
+ org.dromara
+ ruoyi-common-web
+
+
+ org.dromara
+ ruoyi-common-mail
+
+
+ cloud.tianai.captcha
+ tianai-captcha-springboot-starter
+
+
+ org.dromara.sms4j
+ sms4j-api
+ 3.3.5
+ compile
+
+
+ org.dromara.sms4j
+ sms4j-core
+ 3.3.5
+ compile
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.3.2
+ compile
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/config/CaptchaConfig.java b/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/config/CaptchaConfig.java
new file mode 100644
index 000000000..e3efd3800
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/config/CaptchaConfig.java
@@ -0,0 +1,45 @@
+package org.dromara.common.captcha.config;
+
+import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
+import cloud.tianai.captcha.resource.ResourceStore;
+import cloud.tianai.captcha.resource.common.model.dto.Resource;
+import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
+import org.dromara.common.captcha.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.Configuration;
+
+/**
+ * 验证码配置
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(CaptchaProperties.class)
+@Configuration
+public class CaptchaConfig {
+
+
+ @Bean
+ public ResourceStore resourceStore() {
+ // 使用简单的本地内存存储器,实际项目中可以使用数据库等存储
+ LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();
+
+ // 2. 添加自定义背景图片
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg", "default"));
+ resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg", "default"));
+// resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/c.jpg", "default"));
+
+ return resourceStore;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java b/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/config/properties/CaptchaProperties.java
similarity index 82%
rename from ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java
rename to ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/config/properties/CaptchaProperties.java
index 17f165549..0a86e4d26 100644
--- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java
+++ b/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/config/properties/CaptchaProperties.java
@@ -1,4 +1,4 @@
-package org.dromara.common.web.config.properties;
+package org.dromara.common.captcha.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -9,7 +9,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Lion Li
*/
@Data
-@ConfigurationProperties(prefix = "captcha")
+@ConfigurationProperties(prefix = "image-captcha")
public class CaptchaProperties {
/**
diff --git a/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/utils/CaptchaUtils.java b/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/utils/CaptchaUtils.java
new file mode 100644
index 000000000..50f3052c4
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-captcha/src/main/java/org/dromara/common/captcha/utils/CaptchaUtils.java
@@ -0,0 +1,258 @@
+package org.dromara.common.captcha.utils;
+
+import cloud.tianai.captcha.application.ImageCaptchaApplication;
+import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
+import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
+import cloud.tianai.captcha.common.response.ApiResponse;
+import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
+import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.captcha.config.properties.CaptchaProperties;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.regex.RegexValidator;
+import org.dromara.common.mail.core.MailBuilder;
+import org.dromara.common.redis.annotation.RateLimiter;
+import org.dromara.common.redis.enums.LimitType;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.web.core.WaveAndCircleCaptcha;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.stereotype.Component;
+
+import java.awt.*;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 验证码工具类
+ * 支持:图形验证码、手机短信验证码、邮箱验证码、行为验证码
+ * 规范:一次下发生成验证码、二次校验验证码;内置通用随机码生成、图片生成基础能力
+ *
+ * @author xiaozhi
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class CaptchaUtils {
+
+ private final CaptchaProperties captchaProperties;
+ private final ImageCaptchaApplication imageCaptchaApplication;
+
+ /**
+ * SpEL解析器,单例复用,避免频繁创建
+ */
+ private final ExpressionParser expressionParser = new SpelExpressionParser();
+
+
+ // ====================== 【图形验证码】 ======================
+
+ /**
+ * 图片验证码响应对象。
+ *
+ * @param captchaEnabled 是否启用验证码
+ * @param uuid 验证码标识
+ * @param img Base64 图片数据
+ */
+ public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) {
+ }
+
+ /**
+ * 生成图形验证码
+ * 限流:同一IP 60秒最多10次请求
+ *
+ * @return CaptchaVo
+ */
+ @RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
+ public CaptchaVo generateImageCaptcha() {
+ boolean captchaEnabled = captchaProperties.getEnable();
+ if (!captchaEnabled) {
+ return new CaptchaVo(false, null, null);
+ }
+ // 保存验证码信息
+ String uuid = IdUtil.simpleUUID();
+ String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
+ String captchaType = captchaProperties.getType();
+
+ CodeGenerator codeGenerator;
+ if ("math".equals(captchaType)) {
+ codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false);
+ } else {
+ codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
+ }
+
+ WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
+ // captcha.setBackground(Color.WHITE); // 不设置就是透明底
+ captcha.setFont(new Font("Arial", Font.BOLD, 45));
+ captcha.setGenerator(codeGenerator);
+ captcha.createCode();
+ String code = captcha.getCode();
+
+ // 数学验证码,计算表达式结果存储
+ if ("math".equals(captchaType)) {
+ Expression exp = expressionParser.parseExpression(StringUtils.remove(code, "="));
+ code = exp.getValue(String.class);
+ }
+ RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+ return new CaptchaVo(true, uuid, captcha.getImageBase64());
+ }
+
+ /**
+ * 校验图形验证码
+ * 【一次性消费】校验通过自动删除缓存,防止重复使用、重放攻击
+ *
+ * @param uuid 前端传递验证码唯一标识
+ * @param inputCode 用户输入验证码
+ * @return true=验证通过 false=验证失败
+ */
+ public boolean verifyImageCaptcha(String uuid, String inputCode) {
+
+ return true;
+ }
+
+ // ====================== 【手机短信验证码】 ======================
+
+ /**
+ * 生成手机短信验证码并发送
+ *
+ * @param phoneNumber 手机号
+ */
+ public void generateSmsCaptcha(String phoneNumber) {
+ if (!RegexValidator.isMobile(phoneNumber)) {
+ return;
+ }
+ String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
+ String code = RandomUtil.randomNumbers(6);
+ // 验证码模板id 自行处理 (查数据库或写死均可)
+ String templateId = "";
+ LinkedHashMap map = new LinkedHashMap<>(1);
+ map.put("code", code);
+ SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
+
+ SmsResponse smsResponse;
+ try {
+ smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
+ } catch (Exception e) {
+ log.error("验证码短信发送异常,手机号:{}", phoneNumber, e);
+ return;
+ }
+ if (!smsResponse.isSuccess()) {
+ log.error("验证码短信发送失败 => {}", smsResponse);
+ return;
+ }
+ // 发送成功才写入缓存
+ RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+ }
+
+ /**
+ * 校验短信验证码
+ * 【一次性消费】校验成功自动删除缓存
+ *
+ * @param phoneNumber 手机号
+ * @param inputCode 用户输入验证码
+ * @return 是否通过
+ */
+ public boolean verifySmsCaptcha(String phoneNumber, String inputCode) {
+ return true;
+ }
+ // ====================== 【邮箱验证码】 ======================
+
+ /**
+ * 生成邮箱验证码并发送
+ * 限流:同一个邮箱60秒最多1次
+ *
+ * @param email 邮箱地址
+ */
+ @RateLimiter(key = "#email", time = 60, count = 1)
+ public void generateEmailCaptcha(String email) {
+ if (!RegexValidator.isEmail(email)) {
+ return;
+ }
+ String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
+ String code = RandomUtil.randomNumbers(6);
+ try {
+ MailBuilder.of().to(email).subject("登录验证码").text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。").send();
+ RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+ } catch (Exception e) {
+ log.error("验证码短信发送异常 => {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 校验邮箱验证码
+ *
+ * @param inputCode 用户输入
+ * @param cacheCode 缓存验证码
+ * @return 是否通过
+ */
+ public boolean verifyEmailCaptcha(String inputCode, String cacheCode) {
+ return true;
+ }
+
+ // ====================== 【行为验证码(滑块/点选)】 ======================
+
+ /**
+ * 生成行为验证码初始票据 ticket
+ * 用于前端拉取滑块背景图、滑块图
+ *
+ * @return ticket字符串
+ */
+ public ApiResponse generateBehaviorCaptchaTicket() {
+ String type;
+ int i = ThreadLocalRandom.current().nextInt(0, 4);
+ if (i == 0) {
+ type = CaptchaTypeConstant.SLIDER;
+ } else if (i == 1) {
+ type = CaptchaTypeConstant.CONCAT;
+ } else if (i == 2) {
+ type = CaptchaTypeConstant.ROTATE;
+ } else {
+ type = CaptchaTypeConstant.WORD_IMAGE_CLICK;
+ }
+ GenerateParam generateParam = new GenerateParam();
+ // 要生成的验证码类型
+ generateParam.setType(type);
+ return imageCaptchaApplication.generateCaptcha(generateParam);
+ }
+
+
+ /**
+ * 行为验证码一次校验(滑块坐标、轨迹校验由第三方SDK/自研逻辑实现)
+ * 本方法仅做凭证匹配占位,实际行为判定需要额外业务逻辑
+ *
+ * @param ticket 请求票据
+ * @param cacheTicket 缓存票据
+ * @return 票据是否匹配
+ */
+ public ApiResponse> verifyBehaviorCaptchaFirstStage(String id, ImageCaptchaTrack data) {
+ ApiResponse> response = imageCaptchaApplication.matching(id, data);
+ if (response.isSuccess()) {
+ return ApiResponse.ofSuccess(Collections.singletonMap("id", id));
+ }
+ return response;
+ }
+
+ /**
+ * 行为验证码二次验证(业务接口调用,例如登录接口)
+ *
+ * @param verifyTicket 前端传入二次凭证
+ * @param cacheVerifyTicket 服务端缓存凭证
+ * @return 验证结果
+ */
+ public boolean verifyBehaviorCaptchaSecondStage(String verifyTicket, String cacheVerifyTicket) {
+ return true;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 000000000..ee7d08602
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.dromara.common.captcha.config.CaptchaConfig
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
deleted file mode 100644
index 7140db947..000000000
--- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaConfig.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.dromara.common.web.config;
-
-import org.dromara.common.web.config.properties.CaptchaProperties;
-import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-
-/**
- * 验证码配置
- *
- * @author Lion Li
- */
-@AutoConfiguration
-@EnableConfigurationProperties(CaptchaProperties.class)
-public class CaptchaConfig {
-
-}
diff --git a/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 7299472a0..1f706e721 100644
--- a/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,4 +1,3 @@
-org.dromara.common.web.config.CaptchaConfig
org.dromara.common.web.config.FilterConfig
org.dromara.common.web.config.I18nConfig
org.dromara.common.web.config.ResourcesConfig