diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml b/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
index cbdb3a100..83ee5e5ba 100644
--- a/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
@@ -12,8 +12,8 @@
jar
${project.artifactId}
-
- 验证码
+ 验证码拓展
+ 1. 基于 aj-captcha 实现图形验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/
@@ -23,9 +23,10 @@
spring-boot-starter
+
- cn.iocoder.boot
- yudao-spring-boot-starter-redis
+ org.springframework.boot
+ spring-boot-starter-data-redis
@@ -33,7 +34,6 @@
com.anji-plus
spring-boot-starter-captcha
-
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/config/YudaoCaptchaConfiguration.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/config/YudaoCaptchaConfiguration.java
new file mode 100644
index 000000000..0f47b0844
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/config/YudaoCaptchaConfiguration.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.captcha.config;
+
+import cn.hutool.core.util.ClassUtil;
+import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants;
+import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl;
+import com.anji.captcha.service.CaptchaCacheService;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+@Configuration
+public class YudaoCaptchaConfiguration {
+
+ static {
+ // 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到
+ // 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举
+ ClassUtil.loadClass(CaptchaRedisKeyConstants.class.getName());
+ }
+
+ @Bean
+ public CaptchaCacheService captchaCacheService(StringRedisTemplate stringRedisTemplate) {
+ return new RedisCaptchaServiceImpl(stringRedisTemplate);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/captcha/core/service/CaptchaServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/service/RedisCaptchaServiceImpl.java
similarity index 69%
rename from yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/captcha/core/service/CaptchaServiceImpl.java
rename to yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/service/RedisCaptchaServiceImpl.java
index c10390e9d..c14901efb 100644
--- a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/captcha/core/service/CaptchaServiceImpl.java
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/service/RedisCaptchaServiceImpl.java
@@ -1,23 +1,31 @@
-package cn.iocoder.yudao.captcha.core.service;
+package cn.iocoder.yudao.framework.captcha.core.service;
import com.anji.captcha.service.CaptchaCacheService;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
-@Service
-public class CaptchaServiceImpl implements CaptchaCacheService {
+/**
+ * 基于 Redis 实现验证码的存储
+ *
+ * @author 星语
+ */
+@NoArgsConstructor // 保证 aj-captcha 的 SPI 创建
+@AllArgsConstructor
+public class RedisCaptchaServiceImpl implements CaptchaCacheService {
+
+ @Resource // 保证 aj-captcha 的 SPI 创建时的注入
+ private StringRedisTemplate stringRedisTemplate;
@Override
public String type() {
return "redis";
}
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
@Override
public void set(String key, String value, long expiresInSeconds) {
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
@@ -42,4 +50,5 @@ public class CaptchaServiceImpl implements CaptchaCacheService {
public Long increment(String key, long val) {
return stringRedisTemplate.opsForValue().increment(key,val);
}
+
}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/package-info.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/package-info.java
new file mode 100644
index 000000000..5fd8a52ba
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 验证码拓展
+ * 1. 基于 aj-captcha 实现图形验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/
+ *
+ * @author 星语
+ */
+package cn.iocoder.yudao.framework.captcha;
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService
index f52c57354..afede97de 100644
--- a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService
@@ -1 +1 @@
-cn.iocoder.yudao.captcha.core.service.CaptchaServiceImpl
\ No newline at end of file
+cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..ed8b528ff
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
index 2f39519a3..97bb86262 100644
--- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -12,8 +12,7 @@ public interface ErrorCodeConstants {
// ========== AUTH 模块 1002000000 ==========
ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确");
ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用");
- ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在");
- ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确");
+ ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确,原因:{}");
ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定");
ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1002000006, "Token 已经过期");
ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1002000007, "手机号不存在");
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
index f85304067..346ce78c7 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
@@ -33,8 +33,10 @@ public class AuthLoginReqVO {
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
- @ApiModelProperty(value = "验证码", required = true, example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==")
- @NotEmpty(message = "验证码不能为空")
+ @ApiModelProperty(value = "验证码", required = true,
+ example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==",
+ notes = "验证码开启时,需要传递")
+ @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class)
private String captchaVerification;
// ========== 绑定社交登录时,需要传递如下参数 ==========
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java
deleted file mode 100644
index 4028f6cef..000000000
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package cn.iocoder.yudao.module.system.framework.captcha.config;
-
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableConfigurationProperties(CaptchaProperties.class)
-public class CaptchaConfig {
-}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java
deleted file mode 100644
index 0d7cd0d20..000000000
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.module.system.framework.captcha.config;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.validation.annotation.Validated;
-
-import javax.validation.constraints.NotNull;
-import java.time.Duration;
-
-@ConfigurationProperties(prefix = "yudao.captcha")
-@Validated
-@Data
-public class CaptchaProperties {
-
- private static final Boolean ENABLE_DEFAULT = true;
-
- /**
- * 是否开启
- * 注意,这里仅仅是后端 Server 是否校验,暂时不控制前端的逻辑
- */
- private Boolean enable = ENABLE_DEFAULT;
- /**
- * 验证码的过期时间
- */
- @NotNull(message = "验证码的过期时间不为空")
- private Duration timeout;
- /**
- * 验证码的高度
- */
- @NotNull(message = "验证码的高度不能为空")
- private Integer height;
- /**
- * 验证码的宽度
- */
- @NotNull(message = "验证码的宽度不能为空")
- private Integer width;
-
-}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java
deleted file mode 100644
index ee406c079..000000000
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * 基于 Hutool captcha 库,实现验证码功能
- */
-package cn.iocoder.yudao.module.system.framework.captcha;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
index a3d3e4283..ce860add5 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
@@ -24,7 +25,9 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
+import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -61,6 +64,12 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Resource
private SmsCodeApi smsCodeApi;
+ /**
+ * 验证码的开关,默认为 true
+ */
+ @Value("${yudao.captcha.enable:true}")
+ private Boolean captchaEnable;
+
@Override
public AdminUserDO authenticate(String username, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
@@ -84,23 +93,19 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Override
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
- CaptchaVO captchaVO = new CaptchaVO();
- captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
- ResponseModel response = captchaService.verification(captchaVO);
- if(response.isSuccess()){
- // 使用账号密码,进行登录
- AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
+ // 校验验证码
+ verifyCaptcha(reqVO);
- // 如果 socialType 非空,说明需要绑定社交用户
- if (reqVO.getSocialType() != null) {
- socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
- reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
- }
- // 创建 Token 令牌,记录登录日志
- return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
- }else{
- throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR);
+ // 使用账号密码,进行登录
+ AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
+
+ // 如果 socialType 非空,说明需要绑定社交用户
+ if (reqVO.getSocialType() != null) {
+ socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
+ reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
}
+ // 创建 Token 令牌,记录登录日志
+ return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
}
@Override
@@ -172,6 +177,25 @@ public class AdminAuthServiceImpl implements AdminAuthService {
return AuthConvert.INSTANCE.convert(accessTokenDO);
}
+ @VisibleForTesting
+ void verifyCaptcha(AuthLoginReqVO reqVO) {
+ // 如果验证码关闭,则不进行校验
+ if (!captchaEnable) {
+ return;
+ }
+ // 校验验证码
+ ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class);
+ CaptchaVO captchaVO = new CaptchaVO();
+ captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
+ ResponseModel response = captchaService.verification(captchaVO);
+ // 验证不通过
+ if (!response.isSuccess()) {
+ // 创建登录失败日志(验证码不正确)
+ createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);
+ throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());
+ }
+ }
+
private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
// 插入登陆日志
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index e510a68ca..85d52550d 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -61,38 +61,20 @@ mybatis-plus:
aj:
captcha:
- # 滑动验证,底图路径,不配置将使用默认图片
- # 支持全路径
- # 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/jigsaw
- jigsaw: classpath:images/jigsaw
- #滑动验证,底图路径,不配置将使用默认图片
- ##支持全路径
- # 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/pic-click
- pic-click: classpath:images/pic-click
- # 缓存local/redis...
- cache-type: redis
- # local缓存的阈值,达到这个值,清除缓存
- cache-number: 1000
- # local定时清除过期缓存(单位秒),设置为0代表不执行
- timing-clear: 180
- # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
- type: blockPuzzle
- # 右下角水印文字(我的水印)https://tool.chinaz.com/tools/unicode.aspx 中文转Unicode Linux可能需要转unicode
- water-mark: 芋道源码
- # 滑动干扰项(0/1/2)
- interference-options: 2
- # 接口请求次数一分钟限制是否开启 true|false
- req-frequency-limit-enable: true
- # 验证失败5次,get接口锁定
- req-get-lock-limit: 5
- # 验证失败后,锁定时间间隔,s
- req-get-lock-seconds: 10
- # get接口一分钟内请求数限制
- req-get-minute-limit: 30
- # check接口一分钟内请求数限制
- req-check-minute-limit: 60
- # verify接口一分钟内请求数限制
- req-verify-minute-limit: 60
+ jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
+ pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
+ cache-type: redis # 缓存 local/redis...
+ cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存
+ timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行
+ type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
+ water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode
+ interference-options: 2 # 滑动干扰项(0/1/2)
+ req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false
+ req-get-lock-limit: 5 # 验证失败5次,get接口锁定
+ req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔
+ req-get-minute-limit: 30 # get 接口一分钟内请求数限制
+ req-check-minute-limit: 60 # check 接口一分钟内请求数限制
+ req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制
--- #################### 芋道相关配置 ####################
@@ -112,9 +94,7 @@ yudao:
version: ${yudao.info.version}
base-package: ${yudao.info.base-package}
captcha:
- timeout: 5m
- width: 160
- height: 60
+ enable: true # 验证码的开关,默认为 true;注意,优先读取数据库 infra_config 的 yudao.captcha.enable,所以请从数据库修改,可能需要重启项目
codegen:
base-package: ${yudao.info.base-package}
db-schemas: ${spring.datasource.dynamic.datasource.master.name}
@@ -134,8 +114,7 @@ yudao:
- /admin-api/infra/file/*/get/** # 获取图片,和租户无关
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
- /app-api/pay/order/notify/* # 支付回调通知,不携带租户编号
-# - /jmreport/list
- - /jmreport/*
+ - /jmreport/* # 积木报表,无法携带租户编号
ignore-tables:
- system_tenant
- system_tenant_package