Merge remote-tracking branch 'origin/master'

This commit is contained in:
天爱有情
2025-12-30 09:10:57 +08:00
15 changed files with 589 additions and 4 deletions
+1
View File
@@ -13,6 +13,7 @@
<module>tianai-captcha</module>
<module>tianai-captcha-springboot-starter</module>
<module>tianai-captcha-solon-plugin</module>
<module>tianai-captcha-springboot4-starter</module>
</modules>
<properties>
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>tianai-captcha-springboot4-starter</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<commons-lang3.version>3.18.0</commons-lang3.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>4.0.0</version>
<type>pom</type>
<scope>import</scope>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>${revision}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>4.0.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,63 @@
package cloud.tianai.captcha.spring4.autoconfiguration;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.spring4.store.impl.RedisCacheStore;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 缓存存储器的自动配置类
*
* @author lichenpark
*/
@AutoConfigureAfter({DataRedisAutoConfiguration.class})
@Configuration(proxyBeanMethods = false)
public class CacheStoreAutoConfiguration {
/**
* RedisCacheStoreConfiguration
*
* @author 天爱有情
* @since 2020/10/27 14:06
*/
@Order(1)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(StringRedisTemplate.class)
public static class RedisCacheStoreConfiguration {
@Bean
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnMissingBean(CacheStore.class)
public CacheStore redis(StringRedisTemplate redisTemplate) {
return new RedisCacheStore(redisTemplate);
}
}
/**
* LocalCacheStoreConfiguration
*
* @author 天爱有情
* @since 2020/10/27 14:06
*/
@Order(2)
@Configuration(proxyBeanMethods = false)
public static class LocalCacheStoreConfiguration {
@Bean
@ConditionalOnMissingBean(CacheStore.class)
public CacheStore local() {
return new LocalCacheStore();
}
}
}
@@ -0,0 +1,131 @@
package cloud.tianai.captcha.spring4.autoconfiguration;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.TACBuilder;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.CaptchaInterceptorGroup;
import cloud.tianai.captcha.interceptor.impl.ParamCheckCaptchaInterceptor;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.ResourceProviders;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import cloud.tianai.captcha.spring4.plugins.SpringMultiImageCaptchaGenerator;
import cloud.tianai.captcha.spring4.plugins.secondary.SecondaryVerificationApplication;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.Order;
/**
* @Author: 天爱有情
* @Date 2020/5/29 9:49
* @Description 滑块验证码自动装配
*/
@Slf4j
@Order
@Configuration
@AutoConfigureAfter(CacheStoreAutoConfiguration.class)
@EnableConfigurationProperties({SpringImageCaptchaProperties.class})
public class ImageCaptchaAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ResourceStore resourceStore() {
return new LocalMemoryResourceStore();
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaResourceManager imageCaptchaResourceManager(ResourceStore resourceStore) {
ResourceProviders resourceProviders = new ResourceProviders();
return new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders);
}
@Bean
@ConditionalOnMissingBean
public ImageTransform imageTransform() {
return new Base64ImageTransform();
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaGenerator imageCaptchaTemplate(SpringImageCaptchaProperties prop,
ImageCaptchaResourceManager captchaResourceManager,
ImageTransform imageTransform,
BeanFactory beanFactory) {
return new SpringMultiImageCaptchaGenerator(captchaResourceManager, imageTransform, beanFactory);
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaValidator imageCaptchaValidator() {
return new SimpleImageCaptchaValidator();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean
public CaptchaInterceptor captchaInterceptor() {
CaptchaInterceptorGroup group = new CaptchaInterceptorGroup();
group.addInterceptor(new ParamCheckCaptchaInterceptor());
// group.addInterceptor(new BasicTrackCaptchaInterceptor());
return group;
}
@Bean
@ConditionalOnMissingBean
public ImageCaptchaApplication imageCaptchaApplication(ImageCaptchaGenerator captchaGenerator,
ImageCaptchaValidator imageCaptchaValidator,
CacheStore cacheStore,
ResourceStore resourceStore,
SpringImageCaptchaProperties prop,
CaptchaInterceptor captchaInterceptor,
ApplicationContext applicationContext) {
TACBuilder tacBuilder = TACBuilder.builder(resourceStore)
.setGenerator(captchaGenerator)
.setValidator(imageCaptchaValidator)
.setCacheStore(cacheStore)
.setProp(prop)
.setInterceptor(captchaInterceptor);
if (prop.getInitDefaultResource()) {
tacBuilder.addDefaultTemplate(prop.getDefaultResourcePrefix());
}
if (!CollectionUtils.isEmpty(prop.getFontPath())) {
// 读取字体包
for (String fontPath : prop.getFontPath()) {
int index = fontPath.indexOf(":");
String[] split = index > 0 ? new String[]{fontPath.substring(0, index), fontPath.substring(index + 1)} : new String[]{"", fontPath};
String type = split[0];
String path = split[1];
tacBuilder.addFont(new cloud.tianai.captcha.resource.common.model.dto.Resource(type, path));
}
}
ImageCaptchaApplication target = tacBuilder.build();
if (prop.getSecondary() != null && Boolean.TRUE.equals(prop.getSecondary().getEnabled())) {
// 一个简单的二次验证
target = new SecondaryVerificationApplication(target, prop.getSecondary());
}
return target;
}
}
@@ -0,0 +1,12 @@
package cloud.tianai.captcha.spring4.autoconfiguration;
import lombok.Data;
@Data
public class SecondaryVerificationProperties {
private Boolean enabled = false;
private Long expire = 120000L;
private String keyPrefix = "captcha:secondary";
}
@@ -0,0 +1,32 @@
package cloud.tianai.captcha.spring4.autoconfiguration;
import cloud.tianai.captcha.application.ImageCaptchaProperties;
import cloud.tianai.captcha.resource.DefaultBuiltInResources;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* @Author: 天爱有情
* @date 2020/10/19 18:41
* @Description 滑块验证码属性
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "captcha")
public class SpringImageCaptchaProperties extends ImageCaptchaProperties {
/** 是否初始化默认资源. */
private Boolean initDefaultResource = false;
/** 默认资源的位置. */
private String defaultResourcePrefix = DefaultBuiltInResources.PATH_PREFIX;
/** 字体包路径. */
private List<String> fontPath;
/** 二次验证配置. */
@NestedConfigurationProperty
private SecondaryVerificationProperties secondary;
}
@@ -0,0 +1,41 @@
package cloud.tianai.captcha.spring4.exception;
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
import lombok.Getter;
import lombok.Setter;
/**
* @Author: 天爱有情
* @Date 2020/6/19 16:36
* @Description 验证码验证失败异常
*/
@Getter
@Setter
public class CaptchaValidException extends ImageCaptchaException {
private String captchaType;
private Integer code;
public CaptchaValidException() {
}
public CaptchaValidException(String captchaType,String message) {
super(message);
this.captchaType = captchaType;
}
public CaptchaValidException(String captchaType,Integer code, String message) {
super(message);
this.code = code;
this.captchaType = captchaType;
}
public CaptchaValidException(String message, Throwable cause) {
super(message, cause);
}
public CaptchaValidException(Throwable cause) {
super(cause);
}
public CaptchaValidException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -0,0 +1,36 @@
package cloud.tianai.captcha.spring4.plugins;
import cloud.tianai.captcha.generator.ImageCaptchaGeneratorProvider;
import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
/**
* @Author: 天爱有情
* @date 2022/5/19 14:37
* @Description 基于spring的 多验证码生成器
*/
public class SpringMultiImageCaptchaGenerator extends MultiImageCaptchaGenerator {
private ListableBeanFactory beanFactory;
public SpringMultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform,
BeanFactory beanFactory) {
super(imageCaptchaResourceManager, imageTransform);
this.beanFactory = (ListableBeanFactory) beanFactory;
}
@Override
protected void doInit() {
super.doInit();
String[] beanNamesForType = beanFactory.getBeanNamesForType(ImageCaptchaGeneratorProvider.class);
if (!ArrayUtils.isEmpty(beanNamesForType)) {
for (String beanName : beanNamesForType) {
ImageCaptchaGeneratorProvider provider = beanFactory.getBean(beanName, ImageCaptchaGeneratorProvider.class);
addImageCaptchaGeneratorProvider(provider);
}
}
}
}
@@ -0,0 +1,58 @@
package cloud.tianai.captcha.spring4.plugins.secondary;
import cloud.tianai.captcha.application.FilterImageCaptchaApplication;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.spring4.autoconfiguration.SecondaryVerificationProperties;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author: 天爱有情
* @date 2022/3/2 14:16
* @Description 二次验证
*/
public class SecondaryVerificationApplication extends FilterImageCaptchaApplication {
private SecondaryVerificationProperties prop;
public SecondaryVerificationApplication(ImageCaptchaApplication target, SecondaryVerificationProperties prop) {
super(target);
this.prop = prop;
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack imageCaptchaTrack) {
ApiResponse<?> match = super.matching(id, imageCaptchaTrack);
if (match.isSuccess()) {
// 如果匹配成功, 添加二次验证记录
addSecondaryVerification(id, imageCaptchaTrack);
}
return match;
}
/**
* 二次缓存验证
* @param id id
* @return boolean
*/
public boolean secondaryVerification(String id) {
Map<String, Object> cache = target.getCacheStore().getAndRemoveCache(getKey(id));
return cache != null;
}
/**
* 添加二次缓存验证记录
* @param id id
* @param imageCaptchaTrack sliderCaptchaTrack
*/
protected void addSecondaryVerification(String id, ImageCaptchaTrack imageCaptchaTrack) {
target.getCacheStore().setCache(getKey(id), new AnyMap(), prop.getExpire(), TimeUnit.MILLISECONDS);
}
protected String getKey(String id) {
return prop.getKeyPrefix().concat(":").concat(id);
}
}
@@ -0,0 +1,71 @@
package cloud.tianai.captcha.spring4.store.impl;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.AnyMap;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Author: 天爱有情
* @date 2022/3/2 14:42
* @Description redis实现的缓存
*/
public class RedisCacheStore implements CacheStore {
private static final RedisScript<String> SCRIPT_GET_CACHE = new DefaultRedisScript<>("local res = redis.call('get',KEYS[1]) if res == nil then return nil else redis.call('del',KEYS[1]) return res end", String.class);
protected StringRedisTemplate redisTemplate;
private Gson gson = new Gson();
public RedisCacheStore(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public AnyMap getCache(String key) {
String jsonData = redisTemplate.opsForValue().get(key);
if (StringUtils.isEmpty(jsonData)) {
return null;
}
return gson.fromJson(jsonData, new TypeToken<AnyMap>() {
}.getType());
}
@Override
public AnyMap getAndRemoveCache(String key) {
String json = redisTemplate.execute(SCRIPT_GET_CACHE, Collections.singletonList(key));
if (org.apache.commons.lang3.StringUtils.isBlank(json)) {
return null;
}
return gson.fromJson(json, new TypeToken<AnyMap>() {
}.getType());
}
@Override
public boolean setCache(String key, AnyMap data, Long expire, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, gson.toJson(data), expire, timeUnit);
return true;
}
@Override
public Long incr(String key, long delta, Long expire, TimeUnit timeUnit) {
Long increment = redisTemplate.opsForValue().increment(key, delta);
redisTemplate.expire(key, expire, timeUnit);
return increment;
}
@Override
public Long getLong(String key) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
return Long.valueOf(value);
}
}
@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cloud.tianai.captcha.spring4.autoconfiguration.CacheStoreAutoConfiguration,\
cloud.tianai.captcha.spring4.autoconfiguration.ImageCaptchaAutoConfiguration
@@ -0,0 +1,2 @@
cloud.tianai.captcha.spring4.autoconfiguration.CacheStoreAutoConfiguration
cloud.tianai.captcha.spring4.autoconfiguration.ImageCaptchaAutoConfiguration
@@ -2,6 +2,7 @@ package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.StoreCacheKeyPrefix;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
@@ -41,6 +42,8 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
private CacheStore cacheStore;
/** 验证码配置属性. */
private final ImageCaptchaProperties prop;
/** 缓存key 前缀处理器. */
private StoreCacheKeyPrefix storeCacheKeyPrefix;
/** 默认的过期时间. */
private long defaultExpire = 20000L;
@@ -50,9 +53,10 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
ImageCaptchaValidator imageCaptchaValidator,
CacheStore cacheStore,
ImageCaptchaProperties prop,
CaptchaInterceptor captchaInterceptor) {
CaptchaInterceptor captchaInterceptor,
StoreCacheKeyPrefix storeCacheKeyPrefix) {
this.prop = prop;
this.storeCacheKeyPrefix = null != storeCacheKeyPrefix? storeCacheKeyPrefix: StoreCacheKeyPrefix.prefixed(prop.getPrefix());
setImageCaptchaValidator(imageCaptchaValidator);
setCacheStore(cacheStore);
// 默认过期时间
@@ -235,7 +239,8 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
}
protected String getKey(String id) {
return prop.getPrefix().concat(":").concat(id);
// 改为通过接口扩展
return storeCacheKeyPrefix.compute(id);
}
@Override
@@ -1,6 +1,7 @@
package cloud.tianai.captcha.application;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.StoreCacheKeyPrefix;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.ImageTransform;
@@ -29,6 +30,7 @@ public class TACBuilder {
private ImageCaptchaProperties prop = new ImageCaptchaProperties();
private ResourceStore resourceStore;
private ImageTransform imageTransform;
private StoreCacheKeyPrefix cacheKeyPrefix;
// private List<FontWrapper> fontWrappers = new ArrayList<>();
public static TACBuilder builder() {
@@ -75,6 +77,10 @@ public class TACBuilder {
return this;
}
public TACBuilder setCacheKeyPrefix(StoreCacheKeyPrefix cacheKeyPrefix) {
this.cacheKeyPrefix = cacheKeyPrefix;
return this;
}
public TACBuilder addFont(Resource resource) {
this.addResource(FontCache.FONT_TYPE, resource);
return this;
@@ -148,7 +154,8 @@ public class TACBuilder {
if (interceptor == null) {
interceptor = EmptyCaptchaInterceptor.INSTANCE;
}
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor);
// 增加前缀处理接口
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor, cacheKeyPrefix);
return application;
}
}
@@ -0,0 +1,26 @@
package cloud.tianai.captcha.cache;
/**
* 验证码缓存Key前缀处理
*
* @author Alay
* @since 2025-11-11 13:44
*/
public interface StoreCacheKeyPrefix {
/**
* 缓存Key 计算处理
*
* @param captchaId 原始验证码Id
* @return 处理后的验证码缓存Key
*/
String compute(String captchaId);
static StoreCacheKeyPrefix prefixed(String prefix) {
if (prefix == null || prefix.isEmpty()) {
throw new IllegalArgumentException("prefix must not be null or empty");
}
return captchaId -> prefix.concat(":").concat(captchaId);
}
}