3 Commits

Author SHA1 Message Date
天爱有情 e7b9fd923b 1.5.5 2026-02-28 09:58:04 +08:00
天爱有情 a7fca6ee57 solon插件暂时无人维护,先考虑单独搬迁项目到另一个仓库 2026-02-10 08:47:28 +08:00
tianai cfab8ce3ac fix: TacBuilder中添加资源只能添加一条的bug 2026-02-09 19:28:56 +08:00
13 changed files with 24 additions and 567 deletions
+1 -2
View File
@@ -12,11 +12,10 @@
<modules> <modules>
<module>tianai-captcha</module> <module>tianai-captcha</module>
<module>tianai-captcha-springboot-starter</module> <module>tianai-captcha-springboot-starter</module>
<module>tianai-captcha-solon-plugin</module>
</modules> </modules>
<properties> <properties>
<revision>1.5.4</revision> <revision>1.5.5</revision>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<!-- 打包跳过单元测试 --> <!-- 打包跳过单元测试 -->
<skipTests>true</skipTests> <skipTests>true</skipTests>
+1 -1
View File
@@ -32,7 +32,7 @@
<dependency> <dependency>
<groupId>cloud.tianai.captcha</groupId> <groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId> <artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.5.4</version> <version>1.5.5</version>
</dependency> </dependency>
``` ```
-63
View File
@@ -1,63 +0,0 @@
<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/maven-v4_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-solon-plugin</artifactId>
<name>tianai-captcha-solon-plugin</name>
<description>行为验证码的solon插件</description>
<url>https://gitee.com/tianai/tianai-captcha-solon-plugin</url>
<properties>
<solon.version>3.5.2</solon.version>
<maven.compiler.source>8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon</artifactId>
<version>${solon.version}</version>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
<version>${solon.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,21 +0,0 @@
package cloud.tianai.captcha.solon;
import cloud.tianai.captcha.solon.config.ImageCaptchaAutoConfiguration;
import cloud.tianai.captcha.solon.properties.CaptchaProperties;
import org.noear.solon.core.AppContext;
import org.noear.solon.core.Plugin;
/**
* 插件启动类
* @Author XT
* @Date 2024.09.03
*/
public class XPluginImp implements Plugin {
@Override
public void start(AppContext context) {
context.beanMake(CaptchaProperties.class);
context.beanMake(ImageCaptchaAutoConfiguration.class);
}
}
@@ -1,95 +0,0 @@
package cloud.tianai.captcha.solon.config;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.TACBuilder;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.solon.plugins.secondary.SecondaryVerificationApplication;
import cloud.tianai.captcha.solon.properties.CaptchaProperties;
import cloud.tianai.captcha.solon.service.CaptchaRedisCacheService;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import java.util.List;
/**
* @Author XT
* @Date 2024.09.03
*/
@Configuration
public class ImageCaptchaAutoConfiguration {
@Bean
public ImageCaptchaApplication imageCaptchaApplication(CaptchaProperties captchaProperties, @Inject(required = false) CaptchaRedisCacheService cacheService) {
TACBuilder tacBuilder = TACBuilder.builder();
tacBuilder.addDefaultTemplate();
tacBuilder.expire("default", captchaProperties.getExpire());
tacBuilder.prefix(captchaProperties.getPrefix());
// 注入背景图资源
if (captchaProperties.getResources().getAuto()) {
String[] split = captchaProperties.getResources().getAutoType().split(",");
List<String> wordImageClickList = captchaProperties.getResources().getImages();
for (String type : split) {
for (String path : wordImageClickList) {
tacBuilder.addResource(type, new Resource("classpath", path));
}
}
} else {
List<String> wordImageClickList = captchaProperties.getResources().getWORD_IMAGE_CLICK();
if (!wordImageClickList.isEmpty()) {
for (String path : wordImageClickList) {
tacBuilder.addResource("WORD_IMAGE_CLICK", new Resource("classpath", path));
}
}
List<String> concatList = captchaProperties.getResources().getCONCAT();
if (!concatList.isEmpty()) {
for (String path : concatList) {
tacBuilder.addResource("CONCAT", new Resource("classpath", path));
}
}
List<String> sliderList = captchaProperties.getResources().getSLIDER();
if (!sliderList.isEmpty()) {
for (String path : sliderList) {
tacBuilder.addResource("SLIDER", new Resource("classpath", path));
}
}
List<String> rotateList = captchaProperties.getResources().getROTATE();
if (!rotateList.isEmpty()) {
for (String path : rotateList) {
tacBuilder.addResource("ROTATE", new Resource("classpath", path));
}
}
}
// 注入字体包
if (null != captchaProperties.getFontPath()) {
List<String> fontPathList = captchaProperties.getFontPath();
if (!fontPathList.isEmpty()) {
for (String path : fontPathList) {
try {
tacBuilder.addFont(new Resource("classpath", path));
} catch (Exception e) {
throw new RuntimeException("读取字体包失败,path=" + path, e);
}
}
}
}
CacheStore cacheStore = cacheService;
// 注入缓存器
if (null == cacheStore) {
cacheStore = new LocalCacheStore();
}
tacBuilder.setCacheStore(cacheStore);
ImageCaptchaApplication target = tacBuilder.build();
// 二次验证
if (captchaProperties.getSecondary().getEnabled()) {
target = new SecondaryVerificationApplication(target, captchaProperties, cacheStore);
}
return target;
}
}
@@ -1,109 +0,0 @@
package cloud.tianai.captcha.solon.plugins.secondary;
import cloud.tianai.captcha.application.FilterImageCaptchaApplication;
import cloud.tianai.captcha.application.ImageCaptchaApplication;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.solon.properties.CaptchaLimit;
import cloud.tianai.captcha.solon.properties.CaptchaProperties;
import cloud.tianai.captcha.solon.properties.CaptchaSecondary;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import org.noear.solon.core.handle.Context;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author XT
* @Date 2024.09.03
*/
public class SecondaryVerificationApplication extends FilterImageCaptchaApplication {
private final CaptchaSecondary prop;
private final CaptchaProperties captchaProperties;
private final CacheStore redisCacheService;
public SecondaryVerificationApplication(ImageCaptchaApplication target, CaptchaProperties captchaProperties, CacheStore redisCacheService) {
super(target);
this.captchaProperties = captchaProperties;
this.prop = captchaProperties.getSecondary();
this.redisCacheService = redisCacheService;
}
@Override
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
// 检查是否每分钟超过限制
CaptchaLimit limit = captchaProperties.getLimit();
if (null != limit && limit.getEnable()) {
Context current = Context.current();
String errLimitKey = getLimitKey(current, "error");
Long errLimit = redisCacheService.getLong(errLimitKey);
if (null != errLimit && errLimit >= limit.getErrorLimit()) {
throw new ImageCaptchaException("验证次数过多,请稍后再试");
}
String reqLimitKey = getLimitKey(current, "req");
Long reqLimit = redisCacheService.getLong(reqLimitKey);
if (null != reqLimit && reqLimit >= limit.getReqLimit()) {
throw new ImageCaptchaException("获取验证码频繁,请稍后再试");
}
redisCacheService.incr(reqLimitKey, 1, 60L, TimeUnit.SECONDS);
}
return super.generateCaptcha(type);
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack imageCaptchaTrack) {
ApiResponse<?> match = super.matching(id, imageCaptchaTrack);
if (match.isSuccess()) {
// 如果匹配成功, 添加二次验证记录
addSecondaryVerification(id + getRemoteId(Context.current()), imageCaptchaTrack);
} else {
CaptchaLimit limit = captchaProperties.getLimit();
if (null != limit && limit.getEnable()) {
Context current = Context.current();
String limitKey = getLimitKey(current, "error");
redisCacheService.incr(limitKey, 1, 60L, TimeUnit.SECONDS);
}
}
return match;
}
/**
* 二次缓存验证
* @param id id
* @return boolean
*/
public boolean secondaryVerification(String id) {
Map<String, Object> cache = target.getCacheStore().getAndRemoveCache(getKey(id + getRemoteId(Context.current())));
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);
}
protected String getLimitKey(Context ctx, String type) {
return prop.getKeyPrefix().concat(":limit:")
.concat(type)
.concat(":")
.concat(getRemoteId(ctx));
}
public static String getRemoteId(Context ctx) {
return ctx.realIp() + ctx.userAgent();
}
}
@@ -1,38 +0,0 @@
package cloud.tianai.captcha.solon.properties;
/**
* @Author XT
* @Date 2024.09.04
*/
public class CaptchaLimit {
private Boolean enable;
private Long reqLimit;
private Long errorLimit;
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
public Long getReqLimit() {
return reqLimit;
}
public void setReqLimit(Long reqLimit) {
this.reqLimit = reqLimit;
}
public Long getErrorLimit() {
return errorLimit;
}
public void setErrorLimit(Long errorLimit) {
this.errorLimit = errorLimit;
}
}
@@ -1,93 +0,0 @@
package cloud.tianai.captcha.solon.properties;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import java.util.List;
/**
* @Author XT
* @Date 2024.09.03
*/
@Inject("${captcha}")
@Configuration
public class CaptchaProperties {
/**
* redis 前缀
*/
private String prefix;
/**
* 有效期
*/
private Long expire;
/**
* 字体路径
*/
private List<String> fontPath;
/**
* 资源路径
*/
private CaptchaResource resources;
/**
* 二次验证
*/
private CaptchaSecondary secondary;
/**
* 每分限流
*/
private CaptchaLimit limit;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
public List<String> getFontPath() {
return fontPath;
}
public void setFontPath(List<String> fontPath) {
this.fontPath = fontPath;
}
public CaptchaResource getResources() {
return resources;
}
public void setResources(CaptchaResource resources) {
this.resources = resources;
}
public CaptchaSecondary getSecondary() {
return secondary;
}
public void setSecondary(CaptchaSecondary secondary) {
this.secondary = secondary;
}
public CaptchaLimit getLimit() {
return limit;
}
public void setLimit(CaptchaLimit limit) {
this.limit = limit;
}
}
@@ -1,80 +0,0 @@
package cloud.tianai.captcha.solon.properties;
import java.util.List;
/**
* @Author XT
* @Date 2024.09.03
*/
public class CaptchaResource {
private Boolean auto;
private String autoType;
private List<String> images;
private List<String> SLIDER;
private List<String> WORD_IMAGE_CLICK;
private List<String> ROTATE;
private List<String> CONCAT;
public List<String> getSLIDER() {
return SLIDER;
}
public void setSLIDER(List<String> SLIDER) {
this.SLIDER = SLIDER;
}
public List<String> getWORD_IMAGE_CLICK() {
return WORD_IMAGE_CLICK;
}
public void setWORD_IMAGE_CLICK(List<String> WORD_IMAGE_CLICK) {
this.WORD_IMAGE_CLICK = WORD_IMAGE_CLICK;
}
public List<String> getROTATE() {
return ROTATE;
}
public void setROTATE(List<String> ROTATE) {
this.ROTATE = ROTATE;
}
public List<String> getCONCAT() {
return CONCAT;
}
public void setCONCAT(List<String> CONCAT) {
this.CONCAT = CONCAT;
}
public Boolean getAuto() {
return auto;
}
public void setAuto(Boolean auto) {
this.auto = auto;
}
public String getAutoType() {
return autoType;
}
public void setAutoType(String autoType) {
this.autoType = autoType;
}
public List<String> getImages() {
return images;
}
public void setImages(List<String> images) {
this.images = images;
}
}
@@ -1,38 +0,0 @@
package cloud.tianai.captcha.solon.properties;
/**
* @Author XT
* @Date 2024.09.03
*/
public class CaptchaSecondary {
private Boolean enabled;
private Long expire;
private String keyPrefix;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
}
@@ -1,7 +0,0 @@
package cloud.tianai.captcha.solon.service;
import cloud.tianai.captcha.cache.CacheStore;
public interface CaptchaRedisCacheService extends CacheStore {
}
@@ -17,9 +17,7 @@ import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import cloud.tianai.captcha.validator.ImageCaptchaValidator; import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator; import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
import java.util.LinkedHashMap; import java.util.*;
import java.util.Map;
import java.util.Set;
/** /**
* @Author: 天爱有情 * @Author: 天爱有情
@@ -36,9 +34,10 @@ public class TACBuilder {
private ResourceStore resourceStore; private ResourceStore resourceStore;
private ImageTransform imageTransform; private ImageTransform imageTransform;
// private List<FontWrapper> fontWrappers = new ArrayList<>(); // private List<FontWrapper> fontWrappers = new ArrayList<>();
private Map<String, Resource> resourceCache; private Map<String, List<Resource>> resourceCache = new HashMap<>(8);
private Map<String, ResourceMap> templateCache; private Map<String, List<ResourceMap>> templateCache = new HashMap<>(8);
private String defaultTemplatePrefix = null;
public static TACBuilder builder() { public static TACBuilder builder() {
return new TACBuilder(); return new TACBuilder();
} }
@@ -53,8 +52,7 @@ public class TACBuilder {
} }
public TACBuilder addDefaultTemplate(String defaultPathPrefix) { public TACBuilder addDefaultTemplate(String defaultPathPrefix) {
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix); this.defaultTemplatePrefix = defaultPathPrefix;
defaultBuiltInResources.addDefaultTemplate(resourceStore);
return this; return this;
} }
@@ -155,14 +153,23 @@ public class TACBuilder {
if (resourceStore instanceof CrudResourceStore) { if (resourceStore instanceof CrudResourceStore) {
CrudResourceStore crudResourceStore = (CrudResourceStore) resourceStore; CrudResourceStore crudResourceStore = (CrudResourceStore) resourceStore;
if (!CollectionUtils.isEmpty(resourceCache)) { if (!CollectionUtils.isEmpty(resourceCache)) {
resourceCache.forEach(crudResourceStore::addResource); resourceCache.forEach((type,resources) -> {
resourceCache = null; resources.forEach(resource -> crudResourceStore.addResource(type,resource));
});
resourceCache.clear();
} }
if (!CollectionUtils.isEmpty(templateCache)) { if (!CollectionUtils.isEmpty(templateCache)) {
templateCache.forEach(crudResourceStore::addTemplate); templateCache.forEach((type, templates) -> {
templateCache = null; templates.forEach(template -> crudResourceStore.addTemplate(type, template));
});
templateCache.clear();
} }
} }
// 添加默认模板
if (defaultTemplatePrefix != null) {
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultTemplatePrefix);
defaultBuiltInResources.addDefaultTemplate(resourceStore);
}
if (generator == null) { if (generator == null) {
ResourceProviders resourceProviders = new ResourceProviders(); ResourceProviders resourceProviders = new ResourceProviders();
DefaultImageCaptchaResourceManager resourceManager = new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders); DefaultImageCaptchaResourceManager resourceManager = new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders);
@@ -183,16 +190,10 @@ public class TACBuilder {
} }
private void cacheResource(String captchaType, Resource imageResource) { private void cacheResource(String captchaType, Resource imageResource) {
if (resourceCache == null) { resourceCache.computeIfAbsent(captchaType, k -> new ArrayList<>(4)).add(imageResource);
resourceCache = new LinkedHashMap<>(8);
}
resourceCache.put(captchaType, imageResource);
} }
private void cacheTemplate(String captchaType, ResourceMap resourceMap) { private void cacheTemplate(String captchaType, ResourceMap resourceMap) {
if (templateCache == null) { templateCache.computeIfAbsent(captchaType, k -> new ArrayList<>(4)).add(resourceMap);
templateCache = new LinkedHashMap<>(8);
}
templateCache.put(captchaType, resourceMap);
} }
} }
@@ -58,7 +58,7 @@ public class TACBuilderTest {
ImageCaptchaApplication application = TACBuilder.builder() ImageCaptchaApplication application = TACBuilder.builder()
// 设置资源存储器,默认是 LocalMemoryResourceStore // 设置资源存储器,默认是 LocalMemoryResourceStore
.setResourceStore(new LocalMemoryResourceStore()) // .setResourceStore(new LocalMemoryResourceStore())
// 加载系统自带的默认资源(系统内置了几个滑块验证码缺口模板图,调用此函数加载) // 加载系统自带的默认资源(系统内置了几个滑块验证码缺口模板图,调用此函数加载)
.addDefaultTemplate() .addDefaultTemplate()
// 设置验证码过期时间, 单位毫秒, default 是默认验证码过期时间,当前设置为10秒, // 设置验证码过期时间, 单位毫秒, default 是默认验证码过期时间,当前设置为10秒,
@@ -71,6 +71,7 @@ public class TACBuilderTest {
// arg1 验证码类型(SLIDER、WORD_IMAGE_CLICK、ROTATE、CONCAT), // arg1 验证码类型(SLIDER、WORD_IMAGE_CLICK、ROTATE、CONCAT),
// arg2 验证码背景图片资源 // arg2 验证码背景图片资源
.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) .addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) .addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) .addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) .addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))