Commit 926c75d29a6e956c12e5e6616349f9f35692fdc0

Authored by YunaiV
1 parent 020535ab

优化图片验证码的后端实现

Showing 13 changed files with 117 additions and 121 deletions
yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
... ... @@ -12,8 +12,8 @@
12 12 <packaging>jar</packaging>
13 13  
14 14 <name>${project.artifactId}</name>
15   - <description>
16   - 验证码
  15 + <description>验证码拓展
  16 + 1. 基于 aj-captcha 实现图形验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/
17 17 </description>
18 18  
19 19 <dependencies>
... ... @@ -23,9 +23,10 @@
23 23 <artifactId>spring-boot-starter</artifactId>
24 24 </dependency>
25 25  
  26 + <!-- DB 相关 -->
26 27 <dependency>
27   - <groupId>cn.iocoder.boot</groupId>
28   - <artifactId>yudao-spring-boot-starter-redis</artifactId>
  28 + <groupId>org.springframework.boot</groupId>
  29 + <artifactId>spring-boot-starter-data-redis</artifactId>
29 30 </dependency>
30 31  
31 32 <!-- 验证码相关 -->
... ... @@ -33,7 +34,6 @@
33 34 <groupId>com.anji-plus</groupId>
34 35 <artifactId>spring-boot-starter-captcha</artifactId>
35 36 </dependency>
36   -
37 37 </dependencies>
38 38  
39 39 </project>
... ...
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/config/YudaoCaptchaConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.captcha.config;
  2 +
  3 +import cn.hutool.core.util.ClassUtil;
  4 +import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants;
  5 +import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl;
  6 +import com.anji.captcha.service.CaptchaCacheService;
  7 +import org.springframework.context.annotation.Bean;
  8 +import org.springframework.context.annotation.Configuration;
  9 +import org.springframework.data.redis.core.StringRedisTemplate;
  10 +
  11 +@Configuration
  12 +public class YudaoCaptchaConfiguration {
  13 +
  14 + static {
  15 + // 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到
  16 + // 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举
  17 + ClassUtil.loadClass(CaptchaRedisKeyConstants.class.getName());
  18 + }
  19 +
  20 + @Bean
  21 + public CaptchaCacheService captchaCacheService(StringRedisTemplate stringRedisTemplate) {
  22 + return new RedisCaptchaServiceImpl(stringRedisTemplate);
  23 + }
  24 +
  25 +}
... ...
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/captcha/core/service/CaptchaServiceImpl.java renamed to yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/service/RedisCaptchaServiceImpl.java
1   -package cn.iocoder.yudao.captcha.core.service;
  1 +package cn.iocoder.yudao.framework.captcha.core.service;
2 2  
3 3 import com.anji.captcha.service.CaptchaCacheService;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.NoArgsConstructor;
  6 +import lombok.RequiredArgsConstructor;
4 7 import org.springframework.data.redis.core.StringRedisTemplate;
5   -import org.springframework.stereotype.Service;
6 8  
7 9 import javax.annotation.Resource;
8 10 import java.util.concurrent.TimeUnit;
9 11  
10   -@Service
11   -public class CaptchaServiceImpl implements CaptchaCacheService {
  12 +/**
  13 + * 基于 Redis 实现验证码的存储
  14 + *
  15 + * @author 星语
  16 + */
  17 +@NoArgsConstructor // 保证 aj-captcha 的 SPI 创建
  18 +@AllArgsConstructor
  19 +public class RedisCaptchaServiceImpl implements CaptchaCacheService {
  20 +
  21 + @Resource // 保证 aj-captcha 的 SPI 创建时的注入
  22 + private StringRedisTemplate stringRedisTemplate;
12 23  
13 24 @Override
14 25 public String type() {
15 26 return "redis";
16 27 }
17 28  
18   - @Resource
19   - private StringRedisTemplate stringRedisTemplate;
20   -
21 29 @Override
22 30 public void set(String key, String value, long expiresInSeconds) {
23 31 stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
... ... @@ -42,4 +50,5 @@ public class CaptchaServiceImpl implements CaptchaCacheService {
42 50 public Long increment(String key, long val) {
43 51 return stringRedisTemplate.opsForValue().increment(key,val);
44 52 }
  53 +
45 54 }
... ...
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/package-info.java 0 → 100644
  1 +/**
  2 + * 验证码拓展
  3 + * 1. 基于 aj-captcha 实现图形验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/
  4 + *
  5 + * @author 星语
  6 + */
  7 +package cn.iocoder.yudao.framework.captcha;
... ...
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService
1   -cn.iocoder.yudao.captcha.core.service.CaptchaServiceImpl
2 1 \ No newline at end of file
  2 +cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl
... ...
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring.factories 0 → 100644
  1 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2 + cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration
... ...
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 {
12 12 // ========== AUTH 模块 1002000000 ==========
13 13 ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确");
14 14 ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用");
15   - ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在");
16   - ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确");
  15 + ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确,原因:{}");
17 16 ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定");
18 17 ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1002000006, "Token 已经过期");
19 18 ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1002000007, "手机号不存在");
... ...
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 {
33 33 @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
34 34 private String password;
35 35  
36   - @ApiModelProperty(value = "验证码", required = true, example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==")
37   - @NotEmpty(message = "验证码不能为空")
  36 + @ApiModelProperty(value = "验证码", required = true,
  37 + example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==",
  38 + notes = "验证码开启时,需要传递")
  39 + @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class)
38 40 private String captchaVerification;
39 41  
40 42 // ========== 绑定社交登录时,需要传递如下参数 ==========
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java deleted
1   -package cn.iocoder.yudao.module.system.framework.captcha.config;
2   -
3   -import org.springframework.boot.context.properties.EnableConfigurationProperties;
4   -import org.springframework.context.annotation.Configuration;
5   -
6   -@Configuration
7   -@EnableConfigurationProperties(CaptchaProperties.class)
8   -public class CaptchaConfig {
9   -}
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java deleted
1   -package cn.iocoder.yudao.module.system.framework.captcha.config;
2   -
3   -import lombok.Data;
4   -import org.springframework.boot.context.properties.ConfigurationProperties;
5   -import org.springframework.validation.annotation.Validated;
6   -
7   -import javax.validation.constraints.NotNull;
8   -import java.time.Duration;
9   -
10   -@ConfigurationProperties(prefix = "yudao.captcha")
11   -@Validated
12   -@Data
13   -public class CaptchaProperties {
14   -
15   - private static final Boolean ENABLE_DEFAULT = true;
16   -
17   - /**
18   - * 是否开启
19   - * 注意,这里仅仅是后端 Server 是否校验,暂时不控制前端的逻辑
20   - */
21   - private Boolean enable = ENABLE_DEFAULT;
22   - /**
23   - * 验证码的过期时间
24   - */
25   - @NotNull(message = "验证码的过期时间不为空")
26   - private Duration timeout;
27   - /**
28   - * 验证码的高度
29   - */
30   - @NotNull(message = "验证码的高度不能为空")
31   - private Integer height;
32   - /**
33   - * 验证码的宽度
34   - */
35   - @NotNull(message = "验证码的宽度不能为空")
36   - private Integer width;
37   -
38   -}
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java deleted
1   -/**
2   - * 基于 Hutool captcha 库,实现验证码功能
3   - */
4   -package cn.iocoder.yudao.module.system.framework.captcha;
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;
5 5 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
6 6 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
7 7 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
  8 +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
8 9 import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
9 10 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
10 11 import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
... ... @@ -24,7 +25,9 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService;
24 25 import com.anji.captcha.model.common.ResponseModel;
25 26 import com.anji.captcha.model.vo.CaptchaVO;
26 27 import com.anji.captcha.service.CaptchaService;
  28 +import com.google.common.annotations.VisibleForTesting;
27 29 import lombok.extern.slf4j.Slf4j;
  30 +import org.springframework.beans.factory.annotation.Value;
28 31 import org.springframework.stereotype.Service;
29 32  
30 33 import javax.annotation.Resource;
... ... @@ -61,6 +64,12 @@ public class AdminAuthServiceImpl implements AdminAuthService {
61 64 @Resource
62 65 private SmsCodeApi smsCodeApi;
63 66  
  67 + /**
  68 + * 验证码的开关,默认为 true
  69 + */
  70 + @Value("${yudao.captcha.enable:true}")
  71 + private Boolean captchaEnable;
  72 +
64 73 @Override
65 74 public AdminUserDO authenticate(String username, String password) {
66 75 final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
... ... @@ -84,23 +93,19 @@ public class AdminAuthServiceImpl implements AdminAuthService {
84 93  
85 94 @Override
86 95 public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
87   - CaptchaVO captchaVO = new CaptchaVO();
88   - captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
89   - ResponseModel response = captchaService.verification(captchaVO);
90   - if(response.isSuccess()){
91   - // 使用账号密码,进行登录
92   - AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
93   -
94   - // 如果 socialType 非空,说明需要绑定社交用户
95   - if (reqVO.getSocialType() != null) {
96   - socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
97   - reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
98   - }
99   - // 创建 Token 令牌,记录登录日志
100   - return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
101   - }else{
102   - throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR);
  96 + // 校验验证码
  97 + verifyCaptcha(reqVO);
  98 +
  99 + // 使用账号密码,进行登录
  100 + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
  101 +
  102 + // 如果 socialType 非空,说明需要绑定社交用户
  103 + if (reqVO.getSocialType() != null) {
  104 + socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
  105 + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
103 106 }
  107 + // 创建 Token 令牌,记录登录日志
  108 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
104 109 }
105 110  
106 111 @Override
... ... @@ -172,6 +177,25 @@ public class AdminAuthServiceImpl implements AdminAuthService {
172 177 return AuthConvert.INSTANCE.convert(accessTokenDO);
173 178 }
174 179  
  180 + @VisibleForTesting
  181 + void verifyCaptcha(AuthLoginReqVO reqVO) {
  182 + // 如果验证码关闭,则不进行校验
  183 + if (!captchaEnable) {
  184 + return;
  185 + }
  186 + // 校验验证码
  187 + ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class);
  188 + CaptchaVO captchaVO = new CaptchaVO();
  189 + captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
  190 + ResponseModel response = captchaService.verification(captchaVO);
  191 + // 验证不通过
  192 + if (!response.isSuccess()) {
  193 + // 创建登录失败日志(验证码不正确)
  194 + createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);
  195 + throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());
  196 + }
  197 + }
  198 +
175 199 private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
176 200 // 插入登陆日志
177 201 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -61,38 +61,20 @@ mybatis-plus:
61 61  
62 62 aj:
63 63 captcha:
64   - # 滑动验证,底图路径,不配置将使用默认图片
65   - # 支持全路径
66   - # 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/jigsaw
67   - jigsaw: classpath:images/jigsaw
68   - #滑动验证,底图路径,不配置将使用默认图片
69   - ##支持全路径
70   - # 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/pic-click
71   - pic-click: classpath:images/pic-click
72   - # 缓存local/redis...
73   - cache-type: redis
74   - # local缓存的阈值,达到这个值,清除缓存
75   - cache-number: 1000
76   - # local定时清除过期缓存(单位秒),设置为0代表不执行
77   - timing-clear: 180
78   - # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
79   - type: blockPuzzle
80   - # 右下角水印文字(我的水印)https://tool.chinaz.com/tools/unicode.aspx 中文转Unicode Linux可能需要转unicode
81   - water-mark: 芋道源码
82   - # 滑动干扰项(0/1/2)
83   - interference-options: 2
84   - # 接口请求次数一分钟限制是否开启 true|false
85   - req-frequency-limit-enable: true
86   - # 验证失败5次,get接口锁定
87   - req-get-lock-limit: 5
88   - # 验证失败后,锁定时间间隔,s
89   - req-get-lock-seconds: 10
90   - # get接口一分钟内请求数限制
91   - req-get-minute-limit: 30
92   - # check接口一分钟内请求数限制
93   - req-check-minute-limit: 60
94   - # verify接口一分钟内请求数限制
95   - req-verify-minute-limit: 60
  64 + jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
  65 + pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
  66 + cache-type: redis # 缓存 local/redis...
  67 + cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存
  68 + timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行
  69 + type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
  70 + water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode
  71 + interference-options: 2 # 滑动干扰项(0/1/2)
  72 + req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false
  73 + req-get-lock-limit: 5 # 验证失败5次,get接口锁定
  74 + req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔
  75 + req-get-minute-limit: 30 # get 接口一分钟内请求数限制
  76 + req-check-minute-limit: 60 # check 接口一分钟内请求数限制
  77 + req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制
96 78  
97 79 --- #################### 芋道相关配置 ####################
98 80  
... ... @@ -112,9 +94,7 @@ yudao:
112 94 version: ${yudao.info.version}
113 95 base-package: ${yudao.info.base-package}
114 96 captcha:
115   - timeout: 5m
116   - width: 160
117   - height: 60
  97 + enable: true # 验证码的开关,默认为 true;注意,优先读取数据库 infra_config 的 yudao.captcha.enable,所以请从数据库修改,可能需要重启项目
118 98 codegen:
119 99 base-package: ${yudao.info.base-package}
120 100 db-schemas: ${spring.datasource.dynamic.datasource.master.name}
... ... @@ -134,8 +114,7 @@ yudao:
134 114 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关
135 115 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
136 116 - /app-api/pay/order/notify/* # 支付回调通知,不携带租户编号
137   -# - /jmreport/list
138   - - /jmreport/*
  117 + - /jmreport/* # 积木报表,无法携带租户编号
139 118 ignore-tables:
140 119 - system_tenant
141 120 - system_tenant_package
... ...