Browse Source

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

YunaiV 2 years ago
parent
commit
926c75d29a
13 changed files with 117 additions and 121 deletions
  1. 5 5
      yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
  2. 25 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/config/YudaoCaptchaConfiguration.java
  3. 16 7
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/service/RedisCaptchaServiceImpl.java
  4. 7 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/package-info.java
  5. 1 1
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService
  6. 2 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring.factories
  7. 1 2
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  8. 4 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
  9. 0 9
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java
  10. 0 38
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java
  11. 0 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java
  12. 40 16
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
  13. 16 37
      yudao-server/src/main/resources/application.yaml

+ 5 - 5
yudao-framework/yudao-spring-boot-starter-captcha/pom.xml

@@ -12,8 +12,8 @@
     <packaging>jar</packaging>
 
     <name>${project.artifactId}</name>
-    <description>
-        验证码
+    <description>验证码拓展
+        1. 基于 aj-captcha 实现图形验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/
     </description>
 
     <dependencies>
@@ -23,9 +23,10 @@
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
 
+        <!-- DB 相关 -->
         <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-redis</artifactId>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
 
         <!-- 验证码相关 -->
@@ -33,7 +34,6 @@
             <groupId>com.anji-plus</groupId>
             <artifactId>spring-boot-starter-captcha</artifactId>
         </dependency>
-
     </dependencies>
 
 </project>

+ 25 - 0
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);
+    }
+
+}

+ 16 - 7
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/captcha/core/service/CaptchaServiceImpl.java → 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);
     }
+
 }

+ 7 - 0
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;

+ 1 - 1
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
+cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl

+ 2 - 0
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

+ 1 - 2
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, "手机号不存在");

+ 4 - 2
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;
 
     // ========== 绑定社交登录时,需要传递如下参数 ==========

+ 0 - 9
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaConfig.java

@@ -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 {
-}

+ 0 - 38
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/config/CaptchaProperties.java

@@ -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;
-
-}

+ 0 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/package-info.java

@@ -1,4 +0,0 @@
-/**
- * 基于 Hutool captcha 库,实现验证码功能
- */
-package cn.iocoder.yudao.module.system.framework.captcha;

+ 40 - 16
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());
-
-            // 如果 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);
+        // 校验验证码
+        verifyCaptcha(reqVO);
+
+        // 使用账号密码,进行登录
+        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);

+ 16 - 37
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