diff --git a/pom.xml b/pom.xml index 58672cae4d5cbf83bca43bdfca288061a5891229..57ab3931d4b0a5a8920b9068e1ced5b1539334b6 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,8 @@ 8.7.2-20241022 7.0.1 + + 1.5.1 3.2.2 diff --git a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java index 1a476a94a66de72f32a4e252ca994db1a106a05b..f6986c85083f111ae3851cabbe13d4c480d26e10 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java @@ -1,5 +1,10 @@ package org.dromara.web.controller; +import cloud.tianai.captcha.application.ImageCaptchaApplication; +import cloud.tianai.captcha.application.vo.CaptchaResponse; +import cloud.tianai.captcha.application.vo.ImageCaptchaVO; +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.common.response.ApiResponse; import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.generator.CodeGenerator; @@ -11,6 +16,7 @@ import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.domain.R; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.reflect.ReflectUtils; @@ -21,19 +27,26 @@ import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.web.config.properties.CaptchaProperties; import org.dromara.common.web.enums.CaptchaType; +import org.dromara.common.web.enums.InputCaptchaType; import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.core.factory.SmsFactory; +import org.dromara.web.domain.bo.CaptchaBo; import org.dromara.web.domain.vo.CaptchaVo; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.concurrent.ThreadLocalRandom; /** * 验证码操作处理 @@ -50,6 +63,8 @@ public class CaptchaController { private final CaptchaProperties captchaProperties; private final MailProperties mailProperties; + private final ImageCaptchaApplication imageCaptchaApplication; + /** * 短信验证码 * @@ -109,15 +124,20 @@ public class CaptchaController { captchaVo.setCaptchaEnabled(false); return R.ok(captchaVo); } + captchaVo.setType(captchaProperties.getType()); + if (CaptchaType.ACT.getType().equalsIgnoreCase(captchaProperties.getType())) { + return R.ok(captchaVo); + } // 保存验证码信息 String uuid = IdUtil.simpleUUID(); String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; // 生成验证码 - CaptchaType captchaType = captchaProperties.getType(); - boolean isMath = CaptchaType.MATH == captchaType; - Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength(); - CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length); - AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); + CaptchaProperties.InputCaptchaProperties inputCaptchaProperties = captchaProperties.getInput(); + InputCaptchaType inputCaptchaType = inputCaptchaProperties.getType(); + boolean isMath = InputCaptchaType.MATH == inputCaptchaType; + Integer length = isMath ? inputCaptchaProperties.getNumberLength() : inputCaptchaProperties.getCharLength(); + CodeGenerator codeGenerator = ReflectUtils.newInstance(inputCaptchaType.getClazz(), length); + AbstractCaptcha captcha = SpringUtils.getBean(inputCaptchaProperties.getCategory().getClazz()); captcha.setGenerator(codeGenerator); captcha.createCode(); // 如果是数学验证码,使用SpEL表达式处理验证码结果 @@ -133,4 +153,33 @@ public class CaptchaController { return R.ok(captchaVo); } + /** + * 生成行为验证码 + */ + @RateLimiter(time = 60, count = 10, limitType = LimitType.IP) + @PostMapping("/auth/captcha") + public CaptchaResponse getCaptcha() { + if (!captchaProperties.getEnable() || !CaptchaType.ACT.getType().equalsIgnoreCase(captchaProperties.getType())) { + throw new ServiceException("验证码未开启"); + } + String type = captchaProperties.getAct().getType(); + if ("RANDOM".equalsIgnoreCase(type)) { + type = Arrays.asList(CaptchaTypeConstant.SLIDER, CaptchaTypeConstant.CONCAT, CaptchaTypeConstant.ROTATE, CaptchaTypeConstant.WORD_IMAGE_CLICK) + .get(ThreadLocalRandom.current().nextInt(4)); + } + return imageCaptchaApplication.generateCaptcha(type); + } + + /** + * 校验行为验证码 + */ + @PostMapping("/auth/verify") + public ApiResponse verify(@RequestBody CaptchaBo data) { + ApiResponse response = imageCaptchaApplication.matching(data.getId(), data.getData()); + if (response.isSuccess()) { + return ApiResponse.ofSuccess(Collections.singletonMap("id", data.getId())); + } + return response; + } + } diff --git a/ruoyi-admin/src/main/java/org/dromara/web/domain/bo/CaptchaBo.java b/ruoyi-admin/src/main/java/org/dromara/web/domain/bo/CaptchaBo.java new file mode 100644 index 0000000000000000000000000000000000000000..67c51c69bb0784ee9b1507b833ef2cdf924fcb3d --- /dev/null +++ b/ruoyi-admin/src/main/java/org/dromara/web/domain/bo/CaptchaBo.java @@ -0,0 +1,13 @@ +package org.dromara.web.domain.bo; + +import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; +import lombok.Data; + +@Data +public class CaptchaBo { + + private String id; + + private ImageCaptchaTrack data; + +} diff --git a/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/CaptchaVo.java b/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/CaptchaVo.java index 664df1ee62d7ff7555ad9ab0d843197bfe0e5b32..4839515e8258997a243fb22c6ed5d03c2c595bce 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/CaptchaVo.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/domain/vo/CaptchaVo.java @@ -15,6 +15,11 @@ public class CaptchaVo { */ private Boolean captchaEnabled = true; + /** + * 验证码类型 + */ + private String type; + private String uuid; /** diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java index e8e60e1c4aeba23d79209fe1acf0b552305cf7b1..64d937b89f324dcdbd47ac9f3ee192b629ec320c 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java @@ -1,5 +1,7 @@ package org.dromara.web.service.impl; +import cloud.tianai.captcha.application.ImageCaptchaApplication; +import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication; import cn.dev33.satoken.secure.BCrypt; import cn.dev33.satoken.stp.SaLoginModel; import cn.dev33.satoken.stp.StpUtil; @@ -24,6 +26,7 @@ import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.common.web.config.properties.CaptchaProperties; +import org.dromara.common.web.enums.CaptchaType; import org.dromara.system.domain.SysUser; import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysUserVo; @@ -43,6 +46,8 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class PasswordAuthStrategy implements IAuthStrategy { + private final ImageCaptchaApplication imageCaptchaApplication; + private final CaptchaProperties captchaProperties; private final SysLoginService loginService; private final SysUserMapper userMapper; @@ -95,6 +100,13 @@ public class PasswordAuthStrategy implements IAuthStrategy { * @param uuid 唯一标识 */ private void validateCaptcha(String tenantId, String username, String code, String uuid) { + if (CaptchaType.ACT.getType().equalsIgnoreCase(captchaProperties.getType()) && imageCaptchaApplication instanceof SecondaryVerificationApplication) { + if (!((SecondaryVerificationApplication) imageCaptchaApplication).secondaryVerification(uuid)) { + loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); + throw new CaptchaException(); + } + return; + } String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, ""); String captcha = RedisUtils.getCacheObject(verifyKey); RedisUtils.deleteObject(verifyKey); diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 33fbcb7abfb9624133f0c36337d34c64e1f42de1..a42a50db5e79bc06456d9d4d4c5fe098d5a69d02 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -7,18 +7,6 @@ ruoyi: # 版权年份 copyrightYear: 2024 -captcha: - enable: true - # 页面 <参数设置> 可开启关闭 验证码校验 - # 验证码类型 math 数组计算 char 字符验证 - type: MATH - # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 - category: CIRCLE - # 数字验证码位数 - numberLength: 1 - # 字符验证码长度 - charLength: 4 - # 开发环境配置 server: # 服务器的HTTP端口,默认为8080 @@ -109,6 +97,24 @@ sa-token: # security配置 security: + #验证码 + captcha: + # 是否开启验证码 + enable: true + # 验证码类型 input 输入验证码 act 行为验证码 + type: act + input: + # 验证码类型 math 数组计算 char 字符验证 + type: char + # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 random 随机行为验证码 + category: line + # 数字验证码位数 + numberLength: 1 + # 字符验证码长度 + charLength: 4 + act: + # RANDOM 随机 SLIDER 滑块验证 ROTATE 旋转验证 CONCAT 滑动还原 WORD_IMAGE_CLICK 文字点选 + type: RANDOM # 排除路径 excludes: - /*.html @@ -120,6 +126,37 @@ security: - /*/api-docs - /*/api-docs/** +# 滑块验证码配置, 详细请看 cloud.tianai.captcha.autoconfiguration.ImageCaptchaProperties 类 +captcha: + # 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider + prefix: "captcha_act:" + # 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整 + expire: + # 默认缓存时间 2分钟 + default: 20000 + # 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些 + WORD_IMAGE_CLICK: 30000 + # 使用加载系统自带的资源, 默认是 false(这里系统的默认资源包含 滑动验证码模板/旋转验证码模板,如果想使用系统的模板,这里设置为true) + init-default-resource: true + # 缓存控制, 默认为false不开启 + local-cache-enabled: true + # 验证码会提前缓存一些生成好的验证数据, 默认是20 + local-cache-size: 20 + # 缓存拉取失败后等待时间 默认是 5秒钟 + local-cache-wait-time: 5000 + # 缓存检查间隔 默认是2秒钟 + local-cache-period: 2000 + # 配置字体包,供文字点选验证码使用,可以配置多个,不配置使用默认的字体 + font-path: +# - classpath:font/SimHei.ttf + secondary: + # 二次验证, 默认false 不开启 + enabled: true + # 二次验证过期时间, 默认 2分钟 + expire: 120000 + # 二次验证缓存key前缀,默认是 captcha:secondary + keyPrefix: "captcha_secondary:" + # 多租户配置 tenant: # 是否开启 diff --git a/ruoyi-admin/src/main/resources/bgimages/1.jpg b/ruoyi-admin/src/main/resources/bgimages/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c91e2b15cb16975f2ecb32c5e1a56fddad49af3 Binary files /dev/null and b/ruoyi-admin/src/main/resources/bgimages/1.jpg differ diff --git a/ruoyi-admin/src/main/resources/bgimages/2.jpg b/ruoyi-admin/src/main/resources/bgimages/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a323a0d652bebe4d190203efe52f32220ba30def Binary files /dev/null and b/ruoyi-admin/src/main/resources/bgimages/2.jpg differ diff --git a/ruoyi-admin/src/main/resources/bgimages/3.jpg b/ruoyi-admin/src/main/resources/bgimages/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..023696896f76d0e83a6840fa50dc6ab1a93cc178 Binary files /dev/null and b/ruoyi-admin/src/main/resources/bgimages/3.jpg differ diff --git a/ruoyi-admin/src/main/resources/bgimages/4.jpg b/ruoyi-admin/src/main/resources/bgimages/4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d25dfce168180898bc1e521ddccc6ea011a4debd Binary files /dev/null and b/ruoyi-admin/src/main/resources/bgimages/4.jpg differ diff --git a/ruoyi-admin/src/main/resources/bgimages/5.jpg b/ruoyi-admin/src/main/resources/bgimages/5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9dbcd1244be638a6a58d2b86197885729b9e85f8 Binary files /dev/null and b/ruoyi-admin/src/main/resources/bgimages/5.jpg differ diff --git a/ruoyi-common/ruoyi-common-web/pom.xml b/ruoyi-common/ruoyi-common-web/pom.xml index b250fa9d09267bb64050b0bc096c4271fa3698fd..dd8d025cb127c280d9fd8076422451ad3b37c51b 100644 --- a/ruoyi-common/ruoyi-common-web/pom.xml +++ b/ruoyi-common/ruoyi-common-web/pom.xml @@ -57,6 +57,12 @@ cn.hutool hutool-crypto + + + cloud.tianai.captcha + tianai-captcha-springboot-starter + ${tianai-captcha.version} + diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaResourceConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaResourceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..de0f88d2c130d535694bb74c3e05a259f5b470f0 --- /dev/null +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/CaptchaResourceConfig.java @@ -0,0 +1,44 @@ +package org.dromara.common.web.config; + +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.resource.ResourceStore; +import cloud.tianai.captcha.resource.common.model.dto.Resource; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; + + +@Configuration +@RequiredArgsConstructor +public class CaptchaResourceConfig { + + private final ResourceStore resourceStore; + + @PostConstruct + public void init() { + // 添加自定义背景图片 + resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/1.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/2.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/3.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/4.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/5.jpg", "default")); + + resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/1.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/2.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/3.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/4.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/5.jpg", "default")); + + resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/1.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/2.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/3.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/4.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/5.jpg", "default")); + + resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/1.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/2.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/3.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/4.jpg", "default")); + resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/5.jpg", "default")); + } +} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java index bfc52f4502d13303bf4f6a649f91f95e000fe668..48542107101e5880770c63f5ff5fda803c540cbf 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CaptchaProperties.java @@ -1,8 +1,8 @@ package org.dromara.common.web.config.properties; import org.dromara.common.web.enums.CaptchaCategory; -import org.dromara.common.web.enums.CaptchaType; import lombok.Data; +import org.dromara.common.web.enums.InputCaptchaType; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -11,7 +11,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Lion Li */ @Data -@ConfigurationProperties(prefix = "captcha") +@ConfigurationProperties(prefix = "security.captcha") public class CaptchaProperties { private Boolean enable; @@ -19,20 +19,48 @@ public class CaptchaProperties { /** * 验证码类型 */ - private CaptchaType type; + private String type; /** - * 验证码类别 + * 输入验证码的相关配置 */ - private CaptchaCategory category; + private InputCaptchaProperties input; /** - * 数字验证码位数 + * 行为验证码的相关配置 */ - private Integer numberLength; + private ActCaptchaProperties act; - /** - * 字符验证码长度 - */ - private Integer charLength; + @Data + public static class InputCaptchaProperties { + + /** + * 验证码类型 math 数组计算, char 字符验证 + */ + private InputCaptchaType type; + + /** + * 验证码类别 line 线段干扰, circle 圆圈干扰, shear 扭曲干扰, random 随机行为验证码 + */ + private CaptchaCategory category; + + /** + * 数字验证码位数 + */ + private Integer numberLength; + + /** + * 字符验证码长度 + */ + private Integer charLength; + } + + @Data + public static class ActCaptchaProperties { + + /** + * 行为验证码类型 random 随机行为验证码 + */ + private String type; + } } diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java index d0b73345ac305cc02e1161fa2aabd4ca425a2463..627d000021467eb62327ede0134caf8c32d1ed27 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/CaptchaType.java @@ -1,8 +1,5 @@ package org.dromara.common.web.enums; -import cn.hutool.captcha.generator.CodeGenerator; -import cn.hutool.captcha.generator.RandomGenerator; -import org.dromara.common.web.utils.UnsignedMathGenerator; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,12 +15,12 @@ public enum CaptchaType { /** * 数字 */ - MATH(UnsignedMathGenerator.class), + INPUT("INPUT"), /** * 字符 */ - CHAR(RandomGenerator.class); + ACT("ACT"); - private final Class clazz; + private final String type; } diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/InputCaptchaType.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/InputCaptchaType.java new file mode 100644 index 0000000000000000000000000000000000000000..8df3a0ce5fa862391647a9ecbc7d5fa5b1e669e6 --- /dev/null +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/enums/InputCaptchaType.java @@ -0,0 +1,29 @@ +package org.dromara.common.web.enums; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.dromara.common.web.utils.UnsignedMathGenerator; + +/** + * 验证码类型 + * + * @author Lion Li + */ +@Getter +@AllArgsConstructor +public enum InputCaptchaType { + + /** + * 数字 + */ + MATH(UnsignedMathGenerator.class), + + /** + * 字符 + */ + CHAR(RandomGenerator.class); + + private final Class clazz; +}