refactor(resource): 重构资源存储和管理逻辑

- 移除了 AbstractResourceStore 类
- 新增了 CrudResourceStore 接口,定义了 CRUD操作
- 修改了 DefaultImageCaptchaResourceManager,支持批量获取资源和模板
- 重构了 FontCache 类,改为实现 ResourceStore 接口
- 更新了相关应用类,使用新的资源管理逻辑
This commit is contained in:
天爱有情
2025-06-30 16:34:24 +08:00
parent 12d290919a
commit cb92a224d5
25 changed files with 386 additions and 586 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>cloud.tianai.captcha</groupId> <groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId> <artifactId>tianai-captcha</artifactId>
<version>1.5.2</version> <version>1.5.3</version>
<name>tianai-captcha</name> <name>tianai-captcha</name>
<description>行为验证码</description> <description>行为验证码</description>
+7 -2
View File
@@ -27,12 +27,17 @@
> 注意: 如果你项目是使用的**Springboot** > 注意: 如果你项目是使用的**Springboot**
> >
> >
请使用SpringBoot脚手架工具[tianai-captcha-springboot-starter](https://gitcode.com/tiana/tianai-captcha-springboot-starter); 请使用SpringBoot脚手架工具
- [tianai-captcha-springboot-starter(gitee)](https://gitee.com/tianai/tianai-captcha-springboot-starter);
- [tianai-captcha-springboot-starter(gitcode)](https://gitcode.com/tiana/tianai-captcha-springboot-starter);
- [tianai-captcha-springboot-starter(github)](https://github.com/tianaiyouqing/tianai-captcha-springboot-starter);
> >
> 该工具对tianai-captcha验证码进行了封装,使其使用更加方便快捷 > 该工具对tianai-captcha验证码进行了封装,使其使用更加方便快捷
> **写好的验证码demo移步 [tianai-captcha-demo](https://gitcode.com/tiana/tianai-captcha-demo)** > **写好的验证码demo移步
> - [tianai-captcha-demo(gitee)](https://gitee.com/tianai/tianai-captcha-demo)
> - [tianai-captcha-demo(gitcode)](https://gitcode.com/tiana/tianai-captcha-demo)
### 1. 导入xml ### 1. 导入xml
@@ -1,6 +1,5 @@
package cloud.tianai.captcha.application; package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO; import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore; import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.AnyMap; import cloud.tianai.captcha.common.AnyMap;
@@ -78,21 +77,21 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha() { public ApiResponse<ImageCaptchaVO> generateCaptcha() {
// 生成滑块验证码 // 生成滑块验证码
return generateCaptcha(CaptchaTypeConstant.SLIDER); return generateCaptcha(CaptchaTypeConstant.SLIDER);
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type) { public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
GenerateParam generateParam = new GenerateParam(); GenerateParam generateParam = new GenerateParam();
generateParam.setType(type); generateParam.setType(type);
return generateCaptcha(generateParam); return generateCaptcha(generateParam);
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) { public ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
CaptchaResponse<ImageCaptchaVO> captchaResponse = beforeGenerateCaptcha(param); ApiResponse<ImageCaptchaVO> captchaResponse = beforeGenerateCaptcha(param);
if (captchaResponse != null) { if (captchaResponse != null) {
return captchaResponse; return captchaResponse;
} }
@@ -103,12 +102,12 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) { public ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
return generateCaptcha(CaptchaTypeConstant.SLIDER, captchaImageType); return generateCaptcha(CaptchaTypeConstant.SLIDER, captchaImageType);
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) { public ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
GenerateParam param = new GenerateParam(); GenerateParam param = new GenerateParam();
if (CaptchaImageType.WEBP.equals(captchaImageType)) { if (CaptchaImageType.WEBP.equals(captchaImageType)) {
param.setBackgroundFormatName("webp"); param.setBackgroundFormatName("webp");
@@ -122,14 +121,14 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
} }
public CaptchaResponse<ImageCaptchaVO> convertToCaptchaResponse(ImageCaptchaInfo imageCaptchaInfo) { public ApiResponse<ImageCaptchaVO> convertToCaptchaResponse(ImageCaptchaInfo imageCaptchaInfo) {
if (imageCaptchaInfo == null) { if (imageCaptchaInfo == null) {
// 要是生成失败 // 要是生成失败
throw new ImageCaptchaException("生成验证码失败,验证码生成为空"); throw new ImageCaptchaException("生成验证码失败,验证码生成为空");
} }
// 生成ID // 生成ID
String id = generatorId(imageCaptchaInfo); String id = generatorId(imageCaptchaInfo);
CaptchaResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData(imageCaptchaInfo); ApiResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData(imageCaptchaInfo);
if (response != null) { if (response != null) {
return response; return response;
} }
@@ -151,7 +150,8 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
verificationVO.setTemplateImageWidth(imageCaptchaInfo.getTemplateImageWidth()); verificationVO.setTemplateImageWidth(imageCaptchaInfo.getTemplateImageWidth());
verificationVO.setTemplateImageHeight(imageCaptchaInfo.getTemplateImageHeight()); verificationVO.setTemplateImageHeight(imageCaptchaInfo.getTemplateImageHeight());
verificationVO.setData(imageCaptchaInfo.getData() == null ? null : imageCaptchaInfo.getData().getViewData()); verificationVO.setData(imageCaptchaInfo.getData() == null ? null : imageCaptchaInfo.getData().getViewData());
return CaptchaResponse.of(id, verificationVO); verificationVO.setId(id);
return ApiResponse.ofSuccess(verificationVO);
} }
@@ -286,15 +286,15 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
// ============== 一些模板方法 ================ // ============== 一些模板方法 ================
private void afterGenerateCaptcha(ImageCaptchaInfo imageCaptchaInfo, CaptchaResponse<ImageCaptchaVO> captchaResponse) { private void afterGenerateCaptcha(ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
captchaInterceptor.afterGenerateCaptcha(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, captchaResponse); captchaInterceptor.afterGenerateCaptcha(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, captchaResponse);
} }
private CaptchaResponse<ImageCaptchaVO> beforeGenerateCaptcha(GenerateParam param) { private ApiResponse<ImageCaptchaVO> beforeGenerateCaptcha(GenerateParam param) {
return captchaInterceptor.beforeGenerateCaptcha(captchaInterceptor.createContext(), param.getType(), param); return captchaInterceptor.beforeGenerateCaptcha(captchaInterceptor.createContext(), param.getType(), param);
} }
private CaptchaResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) { private ApiResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) {
return captchaInterceptor.beforeGenerateImageCaptchaValidData(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo); return captchaInterceptor.beforeGenerateImageCaptchaValidData(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo);
} }
@@ -1,6 +1,5 @@
package cloud.tianai.captcha.application; package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO; import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore; import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.response.ApiResponse; import cloud.tianai.captcha.common.response.ApiResponse;
@@ -27,27 +26,27 @@ public class FilterImageCaptchaApplication implements ImageCaptchaApplication {
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha() { public ApiResponse<ImageCaptchaVO> generateCaptcha() {
return target.generateCaptcha(); return target.generateCaptcha();
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type) { public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
return target.generateCaptcha(type); return target.generateCaptcha(type);
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) { public ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
return target.generateCaptcha(captchaImageType); return target.generateCaptcha(captchaImageType);
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) { public ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
return target.generateCaptcha(type, captchaImageType); return target.generateCaptcha(type, captchaImageType);
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) { public ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
return target.generateCaptcha(param); return target.generateCaptcha(param);
} }
@@ -1,7 +1,6 @@
package cloud.tianai.captcha.application; package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO; import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore; import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.common.response.ApiResponse; import cloud.tianai.captcha.common.response.ApiResponse;
@@ -25,7 +24,7 @@ public interface ImageCaptchaApplication {
* *
* @return * @return
*/ */
CaptchaResponse<ImageCaptchaVO> generateCaptcha(); ApiResponse<ImageCaptchaVO> generateCaptcha();
/** /**
* 生成滑块验证码 * 生成滑块验证码
@@ -33,7 +32,7 @@ public interface ImageCaptchaApplication {
* @param type type类型 * @param type type类型
* @return CaptchaResponse<SliderCaptchaVO> * @return CaptchaResponse<SliderCaptchaVO>
*/ */
CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type); ApiResponse<ImageCaptchaVO> generateCaptcha(String type);
/** /**
* 生成滑块验证码 * 生成滑块验证码
@@ -41,7 +40,7 @@ public interface ImageCaptchaApplication {
* @param captchaImageType 要生成webp还是jpg类型的图片 * @param captchaImageType 要生成webp还是jpg类型的图片
* @return CaptchaResponse<SliderCaptchaVO> * @return CaptchaResponse<SliderCaptchaVO>
*/ */
CaptchaResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType); ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType);
/** /**
* 生成验证码 * 生成验证码
@@ -50,7 +49,7 @@ public interface ImageCaptchaApplication {
* @param captchaImageType CaptchaImageType * @param captchaImageType CaptchaImageType
* @return CaptchaResponse<ImageCaptchaVO> * @return CaptchaResponse<ImageCaptchaVO>
*/ */
CaptchaResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType); ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType);
/** /**
@@ -59,7 +58,7 @@ public interface ImageCaptchaApplication {
* @param param param * @param param param
* @return CaptchaResponse<SliderCaptchaVO> * @return CaptchaResponse<SliderCaptchaVO>
*/ */
CaptchaResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param); ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param);
/** /**
* 匹配 * 匹配
@@ -7,10 +7,7 @@ import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor; import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor; import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
import cloud.tianai.captcha.resource.DefaultBuiltInResources; import cloud.tianai.captcha.resource.*;
import cloud.tianai.captcha.resource.FontCache;
import cloud.tianai.captcha.resource.ResourceProviders;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource; import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap; import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
@@ -49,8 +46,8 @@ public class TACBuilder {
} }
public TACBuilder addDefaultTemplate(String defaultPathPrefix) { public TACBuilder addDefaultTemplate(String defaultPathPrefix) {
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix); // DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix);
defaultBuiltInResources.addDefaultTemplate(resourceStore); // defaultBuiltInResources.addDefaultTemplate(resourceStore);
return this; return this;
} }
@@ -115,12 +112,16 @@ public class TACBuilder {
public TACBuilder addResource(String captchaType, Resource imageResource) { public TACBuilder addResource(String captchaType, Resource imageResource) {
this.resourceStore.addResource(captchaType, imageResource); if (resourceStore instanceof CrudResourceStore) {
((CrudResourceStore) resourceStore).addResource(captchaType, imageResource);
}
return this; return this;
} }
public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) { public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) {
this.resourceStore.addTemplate(captchaType, resourceMap); if (resourceStore instanceof CrudResourceStore) {
((CrudResourceStore) resourceStore).addTemplate(captchaType, resourceMap);
}
return this; return this;
} }
@@ -10,6 +10,9 @@ import java.io.Serializable;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class ImageCaptchaVO implements Serializable { public class ImageCaptchaVO implements Serializable {
/** ID.*/
private String id;
/** 验证码类型.*/ /** 验证码类型.*/
private String type; private String type;
/** 背景图.*/ /** 背景图.*/
@@ -3,13 +3,18 @@ package cloud.tianai.captcha.common.constant;
public interface CommonConstant { public interface CommonConstant {
String DEFAULT_TAG = "default"; String DEFAULT_TAG = "default";
/** 图标点选资源存储类型. */ /** 图标点选资源存储类型. */
String IMAGE_CLICK_ICON = "ICON"; String IMAGE_ICON = "ICON";
/** 蜂窝点选.*/ /** 蜂窝点选.*/
String HONEYCOMB_CLICK_ICON = "HONEYCOMB_ICON"; String HONEYCOMB_CLICK_ICON = "HONEYCOMB_ICON";
/** 刮刮卡图标. */ /** 刮刮卡图标. */
String SCRAPE_ICON = "SCRAPE_ICON"; String SCRAPE_ICON = "SCRAPE_ICON";
// String IMAGE_CLICK_ICON = "IMAGE_CLICK_ICON";
String IMAGE_TIP_ICON = "IMAGE_TIP_ICON";
String IMAGE_CLICK_ICON = "IMAGE_CLICK_ICON";
/** /**
* 默认的resource资源文件路径. * 默认的resource资源文件路径.
@@ -7,7 +7,7 @@ import java.io.Serializable;
/** /**
* @Author: 天爱有情 * @Author: 天爱有情
* @date 2023/4/20 9:53 * @date 2023/4/20 9:53
* @Description 可能是最好用的API统一返回格式类 * @Description API统一返回格式类
*/ */
@Data @Data
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
@@ -2,7 +2,8 @@ package cloud.tianai.captcha.generator.common.model.dto;
import cloud.tianai.captcha.common.AnyMap; import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import lombok.*; import lombok.Data;
import lombok.EqualsAndHashCode;
/** /**
* @Author: 天爱有情 * @Author: 天爱有情
@@ -28,6 +28,10 @@ public class MultiImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
protected Map<String, ImageCaptchaGenerator> imageCaptchaGeneratorMap = new ConcurrentHashMap<>(4); protected Map<String, ImageCaptchaGenerator> imageCaptchaGeneratorMap = new ConcurrentHashMap<>(4);
protected Map<String, ImageCaptchaGeneratorProvider> imageCaptchaGeneratorProviderMap = new HashMap<>(4); protected Map<String, ImageCaptchaGeneratorProvider> imageCaptchaGeneratorProviderMap = new HashMap<>(4);
// 点选类验证码字体
// @Setter
// @Getter
// protected List<FontWrapper> fontWrappers;
@Setter @Setter
@Getter @Getter
private String defaultCaptcha = SLIDER; private String defaultCaptcha = SLIDER;
@@ -88,14 +88,11 @@ public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGen
BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(activeTemplate.getWidth(), background.getHeight()); BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(activeTemplate.getWidth(), background.getHeight());
CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY); CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY);
XandY xandY = new XandY();
xandY.x = randomX;
xandY.y = randomY;
captchaExchange.setBackgroundImage(background); captchaExchange.setBackgroundImage(background);
captchaExchange.setTemplateImage(matrixTemplate); captchaExchange.setTemplateImage(matrixTemplate);
captchaExchange.setTemplateResource(templateResource); captchaExchange.setTemplateResource(templateResource);
captchaExchange.setResourceImage(resourceImage); captchaExchange.setResourceImage(resourceImage);
captchaExchange.setTransferData(xandY); captchaExchange.setTransferData(new Point(randomX,randomY));
// 后处理 // 后处理
// applyPostProcessorBeforeWrapImageCaptchaInfo(captchaExchange, this); // applyPostProcessorBeforeWrapImageCaptchaInfo(captchaExchange, this);
// imageCaptchaInfo = wrapSliderCaptchaInfo(randomX, randomY, captchaExchange); // imageCaptchaInfo = wrapSliderCaptchaInfo(randomX, randomY, captchaExchange);
@@ -125,11 +122,6 @@ public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGen
} }
public static class XandY {
int x;
int y;
}
@SneakyThrows @SneakyThrows
@Override @Override
public SliderImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) { public SliderImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
@@ -139,7 +131,7 @@ public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGen
Resource resourceImage = captchaExchange.getResourceImage(); Resource resourceImage = captchaExchange.getResourceImage();
ResourceMap templateResource = captchaExchange.getTemplateResource(); ResourceMap templateResource = captchaExchange.getTemplateResource();
CustomData customData = captchaExchange.getCustomData(); CustomData customData = captchaExchange.getCustomData();
XandY data = (XandY) captchaExchange.getTransferData(); Point data = (Point) captchaExchange.getTransferData();
ImageTransformData transform = getImageTransform().transform(param, backgroundImage, sliderImage, resourceImage, templateResource, customData); ImageTransformData transform = getImageTransform().transform(param, backgroundImage, sliderImage, resourceImage, templateResource, customData);
SliderImageCaptchaInfo imageCaptchaInfo = SliderImageCaptchaInfo.of(data.x, data.y, SliderImageCaptchaInfo imageCaptchaInfo = SliderImageCaptchaInfo.of(data.x, data.y,
@@ -1,6 +1,5 @@
package cloud.tianai.captcha.interceptor; package cloud.tianai.captcha.interceptor;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO; import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.common.AnyMap; import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.response.ApiResponse; import cloud.tianai.captcha.common.response.ApiResponse;
@@ -41,18 +40,18 @@ public interface CaptchaInterceptor {
return new Context(getName(), null, -1, 1, EmptyCaptchaInterceptor.INSTANCE); return new Context(getName(), null, -1, 1, EmptyCaptchaInterceptor.INSTANCE);
} }
default CaptchaResponse<ImageCaptchaVO> beforeGenerateCaptcha(Context context, String type, GenerateParam param) { default ApiResponse<ImageCaptchaVO> beforeGenerateCaptcha(Context context, String type, GenerateParam param) {
return null; return null;
} }
default CaptchaResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) { default ApiResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) {
return null; return null;
} }
default void afterGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, AnyMap validData) { default void afterGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, AnyMap validData) {
} }
default void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, CaptchaResponse<ImageCaptchaVO> captchaResponse) { default void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
} }
default ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) { default ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
@@ -1,6 +1,5 @@
package cloud.tianai.captcha.interceptor; package cloud.tianai.captcha.interceptor;
import cloud.tianai.captcha.application.vo.CaptchaResponse;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO; import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.common.AnyMap; import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.response.ApiResponse; import cloud.tianai.captcha.common.response.ApiResponse;
@@ -66,9 +65,9 @@ public class CaptchaInterceptorGroup implements CaptchaInterceptor {
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> beforeGenerateCaptcha(Context context, String type, GenerateParam param) { public ApiResponse<ImageCaptchaVO> beforeGenerateCaptcha(Context context, String type, GenerateParam param) {
context = createContextIfNecessary(context); context = createContextIfNecessary(context);
CaptchaResponse<ImageCaptchaVO> captchaResponse = null; ApiResponse<ImageCaptchaVO> captchaResponse = null;
while (context.next() < context.getCount()) { while (context.next() < context.getCount()) {
CaptchaInterceptor interceptor = validators.get(context.getCurrent()); CaptchaInterceptor interceptor = validators.get(context.getCurrent());
captchaResponse = interceptor.beforeGenerateCaptcha(context, type, param); captchaResponse = interceptor.beforeGenerateCaptcha(context, type, param);
@@ -78,7 +77,7 @@ public class CaptchaInterceptorGroup implements CaptchaInterceptor {
} }
@Override @Override
public void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, CaptchaResponse<ImageCaptchaVO> captchaResponse) { public void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
context = createContextIfNecessary(context); context = createContextIfNecessary(context);
while (context.next() < context.getCount()) { while (context.next() < context.getCount()) {
CaptchaInterceptor interceptor = validators.get(context.getCurrent()); CaptchaInterceptor interceptor = validators.get(context.getCurrent());
@@ -111,9 +110,9 @@ public class CaptchaInterceptorGroup implements CaptchaInterceptor {
} }
@Override @Override
public CaptchaResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) { public ApiResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) {
context = createContextIfNecessary(context); context = createContextIfNecessary(context);
CaptchaResponse<ImageCaptchaVO> captchaResponse = null; ApiResponse<ImageCaptchaVO> captchaResponse = null;
while (context.next() < context.getCount()) { while (context.next() < context.getCount()) {
CaptchaInterceptor interceptor = validators.get(context.getCurrent()); CaptchaInterceptor interceptor = validators.get(context.getCurrent());
captchaResponse = interceptor.beforeGenerateImageCaptchaValidData(context, type, imageCaptchaInfo); captchaResponse = interceptor.beforeGenerateImageCaptchaValidData(context, type, imageCaptchaInfo);
@@ -17,7 +17,7 @@ import java.util.List;
* @Description BasicCaptchaTrackValidator * @Description BasicCaptchaTrackValidator
*/ */
public class BasicTrackCaptchaInterceptor implements CaptchaInterceptor { public class BasicTrackCaptchaInterceptor implements CaptchaInterceptor {
public static final CodeDefinition DEFINITION = new CodeDefinition(50001, "basic check fail"); public static final CodeDefinition DEFINITION = new CodeDefinition(50001, "basic track check fail");
@Override @Override
public String getName() { public String getName() {
@@ -1,203 +0,0 @@
package cloud.tianai.captcha.resource;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractResourceStore implements ResourceStore {
@Getter
protected ChainListener listener = new ChainListener();
boolean isInit = false;
@Override
public void init(ImageCaptchaResourceManager resourceManager) {
if (isInit) {
return;
}
doInit();
isInit = true;
listener.onInit(this, resourceManager);
}
@Override
public void addListener(ResourceListener listener) {
this.listener.removeListener(listener);
this.listener.addListener(listener);
}
@Override
public void addResource(String type, Resource resource) {
doAddResource(type, resource);
if (isInit) {
listener.onAddResource(type, resource);
}
}
@Override
public void addTemplate(String type, ResourceMap template) {
doAddTemplate(type, template);
if (isInit) {
listener.onAddTemplate(type, template);
}
}
@Override
public Resource deleteResource(String type, String id) {
Resource resource = doDeleteResource(type, id);
if (isInit && resource != null) {
listener.onDeleteResource(type, resource);
}
return resource;
}
@Override
public ResourceMap deleteTemplate(String type, String id) {
ResourceMap resourceMap = doDeleteTemplate(type, id);
if (isInit && resourceMap != null) {
listener.onDeleteTemplate(type, resourceMap);
}
return resourceMap;
}
@Override
public Resource randomGetResourceByTypeAndTag(String type, String tag) {
Resource resource = doRandomGetResourceByTypeAndTag(type, tag);
if (isInit && resource != null) {
listener.onRandomGetResourceByTypeAndTag(type, tag, resource);
}
return resource;
}
@Override
public ResourceMap randomGetTemplateByTypeAndTag(String type, String tag) {
ResourceMap resourceMap = doRandomGetTemplateByTypeAndTag(type, tag);
if (isInit && resourceMap != null) {
listener.onRandomGetTemplateByTypeAndTag(type, tag, resourceMap);
}
return resourceMap;
}
@Override
public void clearAllResources() {
doClearAllResources();
if (isInit) {
listener.onClearAllResources();
}
}
@Override
public void clearAllTemplates() {
doClearAllTemplates();
if (isInit) {
listener.onClearAllTemplates();
}
}
public void doInit() {
}
public abstract void doClearAllResources();
public abstract void doClearAllTemplates();
public abstract Resource doRandomGetResourceByTypeAndTag(String type, String tag);
public abstract ResourceMap doRandomGetTemplateByTypeAndTag(String type, String tag);
public abstract ResourceMap doDeleteTemplate(String type, String id);
public abstract Resource doDeleteResource(String type, String id);
public abstract void doAddResource(String type, Resource resource);
public abstract void doAddTemplate(String type, ResourceMap template);
public static class ChainListener implements ResourceListener {
protected List<ResourceListener> listeners = new ArrayList<>();
public ChainListener() {
}
public void addListener(ResourceListener listener) {
listeners.add(listener);
}
public void removeListener(ResourceListener listener) {
listeners.remove(listener);
}
@Override
public void onInit(ResourceStore resourceStore, ImageCaptchaResourceManager resourceManager) {
for (ResourceListener listener : listeners) {
listener.onInit(resourceStore, resourceManager);
}
}
@Override
public void onAddResource(String type, Resource resource) {
for (ResourceListener listener : listeners) {
listener.onAddResource(type, resource);
}
}
@Override
public void onAddTemplate(String type, ResourceMap template) {
for (ResourceListener listener : listeners) {
listener.onAddTemplate(type, template);
}
}
@Override
public void onClearAllResources() {
for (ResourceListener listener : listeners) {
listener.onClearAllResources();
}
}
@Override
public void onClearAllTemplates() {
for (ResourceListener listener : listeners) {
listener.onClearAllTemplates();
}
}
@Override
public void onRandomGetResourceByTypeAndTag(String type, String tag, Resource resource) {
for (ResourceListener listener : listeners) {
listener.onRandomGetResourceByTypeAndTag(type, tag, resource);
}
}
@Override
public void onRandomGetTemplateByTypeAndTag(String type, String tag, ResourceMap template) {
for (ResourceListener listener : listeners) {
listener.onRandomGetTemplateByTypeAndTag(type, tag, template);
}
}
@Override
public void onDeleteResource(String type, Resource resource) {
for (ResourceListener listener : listeners) {
listener.onDeleteResource(type, resource);
}
}
@Override
public void onDeleteTemplate(String type, ResourceMap template) {
for (ResourceListener listener : listeners) {
listener.onDeleteTemplate(type, template);
}
}
}
}
@@ -0,0 +1,77 @@
package cloud.tianai.captcha.resource;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import java.util.List;
/**
* @Author: 天爱有情
* @date 2025/6/13 16:43
* @Description 具有CRUD属性的资源存储器
*/
public interface CrudResourceStore extends ResourceStore {
/**
* 添加资源
*
* @param type 验证码类型
* @param resource 资源
*/
void addResource(String type, Resource resource);
/**
* 添加模板
*
* @param type 验证码类型
* @param template 模板
*/
void addTemplate(String type, ResourceMap template);
/**
* 删除资源
*
* @param type 验证码类型
* @param id 资源ID
* @return Resource
*/
Resource deleteResource(String type, String id);
/**
* 删除模板
*
* @param type 验证码类型
* @param id 资源ID
* @return ResourceMap
*/
ResourceMap deleteTemplate(String type, String id);
/**
* 获取某个资源列表
*
* @param type 验证码类型
* @param tag 资源标签(可为空)
* @return List<Resource>
*/
List<Resource> listResourcesByTypeAndTag(String type, String tag);
/**
* 获取某个模板列表
*
* @param type 验证码类型
* @param tag 资源标签(可为空)
* @return List<ResourceMap>
*/
List<ResourceMap> listTemplatesByTypeAndTag(String type, String tag);
/**
* 清除所有内置模板
*/
void clearAllTemplates();
/**
* 清除所有内置资源
*/
void clearAllResources();
}
@@ -23,7 +23,7 @@ public class DefaultBuiltInResources {
public static final String PATH_PREFIX = "classpath:META-INF/cut-image/template"; public static final String PATH_PREFIX = "classpath:META-INF/cut-image/template";
private static Map<String, Consumer<ResourceStore>> defaultTemplateResource = new HashMap<>(8); private static Map<String, Consumer<CrudResourceStore>> defaultTemplateResource = new HashMap<>(8);
public DefaultBuiltInResources(String defaultPathPrefix) { public DefaultBuiltInResources(String defaultPathPrefix) {
@@ -75,17 +75,21 @@ public class DefaultBuiltInResources {
public void addDefaultTemplate(String type, ResourceStore resourceStore) { public void addDefaultTemplate(String type, ResourceStore resourceStore) {
Consumer<ResourceStore> resourceStoreConsumer = defaultTemplateResource.get(type); if (resourceStore instanceof CrudResourceStore) {
Consumer<CrudResourceStore> resourceStoreConsumer = defaultTemplateResource.get(type);
if (resourceStoreConsumer == null) { if (resourceStoreConsumer == null) {
return; return;
} }
resourceStoreConsumer.accept(resourceStore); resourceStoreConsumer.accept((CrudResourceStore) resourceStore);
}
} }
public void addDefaultTemplate(ResourceStore resourceStore) { public void addDefaultTemplate(ResourceStore resourceStore) {
if (resourceStore instanceof CrudResourceStore) {
defaultTemplateResource.forEach((type, consumer) -> { defaultTemplateResource.forEach((type, consumer) -> {
consumer.accept(resourceStore); consumer.accept((CrudResourceStore) resourceStore);
}); });
} }
}
} }
@@ -2,7 +2,7 @@ package cloud.tianai.captcha.resource;
import cloud.tianai.captcha.generator.common.FontWrapper; import cloud.tianai.captcha.generator.common.FontWrapper;
import cloud.tianai.captcha.resource.common.model.dto.Resource; import cloud.tianai.captcha.resource.common.model.dto.Resource;
import lombok.Data; import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -20,7 +20,7 @@ import java.util.concurrent.ConcurrentHashMap;
* @Description 一个用于统一缓存字体文件的对象 * @Description 一个用于统一缓存字体文件的对象
*/ */
@Slf4j @Slf4j
public class FontCache implements ResourceListener { public class FontCache implements ResourceStore {
public static final String FONT_TYPE = "font"; public static final String FONT_TYPE = "font";
@@ -31,19 +31,20 @@ public class FontCache implements ResourceListener {
@Setter @Setter
@Getter @Getter
private int fontSize = 70; private int fontSize = 70;
public static FontCache getInstance() {
return INSTANCE.INSTANCE;
}
public FontCache() {
public FontCache(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
} }
@Override @Override
public void onInit(ResourceStore resourceStore, ImageCaptchaResourceManager resourceManager) { public void init(ImageCaptchaResourceManager resourceManager) {
this.resourceStore = resourceStore; resourceStore.init(resourceManager);
this.resourceManager = resourceManager; this.resourceManager = resourceManager;
} }
public FontWrapper getFont(Resource resource) { public FontWrapper getFont(Resource resource) {
try (InputStream stream = resourceManager.getResourceInputStream(resource)) { try (InputStream stream = resourceManager.getResourceInputStream(resource)) {
Font font = Font.createFont(0, stream); Font font = Font.createFont(0, stream);
@@ -53,43 +54,27 @@ public class FontCache implements ResourceListener {
} }
} }
private String calcId(Resource resource) {
// 缓存id, 避免重复加载。 多个验证码可能使用同一个字体, 这里不使用资源ID作为缓存ID, 而是使用type+data作为缓存ID。
return resource.getType() + "_" + resource.getData();
}
@Override @Override
public void onAddResource(String type, Resource resource) { public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
List<Resource> resources = resourceStore.randomGetResourceByTypeAndTag(type, tag, quantity);
// 字体增强
if (FONT_TYPE.equalsIgnoreCase(type)) { if (FONT_TYPE.equalsIgnoreCase(type)) {
fontMap.computeIfAbsent(resource.getId(), v -> getFont(resource)); for (Resource resource : resources) {
} FontWrapper fontWrapper = fontMap.computeIfAbsent(calcId(resource), v -> getFont(resource));
}
@Override
public void onDeleteResource(String type, Resource resource) {
if (FONT_TYPE.equalsIgnoreCase(type)) {
fontMap.remove(resource.getId());
}
}
@Override
public void onClearAllResources() {
fontMap.clear();
}
@Override
public void onRandomGetResourceByTypeAndTag(String type, String tag, Resource resource) {
if (FONT_TYPE.equalsIgnoreCase(type)) {
FontWrapper fontWrapper = fontMap.computeIfAbsent(resource.getId(), v -> getFont(resource));
resource.setExtra(fontWrapper); resource.setExtra(fontWrapper);
} }
} }
return resources;
public void loadAllFonts() {
List<Resource> resources = resourceStore.listResourcesByTypeAndTag(FONT_TYPE, null);
for (Resource resource : resources) {
fontMap.computeIfAbsent(resource.getId(), v -> getFont(resource));
}
} }
@Override
private static class INSTANCE { public List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag, Integer quantity) {
private static final FontCache INSTANCE = new FontCache(); return resourceStore.randomGetTemplateByTypeAndTag(type, tag, quantity);
} }
} }
@@ -31,6 +31,27 @@ public interface ImageCaptchaResourceManager {
*/ */
Resource randomGetResource(String type, String tag); Resource randomGetResource(String type, String tag);
/**
* 随机获取某个模板
*
* @param type 验证码类型
* @param tag 二级过滤,可以为空
* @param quantity 一次性获取的数量
* @return Map<String, Resource>
*/
List<ResourceMap> randomGetTemplate(String type, String tag, Integer quantity);
/**
* 随机获取某个资源对象
*
* @param type 验证码类型
* @param tag 二级过滤,可以为空
* @param quantity 一次性获取的数量
* @return Resource
*/
List<Resource> randomGetResource(String type, String tag, Integer quantity);
/** /**
* 获取真正的资源流通过资源对象 * 获取真正的资源流通过资源对象
* *
@@ -1,50 +0,0 @@
package cloud.tianai.captcha.resource;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
/**
* @Author: 天爱有情
* @date 2024/11/19 9:26
* @Description 此类负责对 ResourceStore 进行一下扩展增强, 对ResourceStore的相关方法添加一个hook回调
*/
public interface ResourceListener {
default void onInit(ResourceStore resourceStore, ImageCaptchaResourceManager resourceManager) {
}
default void onAddResource(String type, Resource resource) {
}
default void onAddTemplate(String type, ResourceMap template) {
}
default void onDeleteResource(String type, Resource resource) {
}
default void onDeleteTemplate(String type, ResourceMap template) {
}
default void onClearAllResources() {
}
default void onClearAllTemplates() {
}
default void onRandomGetResourceByTypeAndTag(String type, String tag, Resource resource) {
}
default void onRandomGetTemplateByTypeAndTag(String type, String tag, ResourceMap template) {
}
}
@@ -13,65 +13,6 @@ import java.util.List;
public interface ResourceStore { public interface ResourceStore {
void init(ImageCaptchaResourceManager resourceManager); void init(ImageCaptchaResourceManager resourceManager);
/**
* 给ResourceStore添加hook,用于一些扩展
*
* @param hook
*/
void addListener(ResourceListener hook);
/**
* 添加资源
*
* @param type 验证码类型
* @param resource 资源
*/
void addResource(String type, Resource resource);
/**
* 添加模板
*
* @param type 验证码类型
* @param template 模板
*/
void addTemplate(String type, ResourceMap template);
/**
* 删除资源
*
* @param type 验证码类型
* @param id 资源ID
* @return Resource
*/
Resource deleteResource(String type, String id);
/**
* 删除模板
*
* @param type 验证码类型
* @param id 资源ID
* @return ResourceMap
*/
ResourceMap deleteTemplate(String type, String id);
/**
* 获取某个资源列表
*
* @param type 验证码类型
* @param tag 资源标签(可为空)
* @return List<Resource>
*/
List<Resource> listResourcesByTypeAndTag(String type, String tag);
/**
* 获取某个模板列表
*
* @param type 验证码类型
* @param tag 资源标签(可为空)
* @return List<ResourceMap>
*/
List<ResourceMap> listTemplatesByTypeAndTag(String type, String tag);
/** /**
* 随机获取某个资源 * 随机获取某个资源
@@ -79,7 +20,7 @@ public interface ResourceStore {
* @param type type * @param type type
* @return Resource * @return Resource
*/ */
Resource randomGetResourceByTypeAndTag(String type, String tag); List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity);
/** /**
* 随机获取某个模板通过type * 随机获取某个模板通过type
@@ -87,16 +28,5 @@ public interface ResourceStore {
* @param type type * @param type type
* @return Map<String, Resource> * @return Map<String, Resource>
*/ */
ResourceMap randomGetTemplateByTypeAndTag(String type, String tag); List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag,Integer quantity);
/**
* 清除所有内置模板
*/
void clearAllTemplates();
/**
* 清除所有内置资源
*/
void clearAllResources();
} }
@@ -1,12 +1,15 @@
package cloud.tianai.captcha.resource.impl; package cloud.tianai.captcha.resource.impl;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.resource.*; import cloud.tianai.captcha.resource.*;
import cloud.tianai.captcha.resource.common.model.dto.Resource; import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap; import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import lombok.Getter; import lombok.Getter;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* @Author: 天爱有情 * @Author: 天爱有情
@@ -36,27 +39,42 @@ public class DefaultImageCaptchaResourceManager implements ImageCaptchaResourceM
this.resourceStore = new LocalMemoryResourceStore(); this.resourceStore = new LocalMemoryResourceStore();
} }
// 在这里临时加上字体缓存器 // 在这里临时加上字体缓存器
resourceStore.addListener(FontCache.getInstance()); resourceStore = new FontCache(resourceStore);
resourceStore.init(this); resourceStore.init(this);
} }
@Override @Override
public ResourceMap randomGetTemplate(String type, String tag) { public ResourceMap randomGetTemplate(String type, String tag) {
ResourceMap resourceMap = resourceStore.randomGetTemplateByTypeAndTag(type, tag); return randomGetTemplate(type, tag, 1).get(0);
if (resourceMap == null) {
throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type);
}
return resourceMap;
} }
@Override @Override
public Resource randomGetResource(String type, String tag) { public Resource randomGetResource(String type, String tag) {
Resource resource = resourceStore.randomGetResourceByTypeAndTag(type, tag); return randomGetResource(type, tag, 1).get(0);
if (resource == null) {
throw new IllegalStateException("随机获取资源错误,store中资源为空, type:" + type);
} }
return resource;
@Override
public List<ResourceMap> randomGetTemplate(String type, String tag, Integer quantity) {
List<ResourceMap> resourceMaps = resourceStore.randomGetTemplateByTypeAndTag(type, tag, quantity);
if (CollectionUtils.isEmpty(resourceMaps) || resourceMaps.size() != quantity) {
throw new IllegalStateException("随机获取**模板**错误,获取到的数量和指定数量不一致," +
" 指定获取数量[" + quantity + "],获取到的数据:[" + Optional.ofNullable(resourceMaps).orElse(Collections.emptyList()).size() + "], " +
"[type:" + type + ",tag:" + tag + "]");
} }
return resourceMaps;
}
@Override
public List<Resource> randomGetResource(String type, String tag, Integer quantity) {
List<Resource> resources = resourceStore.randomGetResourceByTypeAndTag(type, tag, quantity);
if (CollectionUtils.isEmpty(resources) || resources.size() != quantity) {
throw new IllegalStateException("随机获取**资源**错误,获取到的数量和指定数量不一致," +
" 指定获取数量[" + quantity + "],获取到的数据:[" + Optional.ofNullable(resources).orElse(Collections.emptyList()).size() + "], " +
"[type:" + type + ",tag:" + tag + "]");
}
return resources;
}
@Override @Override
public InputStream getResourceInputStream(Resource resource) { public InputStream getResourceInputStream(Resource resource) {
@@ -3,7 +3,8 @@ package cloud.tianai.captcha.resource.impl;
import cloud.tianai.captcha.common.constant.CommonConstant; import cloud.tianai.captcha.common.constant.CommonConstant;
import cloud.tianai.captcha.common.util.CollectionUtils; import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.common.util.ObjectUtils; import cloud.tianai.captcha.common.util.ObjectUtils;
import cloud.tianai.captcha.resource.AbstractResourceStore; import cloud.tianai.captcha.resource.CrudResourceStore;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.resource.common.model.dto.Resource; import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap; import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
@@ -15,43 +16,52 @@ import java.util.concurrent.ThreadLocalRandom;
* @date 2021/8/7 15:43 * @date 2021/8/7 15:43
* @Description 默认的资源存储 * @Description 默认的资源存储
*/ */
public class LocalMemoryResourceStore extends AbstractResourceStore { public class LocalMemoryResourceStore implements CrudResourceStore {
private static final String TYPE_TAG_SPLIT_FLAG = "|";
/** 用于检索 type和tag. */ /** 用于检索 type和tag. */
private Map<String, List<ResourceMap>> templateResourceTagMap = new HashMap<>(2); private final Map<String, Map<String, List<ResourceMap>>> templateResourceTagMap = new HashMap<>(2);
private Map<String, List<Resource>> resourceTagMap = new HashMap<>(2); private final Map<String, Map<String, List<Resource>>> resourceTagMap = new HashMap<>(2);
private void ensureTypeTagMapExists(Map<String, Map<String, List<Resource>>> map, String type, String tag) {
map.computeIfAbsent(type, k -> new HashMap<>())
.computeIfAbsent(tag, k -> new ArrayList<>(20));
}
private void ensureTypeTagMapExistsForTemplate(Map<String, Map<String, List<ResourceMap>>> map, String type, String tag) {
map.computeIfAbsent(type, k -> new HashMap<>())
.computeIfAbsent(tag, k -> new ArrayList<>(2));
}
@Override @Override
public void doAddResource(String type, Resource resource) { public void addResource(String type, Resource resource) {
if (ObjectUtils.isEmpty(resource.getTag())) { if (ObjectUtils.isEmpty(resource.getTag())) {
resource.setTag(CommonConstant.DEFAULT_TAG); resource.setTag(CommonConstant.DEFAULT_TAG);
} }
resourceTagMap.computeIfAbsent(mergeTypeAndTag(type, resource.getTag()), k -> new ArrayList<>(20)).add(resource); ensureTypeTagMapExists(resourceTagMap, type, resource.getTag());
resourceTagMap.get(type).get(resource.getTag()).add(resource);
} }
@Override @Override
public void doAddTemplate(String type, ResourceMap template) { public void addTemplate(String type, ResourceMap template) {
if (ObjectUtils.isEmpty(template.getTag())) { if (ObjectUtils.isEmpty(template.getTag())) {
template.setTag(CommonConstant.DEFAULT_TAG); template.setTag(CommonConstant.DEFAULT_TAG);
} }
templateResourceTagMap.computeIfAbsent(mergeTypeAndTag(type, template.getTag()), k -> new ArrayList<>(2)).add(template); ensureTypeTagMapExistsForTemplate(templateResourceTagMap, type, template.getTag());
templateResourceTagMap.get(type).get(template.getTag()).add(template);
} }
@Override @Override
public Resource doDeleteResource(String type, String id) { public Resource deleteResource(String type, String id) {
for (Map.Entry<String, List<Resource>> entry : resourceTagMap.entrySet()) { Map<String, List<Resource>> tagMap = resourceTagMap.get(type);
String k = entry.getKey(); if (tagMap == null) return null;
List<Resource> v = entry.getValue();
String splitType = splitTypeTag(k)[0]; for (List<Resource> resources : tagMap.values()) {
if (splitType.equals(type)) { Iterator<Resource> iterator = resources.iterator();
Iterator<Resource> iterator = v.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
Resource next = iterator.next(); Resource res = iterator.next();
if (next.getId().equals(id)) { if (res.getId().equals(id)) {
iterator.remove(); iterator.remove();
return next; return res;
}
} }
} }
} }
@@ -59,19 +69,17 @@ public class LocalMemoryResourceStore extends AbstractResourceStore {
} }
@Override @Override
public ResourceMap doDeleteTemplate(String type, String id) { public ResourceMap deleteTemplate(String type, String id) {
for (Map.Entry<String, List<ResourceMap>> entry : templateResourceTagMap.entrySet()) { Map<String, List<ResourceMap>> tagMap = templateResourceTagMap.get(type);
String k = entry.getKey(); if (tagMap == null) return null;
List<ResourceMap> v = entry.getValue();
String splitType = splitTypeTag(k)[0]; for (List<ResourceMap> templates : tagMap.values()) {
if (splitType.equals(type)) { Iterator<ResourceMap> iterator = templates.iterator();
Iterator<ResourceMap> iterator = v.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
ResourceMap next = iterator.next(); ResourceMap temp = iterator.next();
if (next.getId().equals(id)) { if (temp.getId().equals(id)) {
iterator.remove(); iterator.remove();
return next; return temp;
}
} }
} }
} }
@@ -81,135 +89,97 @@ public class LocalMemoryResourceStore extends AbstractResourceStore {
@Override @Override
public List<Resource> listResourcesByTypeAndTag(String type, String tag) { public List<Resource> listResourcesByTypeAndTag(String type, String tag) {
if (!ObjectUtils.isEmpty(tag)) { if (!ObjectUtils.isEmpty(tag)) {
return resourceTagMap.get(mergeTypeAndTag(type, tag)); Map<String, List<Resource>> tagMap = resourceTagMap.get(type);
return tagMap == null ? Collections.emptyList() : tagMap.getOrDefault(tag, Collections.emptyList());
} }
List<Resource> resourceList = new ArrayList<>(); List<Resource> result = new ArrayList<>();
resourceTagMap.forEach((k, v) -> { Map<String, List<Resource>> tagMap = resourceTagMap.get(type);
String splitType = splitTypeTag(k)[0]; if (tagMap != null) {
if (splitType.equals(type)) { for (List<Resource> list : tagMap.values()) {
resourceList.addAll(v); result.addAll(list);
} }
}); }
return resourceList; return result;
} }
@Override @Override
public List<ResourceMap> listTemplatesByTypeAndTag(String type, String tag) { public List<ResourceMap> listTemplatesByTypeAndTag(String type, String tag) {
if (!ObjectUtils.isEmpty(tag)) { if (!ObjectUtils.isEmpty(tag)) {
return templateResourceTagMap.get(mergeTypeAndTag(type, tag)); Map<String, List<ResourceMap>> tagMap = templateResourceTagMap.get(type);
return tagMap == null ? Collections.emptyList() : tagMap.getOrDefault(tag, Collections.emptyList());
} }
List<ResourceMap> resourceMapList = new ArrayList<>(); List<ResourceMap> result = new ArrayList<>();
templateResourceTagMap.forEach((k, v) -> { Map<String, List<ResourceMap>> tagMap = templateResourceTagMap.get(type);
String splitType = splitTypeTag(k)[0]; if (tagMap != null) {
if (splitType.equals(type)) { for (List<ResourceMap> list : tagMap.values()) {
resourceMapList.addAll(v); result.addAll(list);
} }
}); }
return resourceMapList; return result;
} }
@Override @Override
public Resource doRandomGetResourceByTypeAndTag(String type, String tag) { public void init(ImageCaptchaResourceManager resourceManager) {
List<Resource> resources = resourceTagMap.get(mergeTypeAndTag(type, tag));
}
@Override
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
List<Resource> resources = listResourcesByTypeAndTag(type, tag);
if (CollectionUtils.isEmpty(resources)) { if (CollectionUtils.isEmpty(resources)) {
throw new IllegalStateException("随机获取资源错误,store中资源为空, type:" + type + ",tag:" + tag); throw new IllegalStateException("随机获取资源错误,store中资源为空, type:" + type + ",tag:" + tag);
} }
if (resources.size() == 1) { int size = resources.size();
return resources.get(0); if (quantity > size) {
} throw new IllegalArgumentException("请求的资源数量超过可用资源总数");
int randomIndex = ThreadLocalRandom.current().nextInt(resources.size());
try {
return resources.get(randomIndex);
} catch (IndexOutOfBoundsException e) {
try {
Thread.sleep(0);
} catch (InterruptedException ex) {
// ignore
}
return doRandomGetResourceByTypeAndTag(type, tag);
}
} }
Set<Integer> indexes = new HashSet<>(quantity);
while (indexes.size() < quantity) {
indexes.add(ThreadLocalRandom.current().nextInt(size));
}
List<Resource> result = new ArrayList<>(quantity);
for (int index : indexes) {
result.add(resources.get(index));
}
return result;
}
@Override @Override
public ResourceMap doRandomGetTemplateByTypeAndTag(String type, String tag) { public List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag, Integer quantity) {
List<ResourceMap> templateList = templateResourceTagMap.get(mergeTypeAndTag(type, tag)); List<ResourceMap> templates = listTemplatesByTypeAndTag(type, tag);
if (CollectionUtils.isEmpty(templateList)) { if (CollectionUtils.isEmpty(templates)) {
throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type + ",tag:" + tag); throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type + ",tag:" + tag);
} }
if (templateList.size() == 1) { int size = templates.size();
return templateList.get(0); if (quantity > size) {
} throw new IllegalArgumentException("请求的模板数量超过可用模板总数");
int randomIndex = ThreadLocalRandom.current().nextInt(templateList.size());
try {
return templateList.get(randomIndex);
} catch (IndexOutOfBoundsException e) {
try {
Thread.sleep(0);
} catch (InterruptedException ex) {
// ignore
}
return doRandomGetTemplateByTypeAndTag(type, tag);
}
} }
public String mergeTypeAndTag(String type, String tag) { Set<Integer> indexes = new HashSet<>(quantity);
if (tag == null) { while (indexes.size() < quantity) {
tag = CommonConstant.DEFAULT_TAG; indexes.add(ThreadLocalRandom.current().nextInt(size));
}
return type + TYPE_TAG_SPLIT_FLAG + tag;
} }
public String[] splitTypeTag(String k) { List<ResourceMap> result = new ArrayList<>(quantity);
return k.split("\\" + TYPE_TAG_SPLIT_FLAG); for (int index : indexes) {
result.add(templates.get(index));
} }
return result;
public void clearResources(String type, String tag) {
resourceTagMap.remove(mergeTypeAndTag(type, tag));
} }
@Override @Override
public void doClearAllResources() { public void clearAllResources() {
resourceTagMap.clear(); resourceTagMap.clear();
} }
public Map<String, List<Resource>> listAllResources() {
return resourceTagMap;
}
public List<Resource> listResourcesByType(String type, String tag) {
return resourceTagMap.getOrDefault(mergeTypeAndTag(type, tag), Collections.emptyList());
}
public int getAllResourceCount() {
int count = 0;
for (List<Resource> value : resourceTagMap.values()) {
count += value.size();
}
return count;
}
public int getResourceCount(String type, String tag) {
return resourceTagMap.getOrDefault(mergeTypeAndTag(type, tag), Collections.emptyList()).size();
}
@Override @Override
public void doClearAllTemplates() { public void clearAllTemplates() {
templateResourceTagMap.clear(); templateResourceTagMap.clear();
} }
public void clearTemplates(String type, String tag) {
templateResourceTagMap.remove(mergeTypeAndTag(type, tag));
}
public List<ResourceMap> listTemplatesByType(String type, String tag) {
return templateResourceTagMap.getOrDefault(mergeTypeAndTag(type, tag), Collections.emptyList());
}
public Map<String, List<ResourceMap>> listAllTemplates() {
return templateResourceTagMap;
}
} }
@@ -38,6 +38,8 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
public static final String TOLERANT_KEY = "tolerant"; public static final String TOLERANT_KEY = "tolerant";
/** 类型 key, 标识是哪张类型的验证码. */ /** 类型 key, 标识是哪张类型的验证码. */
public static final String TYPE_KEY = "type"; public static final String TYPE_KEY = "type";
/** 点选类验证码验证时判断是否需要校验顺序. */
public static final String CLICK_IMAGE_CHECK_ORDER_KEY = "click_image_check_order";
/** 计算当前验证码用户滑动的百分比率 - 生成时的百分比率, 多个的话取均值. */ /** 计算当前验证码用户滑动的百分比率 - 生成时的百分比率, 多个的话取均值. */
public static final String USER_CURRENT_PERCENTAGE_STD = "user_current_percentage_std"; public static final String USER_CURRENT_PERCENTAGE_STD = "user_current_percentage_std";
public static final String USER_CURRENT_PERCENTAGE = "user_current_percentage"; public static final String USER_CURRENT_PERCENTAGE = "user_current_percentage";
@@ -145,6 +147,12 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
map.put(TOLERANT_KEY, tolerant); map.put(TOLERANT_KEY, tolerant);
} }
} }
// 新增是否判断顺序
if (imageCaptchaInfo.getData() != null && imageCaptchaInfo.getData().getData() != null) {
map.put(CLICK_IMAGE_CHECK_ORDER_KEY, imageCaptchaInfo.getData().getData().getOrDefault(CLICK_IMAGE_CHECK_ORDER_KEY, true));
} else {
map.put(CLICK_IMAGE_CHECK_ORDER_KEY, true);
}
// 添加点选验证数据 // 添加点选验证数据
map.put(PERCENTAGE_KEY, sb.toString()); map.put(PERCENTAGE_KEY, sb.toString());
} else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) { } else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) {
@@ -156,7 +164,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
@Override @Override
public ApiResponse<?> valid(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData) { public ApiResponse<?> valid(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData) {
// 读容错值 // 读容错值
Float tolerant = imageCaptchaValidData.getFloat(TOLERANT_KEY, defaultTolerant); Float tolerant = recalculateTolerant(imageCaptchaValidData.getFloat(TOLERANT_KEY, defaultTolerant), imageCaptchaTrack, imageCaptchaValidData);
// 读验证码类型 // 读验证码类型
String type = imageCaptchaValidData.getString(TYPE_KEY, CaptchaTypeConstant.SLIDER); String type = imageCaptchaValidData.getString(TYPE_KEY, CaptchaTypeConstant.SLIDER);
// 验证前 // 验证前
@@ -176,15 +184,26 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
return ApiResponse.ofCheckError("没有解析到滑动轨迹"); return ApiResponse.ofCheckError("没有解析到滑动轨迹");
} }
// 验证 // 验证
ApiResponse<?> response;
boolean valid = doValid(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); boolean valid = doValid(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
return afterValid(valid, imageCaptchaTrack, imageCaptchaValidData, tolerant, type); return afterValid(valid, imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
} }
/**
* 一个模板方法, 用于自定义处理容错值
*
* @param tolerant 容错值
* @param imageCaptchaTrack imageCaptchaTrack
* @param imageCaptchaValidData captchaValidData
* @return
*/
public Float recalculateTolerant(Float tolerant, ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData) {
return tolerant;
}
/** /**
* 验证前 * 验证前
* *
* @param imageCaptchaTrack sliderCaptchaTrack * @param imageCaptchaTrack imageCaptchaTrack
* @param captchaValidData captchaValidData * @param captchaValidData captchaValidData
* @param tolerant tolerant * @param tolerant tolerant
* @param type type * @param type type
@@ -197,7 +216,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
/** /**
* 验证后 * 验证后
* *
* @param imageCaptchaTrack sliderCaptchaTrack * @param imageCaptchaTrack imageCaptchaTrack
* @param captchaValidData captchaValidData * @param captchaValidData captchaValidData
* @param tolerant tolerant * @param tolerant tolerant
* @param type type * @param type type
@@ -241,7 +260,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
/** /**
* 校验点选验证码 * 校验点选验证码
* *
* @param imageCaptchaTrack sliderCaptchaTrack * @param imageCaptchaTrack imageCaptchaTrack
* @param imageCaptchaValidData imageCaptchaValidData * @param imageCaptchaValidData imageCaptchaValidData
* @param tolerant tolerant * @param tolerant tolerant
* @param type type * @param type type
@@ -252,6 +271,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
Float tolerant, Float tolerant,
String type) { String type) {
String validStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null); String validStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null);
Object checkOrder = imageCaptchaValidData.getOrDefault(CLICK_IMAGE_CHECK_ORDER_KEY, true);
if (ObjectUtils.isEmpty(validStr)) { if (ObjectUtils.isEmpty(validStr)) {
return false; return false;
} }
@@ -271,18 +291,39 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
List<Double> percentages = new ArrayList<>(); List<Double> percentages = new ArrayList<>();
for (int i = 0; i < splitArr.length; i++) { for (int i = 0; i < splitArr.length; i++) {
ImageCaptchaTrack.Track track = clickTrackList.get(i);
String posStr = splitArr[i]; String posStr = splitArr[i];
String[] posArr = posStr.split(","); String[] posArr = posStr.split(",");
float xPercentage = Float.parseFloat(posArr[0]); float xPercentage = Float.parseFloat(posArr[0]);
float yPercentage = Float.parseFloat(posArr[1]); float yPercentage = Float.parseFloat(posArr[1]);
float calcXPercentage = 0f;
float calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth()); float calcYPercentage = 0f;
float calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight()); if (Boolean.TRUE.equals(checkOrder)) {
ImageCaptchaTrack.Track track = clickTrackList.get(0);
calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth());
calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight());
if (!checkPercentage(calcXPercentage, xPercentage, tolerant) if (!checkPercentage(calcXPercentage, xPercentage, tolerant)
|| !checkPercentage(calcYPercentage, yPercentage, tolerant)) { || !checkPercentage(calcYPercentage, yPercentage, tolerant)) {
return false; return false;
} }
clickTrackList.remove(0);
} else {
boolean flag = false;
for (int a = 0; a < clickTrackList.size(); a++) {
ImageCaptchaTrack.Track track = clickTrackList.get(a);
calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth());
calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight());
if (checkPercentage(calcXPercentage, xPercentage, tolerant)
&& checkPercentage(calcYPercentage, yPercentage, tolerant)) {
// 验证命中
clickTrackList.remove(a);
flag = true;
break;
}
}
if (!flag) {
return false;
}
}
if (i > 0) { if (i > 0) {
sb.append("|"); sb.append("|");
} }
@@ -296,7 +337,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
/** /**
* 校验滑动验证码 * 校验滑动验证码
* *
* @param imageCaptchaTrack sliderCaptchaTrack * @param imageCaptchaTrack imageCaptchaTrack
* @param imageCaptchaValidData imageCaptchaValidData * @param imageCaptchaValidData imageCaptchaValidData
* @param tolerant tolerant * @param tolerant tolerant
* @param type type * @param type type