feat 接入tianai行为验证码

This commit is contained in:
疯狂的牛子Li 2024-12-11 10:51:34 +08:00
parent 552e543471
commit 4ec0812066
16 changed files with 256 additions and 34 deletions

View File

@ -51,6 +51,8 @@
<anyline.version>8.7.2-20241022</anyline.version>
<!--工作流配置-->
<flowable.version>7.0.1</flowable.version>
<!-- 行为验证码配置 -->
<tianai-captcha.version>1.5.1</tianai-captcha.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>

View File

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

View File

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

View File

@ -15,6 +15,11 @@ public class CaptchaVo {
*/
private Boolean captchaEnabled = true;
/**
* 验证码类型
*/
private String type;
private String uuid;
/**

View File

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

View File

@ -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:
# 是否开启

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -57,6 +57,12 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>${tianai-captcha.version}</version>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -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<? extends CodeGenerator> clazz;
private final String type;
}

View File

@ -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<? extends CodeGenerator> clazz;
}