Commit 926c75d29a6e956c12e5e6616349f9f35692fdc0
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
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring.factories
0 → 100644
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
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 | ... | ... |