mirror of
https://github.com/dromara/tianai-captcha.git
synced 2026-05-07 06:04:34 +08:00
修复资源泄露问题并优化资源管理
- 添加 AutoCloseable 接口实现,支持自动资源释放 - 修复 Graphics2D、ImageOutputStream 等资源未正确关闭的问题 - 为 Spring Bean 添加 destroyMethod 配置 - 添加定时任务线程池的正确关闭逻辑 - FontCache 添加缓存大小限制防止内存泄露 - 优化 StandardWordClickImageCaptchaGenerator 代码结构
This commit is contained in:
+20
-4
@@ -2,11 +2,15 @@ package cloud.tianai.captcha.spring.autoconfiguration;
|
|||||||
|
|
||||||
import cloud.tianai.captcha.cache.CacheStore;
|
import cloud.tianai.captcha.cache.CacheStore;
|
||||||
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
|
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
|
||||||
|
import cloud.tianai.captcha.resource.ResourceStore;
|
||||||
|
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
|
||||||
|
import cloud.tianai.captcha.spring.plugins.RedisResourceStore;
|
||||||
import cloud.tianai.captcha.spring.store.impl.RedisCacheStore;
|
import cloud.tianai.captcha.spring.store.impl.RedisCacheStore;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
@@ -17,8 +21,6 @@ import org.springframework.data.redis.core.StringRedisTemplate;
|
|||||||
*
|
*
|
||||||
* @author Hccake
|
* @author Hccake
|
||||||
*/
|
*/
|
||||||
@AutoConfigureAfter(name = {"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration",
|
|
||||||
"org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration"})
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
public class CacheStoreAutoConfiguration {
|
public class CacheStoreAutoConfiguration {
|
||||||
|
|
||||||
@@ -31,15 +33,23 @@ public class CacheStoreAutoConfiguration {
|
|||||||
@Order(1)
|
@Order(1)
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(StringRedisTemplate.class)
|
@ConditionalOnClass(StringRedisTemplate.class)
|
||||||
|
@AutoConfigureAfter({RedisAutoConfiguration.class})
|
||||||
public static class RedisCacheStoreConfiguration {
|
public static class RedisCacheStoreConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean(destroyMethod = "")
|
||||||
@ConditionalOnBean(StringRedisTemplate.class)
|
@ConditionalOnBean(StringRedisTemplate.class)
|
||||||
@ConditionalOnMissingBean(CacheStore.class)
|
@ConditionalOnMissingBean(CacheStore.class)
|
||||||
public CacheStore redis(StringRedisTemplate redisTemplate) {
|
public CacheStore redis(StringRedisTemplate redisTemplate) {
|
||||||
return new RedisCacheStore(redisTemplate);
|
return new RedisCacheStore(redisTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnBean(StringRedisTemplate.class)
|
||||||
|
@ConditionalOnMissingBean(ResourceStore.class)
|
||||||
|
public ResourceStore redisResourceStore(StringRedisTemplate redisTemplate) {
|
||||||
|
return new RedisResourceStore(redisTemplate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,12 +62,18 @@ public class CacheStoreAutoConfiguration {
|
|||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
public static class LocalCacheStoreConfiguration {
|
public static class LocalCacheStoreConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean(destroyMethod = "close")
|
||||||
@ConditionalOnMissingBean(CacheStore.class)
|
@ConditionalOnMissingBean(CacheStore.class)
|
||||||
public CacheStore local() {
|
public CacheStore local() {
|
||||||
return new LocalCacheStore();
|
return new LocalCacheStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(ResourceStore.class)
|
||||||
|
public ResourceStore resourceStore() {
|
||||||
|
return new LocalMemoryResourceStore();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-23
@@ -4,33 +4,31 @@ package cloud.tianai.captcha.spring.autoconfiguration;
|
|||||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||||
import cloud.tianai.captcha.application.TACBuilder;
|
import cloud.tianai.captcha.application.TACBuilder;
|
||||||
import cloud.tianai.captcha.cache.CacheStore;
|
import cloud.tianai.captcha.cache.CacheStore;
|
||||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
|
||||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||||
import cloud.tianai.captcha.generator.ImageTransform;
|
import cloud.tianai.captcha.generator.ImageTransform;
|
||||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptorGroup;
|
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
|
||||||
import cloud.tianai.captcha.interceptor.impl.ParamCheckCaptchaInterceptor;
|
|
||||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||||
import cloud.tianai.captcha.resource.ResourceProviders;
|
import cloud.tianai.captcha.resource.ResourceProviders;
|
||||||
import cloud.tianai.captcha.resource.ResourceStore;
|
import cloud.tianai.captcha.resource.ResourceStore;
|
||||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||||
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
|
import cloud.tianai.captcha.spring.common.util.URL;
|
||||||
import cloud.tianai.captcha.spring.plugins.SpringMultiImageCaptchaGenerator;
|
import cloud.tianai.captcha.spring.plugins.SpringMultiImageCaptchaGenerator;
|
||||||
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
|
import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.BeanFactory;
|
import org.springframework.beans.factory.BeanFactory;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Role;
|
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author: 天爱有情
|
* @Author: 天爱有情
|
||||||
@@ -44,17 +42,13 @@ import org.springframework.core.annotation.Order;
|
|||||||
@EnableConfigurationProperties({SpringImageCaptchaProperties.class})
|
@EnableConfigurationProperties({SpringImageCaptchaProperties.class})
|
||||||
public class ImageCaptchaAutoConfiguration {
|
public class ImageCaptchaAutoConfiguration {
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
public ResourceStore resourceStore() {
|
|
||||||
return new LocalMemoryResourceStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
|
@ConditionalOnBean(ResourceStore.class)
|
||||||
public ImageCaptchaResourceManager imageCaptchaResourceManager(ResourceStore resourceStore) {
|
public ImageCaptchaResourceManager imageCaptchaResourceManager(ResourceStore resourceStore) {
|
||||||
ResourceProviders resourceProviders = new ResourceProviders();
|
ResourceProviders resourceProviders = new ResourceProviders();
|
||||||
return new DefaultImageCaptchaResourceManager(resourceStore,resourceProviders);
|
return new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -70,7 +64,9 @@ public class ImageCaptchaAutoConfiguration {
|
|||||||
ImageCaptchaResourceManager captchaResourceManager,
|
ImageCaptchaResourceManager captchaResourceManager,
|
||||||
ImageTransform imageTransform,
|
ImageTransform imageTransform,
|
||||||
BeanFactory beanFactory) {
|
BeanFactory beanFactory) {
|
||||||
return new SpringMultiImageCaptchaGenerator(captchaResourceManager, imageTransform, beanFactory);
|
// 构建多验证码生成器
|
||||||
|
ImageCaptchaGenerator captchaGenerator = new SpringMultiImageCaptchaGenerator(captchaResourceManager, imageTransform, beanFactory);
|
||||||
|
return captchaGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -80,26 +76,24 @@ public class ImageCaptchaAutoConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public CaptchaInterceptor captchaInterceptor() {
|
public CaptchaInterceptor captchaInterceptor() {
|
||||||
CaptchaInterceptorGroup group = new CaptchaInterceptorGroup();
|
return new EmptyCaptchaInterceptor();
|
||||||
group.addInterceptor(new ParamCheckCaptchaInterceptor());
|
|
||||||
// group.addInterceptor(new BasicTrackCaptchaInterceptor());
|
|
||||||
return group;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(destroyMethod = "close")
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
|
@ConditionalOnBean(CacheStore.class)
|
||||||
public ImageCaptchaApplication imageCaptchaApplication(ImageCaptchaGenerator captchaGenerator,
|
public ImageCaptchaApplication imageCaptchaApplication(ImageCaptchaGenerator captchaGenerator,
|
||||||
ImageCaptchaValidator imageCaptchaValidator,
|
ImageCaptchaValidator imageCaptchaValidator,
|
||||||
CacheStore cacheStore,
|
CacheStore cacheStore,
|
||||||
ResourceStore resourceStore,
|
ResourceStore resourceStore,
|
||||||
SpringImageCaptchaProperties prop,
|
SpringImageCaptchaProperties prop,
|
||||||
CaptchaInterceptor captchaInterceptor,
|
CaptchaInterceptor captchaInterceptor,
|
||||||
ApplicationContext applicationContext) {
|
ApplicationContext applicationContext
|
||||||
TACBuilder tacBuilder = TACBuilder.builder(resourceStore)
|
) {
|
||||||
|
TACBuilder tacBuilder = TACBuilder.builder()
|
||||||
|
.setResourceStore(resourceStore)
|
||||||
.setGenerator(captchaGenerator)
|
.setGenerator(captchaGenerator)
|
||||||
.setValidator(imageCaptchaValidator)
|
.setValidator(imageCaptchaValidator)
|
||||||
.setCacheStore(cacheStore)
|
.setCacheStore(cacheStore)
|
||||||
@@ -116,7 +110,10 @@ public class ImageCaptchaAutoConfiguration {
|
|||||||
String[] split = index > 0 ? new String[]{fontPath.substring(0, index), fontPath.substring(index + 1)} : new String[]{"", fontPath};
|
String[] split = index > 0 ? new String[]{fontPath.substring(0, index), fontPath.substring(index + 1)} : new String[]{"", fontPath};
|
||||||
String type = split[0];
|
String type = split[0];
|
||||||
String path = split[1];
|
String path = split[1];
|
||||||
tacBuilder.addFont(new cloud.tianai.captcha.resource.common.model.dto.Resource(type, path));
|
|
||||||
|
URL fontUrl = URL.valueOf(fontPath);
|
||||||
|
String tag = fontUrl.getParam(URL.PARAM_TAG_KEY, null);
|
||||||
|
tacBuilder.addFont(new cloud.tianai.captcha.resource.common.model.dto.Resource(type, path, tag));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImageCaptchaApplication target = tacBuilder.build();
|
ImageCaptchaApplication target = tacBuilder.build();
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package cloud.tianai.captcha.spring.common.util;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class URL {
|
||||||
|
|
||||||
|
public static final String PARAM_TAG_KEY = "tag";
|
||||||
|
|
||||||
|
private String protocol;
|
||||||
|
private String path;
|
||||||
|
private Map<String, String> params;
|
||||||
|
|
||||||
|
public String getParam(String key, String defaultValue) {
|
||||||
|
return params.getOrDefault(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URL valueOf(String input) {
|
||||||
|
// 分割协议和剩余部分
|
||||||
|
String[] parts = input.split(":", 2);
|
||||||
|
String protocol = parts[0];
|
||||||
|
String remaining = parts[1];
|
||||||
|
|
||||||
|
// 分割路径和查询参数
|
||||||
|
String path;
|
||||||
|
String query = null;
|
||||||
|
|
||||||
|
if (remaining.contains("?")) {
|
||||||
|
String[] pathQuerySplit = remaining.split("\\?", 2);
|
||||||
|
path = pathQuerySplit[0];
|
||||||
|
query = pathQuerySplit[1];
|
||||||
|
} else {
|
||||||
|
path = remaining;
|
||||||
|
}
|
||||||
|
if (path.startsWith("//")) {
|
||||||
|
path = path.substring(2);
|
||||||
|
}
|
||||||
|
// 处理查询参数,提取键值对
|
||||||
|
Map<String, String> queryParams = new HashMap<>();
|
||||||
|
if (query != null) {
|
||||||
|
for (String param : query.split("&")) {
|
||||||
|
String[] keyValue = param.split("=", 2);
|
||||||
|
String key = keyValue[0];
|
||||||
|
String value = keyValue.length > 1 ? keyValue[1] : "";
|
||||||
|
queryParams.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(protocol, path, queryParams);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
package cloud.tianai.captcha.spring.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
package cloud.tianai.captcha.spring.plugins;
|
||||||
|
|
||||||
|
import cloud.tianai.captcha.common.constant.CommonConstant;
|
||||||
|
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||||
|
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.ResourceMap;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: 天爱有情
|
||||||
|
* @date 2023/8/23 10:52
|
||||||
|
* @Description 基于redis的store
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RedisResourceStore implements CrudResourceStore {
|
||||||
|
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String resourcePrefix = "captcha:config:resource:";
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String templatePrefix = "captcha:config:template:";
|
||||||
|
private Gson gson = new Gson();
|
||||||
|
|
||||||
|
public String joinResourceKey(String type, String tag) {
|
||||||
|
if (tag == null) {
|
||||||
|
tag = CommonConstant.DEFAULT_TAG;
|
||||||
|
}
|
||||||
|
type = type.toUpperCase();
|
||||||
|
return resourcePrefix + tag + ":" + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String joinTemplateKey(String type, String tag) {
|
||||||
|
if (tag == null) {
|
||||||
|
tag = CommonConstant.DEFAULT_TAG;
|
||||||
|
}
|
||||||
|
type = type.toUpperCase();
|
||||||
|
return templatePrefix + tag + ":" + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Resource> getResources(String type, String tag) {
|
||||||
|
String key = joinResourceKey(type, tag);
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size == null || size < 1) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<String> range = redisTemplate.opsForList().range(key, 0, size);
|
||||||
|
List<Resource> result = new ArrayList<>(range.size());
|
||||||
|
for (String json : range) {
|
||||||
|
result.add(gson.fromJson(json, Resource.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ResourceMap> getTemplates(String type, String tag) {
|
||||||
|
String key = joinTemplateKey(type, tag);
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size == null || size < 1) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<String> range = redisTemplate.opsForList().range(key, 0, size);
|
||||||
|
List<ResourceMap> result = new ArrayList<>(range.size());
|
||||||
|
for (String json : range) {
|
||||||
|
result.add(gson.fromJson(json, ResourceMap.class));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setResources(String type, String tag, List<Resource> resources) {
|
||||||
|
String key = joinResourceKey(type, tag);
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size != null && size > 0) {
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
for (Resource resource : resources) {
|
||||||
|
addResource(type, resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTemplates(String type, String tag, List<ResourceMap> templates) {
|
||||||
|
String key = joinTemplateKey(type, tag);
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size != null && size > 0) {
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
for (ResourceMap template : templates) {
|
||||||
|
addTemplate(type, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResource(String type, Resource resource) {
|
||||||
|
// 添加tag标签字典
|
||||||
|
redisTemplate.opsForList().rightPush(joinResourceKey(type, resource.getTag()), gson.toJson(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTemplate(String type, ResourceMap template) {
|
||||||
|
// 添加tag标签字典
|
||||||
|
redisTemplate.opsForList().rightPush(joinTemplateKey(type, template.getTag()), gson.toJson(template));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Resource deleteResource(String type, String id) {
|
||||||
|
Set<String> keys = redisTemplate.keys(joinResourceKey(type, "*"));
|
||||||
|
if (!CollectionUtils.isEmpty(keys)) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size == null || size < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> range = redisTemplate.opsForList().range(key, 0, size);
|
||||||
|
if (range != null) {
|
||||||
|
for (String json : range) {
|
||||||
|
Resource resource = gson.fromJson(json, Resource.class);
|
||||||
|
if (resource.getId().equals(id)) {
|
||||||
|
redisTemplate.opsForList().remove(key, 1, json);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceMap deleteTemplate(String type, String id) {
|
||||||
|
Set<String> keys = redisTemplate.keys(joinTemplateKey(type, "*"));
|
||||||
|
if (!CollectionUtils.isEmpty(keys)) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size == null || size < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> range = redisTemplate.opsForList().range(key, 0, size);
|
||||||
|
if (range != null) {
|
||||||
|
for (String json : range) {
|
||||||
|
ResourceMap resourceMap = gson.fromJson(json, ResourceMap.class);
|
||||||
|
if (resourceMap.getId().equals(id)) {
|
||||||
|
redisTemplate.opsForList().remove(key, 1, json);
|
||||||
|
return resourceMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Resource> listResourcesByTypeAndTag(String type, String tag) {
|
||||||
|
if (StringUtils.isNotBlank(tag)) {
|
||||||
|
return getResources(type, tag);
|
||||||
|
}
|
||||||
|
Set<String> keys = redisTemplate.keys(joinResourceKey(type, "*"));
|
||||||
|
if (!CollectionUtils.isEmpty(keys)) {
|
||||||
|
List<Resource> resources = new ArrayList<>();
|
||||||
|
for (String key : keys) {
|
||||||
|
Long size1 = redisTemplate.opsForList().size(key);
|
||||||
|
if (size1 == null || size1 < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> range = redisTemplate.opsForList().range(key, 0, size1);
|
||||||
|
if (range != null) {
|
||||||
|
for (String json : range) {
|
||||||
|
Resource resource = gson.fromJson(json, Resource.class);
|
||||||
|
resources.add(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ResourceMap> listTemplatesByTypeAndTag(String type, String tag) {
|
||||||
|
if (StringUtils.isNotBlank(tag)) {
|
||||||
|
return getTemplates(type, tag);
|
||||||
|
}
|
||||||
|
Set<String> keys = redisTemplate.keys(joinTemplateKey(type, "*"));
|
||||||
|
if (!CollectionUtils.isEmpty(keys)) {
|
||||||
|
List<ResourceMap> templates = new ArrayList<>();
|
||||||
|
for (String key : keys) {
|
||||||
|
Long size1 = redisTemplate.opsForList().size(key);
|
||||||
|
if (size1 == null || size1 < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> range = redisTemplate.opsForList().range(key, 0, size1);
|
||||||
|
if (range != null) {
|
||||||
|
for (String json : range) {
|
||||||
|
ResourceMap template = gson.fromJson(json, ResourceMap.class);
|
||||||
|
templates.add(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ImageCaptchaResourceManager resourceManager) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
|
||||||
|
String key = joinResourceKey(type, tag);
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size == null || quantity > size) {
|
||||||
|
throw new IllegalArgumentException("请求的资源数量超过可用资源总数");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> indexes = new HashSet<>(quantity);
|
||||||
|
while (indexes.size() < quantity) {
|
||||||
|
indexes.add(ThreadLocalRandom.current().nextLong(size));
|
||||||
|
}
|
||||||
|
List<Resource> result = new ArrayList<>(quantity);
|
||||||
|
for (Long index : indexes) {
|
||||||
|
String resourceJson = redisTemplate.opsForList().index(key, index);
|
||||||
|
result.add(gson.fromJson(resourceJson, Resource.class));
|
||||||
|
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag, Integer quantity) {
|
||||||
|
String key = joinTemplateKey(type, tag);
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size == null || size < 1) {
|
||||||
|
throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type);
|
||||||
|
}
|
||||||
|
if (quantity > size) {
|
||||||
|
throw new IllegalArgumentException("请求的模板数量超过可用模板总数");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> indexes = new HashSet<>(quantity);
|
||||||
|
while (indexes.size() < quantity) {
|
||||||
|
indexes.add(ThreadLocalRandom.current().nextLong(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResourceMap> result = new ArrayList<>(quantity);
|
||||||
|
|
||||||
|
for (Long index : indexes) {
|
||||||
|
String resourceJson = redisTemplate.opsForList().index(key, index);
|
||||||
|
result.add(gson.fromJson(resourceJson, ResourceMap.class));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAllTemplates() {
|
||||||
|
Set<String> keys = redisTemplate.keys(templatePrefix + "*");
|
||||||
|
if (!CollectionUtils.isEmpty(keys)) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAllResources() {
|
||||||
|
Set<String> keys = redisTemplate.keys(resourcePrefix + "*");
|
||||||
|
if (!CollectionUtils.isEmpty(keys)) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+5
@@ -68,4 +68,9 @@ public class RedisCacheStore implements CacheStore {
|
|||||||
}
|
}
|
||||||
return Long.valueOf(value);
|
return Long.valueOf(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-14
@@ -2,16 +2,17 @@ package cloud.tianai.captcha.application;
|
|||||||
|
|
||||||
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.cache.StoreCacheKeyPrefix;
|
|
||||||
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 cloud.tianai.captcha.common.exception.ImageCaptchaException;
|
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
|
||||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||||
import cloud.tianai.captcha.common.response.ApiResponseStatusConstant;
|
import cloud.tianai.captcha.common.response.ApiResponseStatusConstant;
|
||||||
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.generator.ImageCaptchaGenerator;
|
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||||
|
import cloud.tianai.captcha.generator.common.model.dto.ParamKeyEnum;
|
||||||
import cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator;
|
import cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator;
|
||||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||||
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
|
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
|
||||||
@@ -42,8 +43,6 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
private CacheStore cacheStore;
|
private CacheStore cacheStore;
|
||||||
/** 验证码配置属性. */
|
/** 验证码配置属性. */
|
||||||
private final ImageCaptchaProperties prop;
|
private final ImageCaptchaProperties prop;
|
||||||
/** 缓存key 前缀处理器. */
|
|
||||||
private StoreCacheKeyPrefix storeCacheKeyPrefix;
|
|
||||||
/** 默认的过期时间. */
|
/** 默认的过期时间. */
|
||||||
private long defaultExpire = 20000L;
|
private long defaultExpire = 20000L;
|
||||||
|
|
||||||
@@ -53,10 +52,9 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
ImageCaptchaValidator imageCaptchaValidator,
|
ImageCaptchaValidator imageCaptchaValidator,
|
||||||
CacheStore cacheStore,
|
CacheStore cacheStore,
|
||||||
ImageCaptchaProperties prop,
|
ImageCaptchaProperties prop,
|
||||||
CaptchaInterceptor captchaInterceptor,
|
CaptchaInterceptor captchaInterceptor) {
|
||||||
StoreCacheKeyPrefix storeCacheKeyPrefix) {
|
|
||||||
this.prop = prop;
|
this.prop = prop;
|
||||||
this.storeCacheKeyPrefix = null != storeCacheKeyPrefix? storeCacheKeyPrefix: StoreCacheKeyPrefix.prefixed(prop.getPrefix());
|
|
||||||
setImageCaptchaValidator(imageCaptchaValidator);
|
setImageCaptchaValidator(imageCaptchaValidator);
|
||||||
setCacheStore(cacheStore);
|
setCacheStore(cacheStore);
|
||||||
// 默认过期时间
|
// 默认过期时间
|
||||||
@@ -99,9 +97,13 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
if (captchaResponse != null) {
|
if (captchaResponse != null) {
|
||||||
return captchaResponse;
|
return captchaResponse;
|
||||||
}
|
}
|
||||||
|
String id = generatorId(param);
|
||||||
|
|
||||||
ImageCaptchaInfo imageCaptchaInfo = getImageCaptchaGenerator().generateCaptchaImage(param);
|
ImageCaptchaInfo imageCaptchaInfo = getImageCaptchaGenerator().generateCaptchaImage(param);
|
||||||
captchaResponse = convertToCaptchaResponse(imageCaptchaInfo);
|
captchaResponse = convertToCaptchaResponse(id, imageCaptchaInfo);
|
||||||
afterGenerateCaptcha(imageCaptchaInfo, captchaResponse);
|
afterGenerateCaptcha(imageCaptchaInfo, captchaResponse);
|
||||||
|
|
||||||
|
param.removeParam(ParamKeyEnum.ID);
|
||||||
return captchaResponse;
|
return captchaResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,14 +127,14 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ApiResponse<ImageCaptchaVO> convertToCaptchaResponse(ImageCaptchaInfo imageCaptchaInfo) {
|
public ApiResponse<ImageCaptchaVO> convertToCaptchaResponse(String id, ImageCaptchaInfo imageCaptchaInfo) {
|
||||||
if (imageCaptchaInfo == null) {
|
if (imageCaptchaInfo == null) {
|
||||||
// 要是生成失败
|
// 要是生成失败
|
||||||
throw new ImageCaptchaException("生成验证码失败,验证码生成为空");
|
throw new ImageCaptchaException("生成验证码失败,验证码生成为空");
|
||||||
}
|
}
|
||||||
// 生成ID
|
// 生成ID
|
||||||
String id = generatorId(imageCaptchaInfo);
|
|
||||||
ApiResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData(imageCaptchaInfo);
|
ApiResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData( imageCaptchaInfo);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -209,8 +211,14 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String generatorId(ImageCaptchaInfo imageCaptchaInfo) {
|
protected String generatorId(GenerateParam param) {
|
||||||
return imageCaptchaInfo.getType() + ID_SPLIT + UUID.randomUUID().toString().replace("-", "");
|
String id = param.getParam(ParamKeyEnum.ID);
|
||||||
|
if (ObjectUtils.isEmpty(id)) {
|
||||||
|
id = param.getType() + ID_SPLIT + UUID.randomUUID().toString().replace("-", "");
|
||||||
|
|
||||||
|
param.addParam(ParamKeyEnum.ID, id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -239,8 +247,7 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String getKey(String id) {
|
protected String getKey(String id) {
|
||||||
// 改为通过接口扩展
|
return prop.getPrefix().concat(":").concat(id);
|
||||||
return storeCacheKeyPrefix.compute(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -292,6 +299,7 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
// ============== 一些模板方法 ================
|
// ============== 一些模板方法 ================
|
||||||
|
|
||||||
private void afterGenerateCaptcha(ImageCaptchaInfo imageCaptchaInfo, ApiResponse<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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,4 +323,24 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData, basicValid);
|
return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData, basicValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁应用程序,释放资源
|
||||||
|
* 建议在不再使用或应用关闭时调用
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// 如果生成器是 CacheImageCaptchaGenerator,需要关闭其定时任务
|
||||||
|
if (captchaGenerator instanceof CacheImageCaptchaGenerator) {
|
||||||
|
((CacheImageCaptchaGenerator) captchaGenerator).destroy();
|
||||||
|
}
|
||||||
|
// 如果缓存存储是 AutoCloseable,关闭它
|
||||||
|
if (cacheStore instanceof AutoCloseable) {
|
||||||
|
try {
|
||||||
|
((AutoCloseable) cacheStore).close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("关闭 CacheStore 时出错", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -114,4 +114,9 @@ public class FilterImageCaptchaApplication implements ImageCaptchaApplication {
|
|||||||
public CacheStore getCacheStore() {
|
public CacheStore getCacheStore() {
|
||||||
return target.getCacheStore();
|
return target.getCacheStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
target.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
|||||||
* @Date 2020/5/29 8:33
|
* @Date 2020/5/29 8:33
|
||||||
* @Description 滑块验证码应用程序
|
* @Description 滑块验证码应用程序
|
||||||
*/
|
*/
|
||||||
public interface ImageCaptchaApplication {
|
public interface ImageCaptchaApplication extends AutoCloseable{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成滑块验证码
|
* 生成滑块验证码
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cloud.tianai.captcha.application;
|
|||||||
import cloud.tianai.captcha.cache.CacheStore;
|
import cloud.tianai.captcha.cache.CacheStore;
|
||||||
import cloud.tianai.captcha.cache.StoreCacheKeyPrefix;
|
import cloud.tianai.captcha.cache.StoreCacheKeyPrefix;
|
||||||
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
|
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
|
||||||
|
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||||
import cloud.tianai.captcha.generator.ImageTransform;
|
import cloud.tianai.captcha.generator.ImageTransform;
|
||||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||||
@@ -16,6 +17,9 @@ 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.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author: 天爱有情
|
* @Author: 天爱有情
|
||||||
* @date 2024/7/14 16:41
|
* @date 2024/7/14 16:41
|
||||||
@@ -30,22 +34,23 @@ public class TACBuilder {
|
|||||||
private ImageCaptchaProperties prop = new ImageCaptchaProperties();
|
private ImageCaptchaProperties prop = new ImageCaptchaProperties();
|
||||||
private ResourceStore resourceStore;
|
private ResourceStore resourceStore;
|
||||||
private ImageTransform imageTransform;
|
private ImageTransform imageTransform;
|
||||||
private StoreCacheKeyPrefix cacheKeyPrefix;
|
|
||||||
// private List<FontWrapper> fontWrappers = new ArrayList<>();
|
// private List<FontWrapper> fontWrappers = new ArrayList<>();
|
||||||
|
private Map<String, Resource> resourceCache;
|
||||||
|
private Map<String, ResourceMap> templateCache;
|
||||||
|
|
||||||
public static TACBuilder builder() {
|
public static TACBuilder builder() {
|
||||||
return TACBuilder.builder(new LocalMemoryResourceStore());
|
return TACBuilder.builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TACBuilder builder(ResourceStore resourceStore) {
|
|
||||||
TACBuilder builder = new TACBuilder(resourceStore);
|
|
||||||
builder.prop = new ImageCaptchaProperties();
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TACBuilder(ResourceStore resourceStore) {
|
private TACBuilder(ResourceStore resourceStore) {
|
||||||
this.resourceStore = resourceStore;
|
this.resourceStore = resourceStore;
|
||||||
}
|
}
|
||||||
|
public TACBuilder setResourceStore(ResourceStore resourceStore) {
|
||||||
|
this.resourceStore = resourceStore;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TACBuilder addDefaultTemplate(String defaultPathPrefix) {
|
public TACBuilder addDefaultTemplate(String defaultPathPrefix) {
|
||||||
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix);
|
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix);
|
||||||
@@ -77,10 +82,6 @@ public class TACBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TACBuilder setCacheKeyPrefix(StoreCacheKeyPrefix cacheKeyPrefix) {
|
|
||||||
this.cacheKeyPrefix = cacheKeyPrefix;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public TACBuilder addFont(Resource resource) {
|
public TACBuilder addFont(Resource resource) {
|
||||||
this.addResource(FontCache.FONT_TYPE, resource);
|
this.addResource(FontCache.FONT_TYPE, resource);
|
||||||
return this;
|
return this;
|
||||||
@@ -118,16 +119,18 @@ public class TACBuilder {
|
|||||||
|
|
||||||
|
|
||||||
public TACBuilder addResource(String captchaType, Resource imageResource) {
|
public TACBuilder addResource(String captchaType, Resource imageResource) {
|
||||||
if (resourceStore instanceof CrudResourceStore) {
|
// if (resourceStore instanceof CrudResourceStore) {
|
||||||
((CrudResourceStore) resourceStore).addResource(captchaType, imageResource);
|
// ((CrudResourceStore) resourceStore).addResource(captchaType, imageResource);
|
||||||
}
|
// }
|
||||||
|
cacheResource(captchaType, imageResource);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) {
|
public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) {
|
||||||
if (resourceStore instanceof CrudResourceStore) {
|
// if (resourceStore instanceof CrudResourceStore) {
|
||||||
((CrudResourceStore) resourceStore).addTemplate(captchaType, resourceMap);
|
// ((CrudResourceStore) resourceStore).addTemplate(captchaType, resourceMap);
|
||||||
}
|
// }
|
||||||
|
cacheTemplate(captchaType, resourceMap);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +143,20 @@ public class TACBuilder {
|
|||||||
if (cacheStore == null) {
|
if (cacheStore == null) {
|
||||||
cacheStore = new LocalCacheStore();
|
cacheStore = new LocalCacheStore();
|
||||||
}
|
}
|
||||||
|
if (resourceStore == null) {
|
||||||
|
resourceStore = new LocalMemoryResourceStore();
|
||||||
|
}
|
||||||
|
if (resourceStore instanceof CrudResourceStore) {
|
||||||
|
CrudResourceStore crudResourceStore = (CrudResourceStore) resourceStore;
|
||||||
|
if (!CollectionUtils.isEmpty(resourceCache)) {
|
||||||
|
resourceCache.forEach(crudResourceStore::addResource);
|
||||||
|
resourceCache = null;
|
||||||
|
}
|
||||||
|
if (!CollectionUtils.isEmpty(templateCache)) {
|
||||||
|
templateCache.forEach(crudResourceStore::addTemplate);
|
||||||
|
templateCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
@@ -155,7 +172,20 @@ public class TACBuilder {
|
|||||||
interceptor = EmptyCaptchaInterceptor.INSTANCE;
|
interceptor = EmptyCaptchaInterceptor.INSTANCE;
|
||||||
}
|
}
|
||||||
// 增加前缀处理接口
|
// 增加前缀处理接口
|
||||||
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor, cacheKeyPrefix);
|
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor);
|
||||||
return application;
|
return application;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cacheResource(String captchaType, Resource imageResource) {
|
||||||
|
if (resourceCache == null) {
|
||||||
|
resourceCache = new LinkedHashMap<>(8);
|
||||||
|
}
|
||||||
|
resourceCache.put(captchaType, imageResource);
|
||||||
|
}
|
||||||
|
private void cacheTemplate(String captchaType, ResourceMap resourceMap) {
|
||||||
|
if (templateCache == null) {
|
||||||
|
templateCache = new LinkedHashMap<>(8);
|
||||||
|
}
|
||||||
|
templateCache.put(captchaType, resourceMap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* @date 2022/3/2 14:35
|
* @date 2022/3/2 14:35
|
||||||
* @Description 提取出用于缓存的接口
|
* @Description 提取出用于缓存的接口
|
||||||
*/
|
*/
|
||||||
public interface CacheStore {
|
public interface CacheStore extends AutoCloseable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取缓存数据通过key
|
* 读取缓存数据通过key
|
||||||
|
|||||||
+22
-1
@@ -12,7 +12,6 @@ import java.util.stream.Collectors;
|
|||||||
/**
|
/**
|
||||||
* @Author: 天爱有情
|
* @Author: 天爱有情
|
||||||
* @date 2020/10/12 10:02
|
* @date 2020/10/12 10:02
|
||||||
* @Description 给予本人以前写的 expiring-map(redis淘汰策略的java实现) 项目进行改造
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@@ -198,6 +197,28 @@ public class ConCurrentExpiringMap<K, V> implements ExpiringMap<K, V> {
|
|||||||
throw new IllegalArgumentException("timemap not impl entrySet.");
|
throw new IllegalArgumentException("timemap not impl entrySet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁方法,关闭定时任务线程池
|
||||||
|
* 应该在不再使用此 Map 时调用,以防止线程泄露
|
||||||
|
*
|
||||||
|
* @since 0.0.3
|
||||||
|
*/
|
||||||
|
public void destroy() {
|
||||||
|
try {
|
||||||
|
scheduledExecutor.shutdown();
|
||||||
|
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
log.warn("ConCurrentExpiringMap 定时任务线程池未能正常关闭");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.warn("ConCurrentExpiringMap 定时任务线程池关闭时被中断", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时执行任务
|
* 定时执行任务
|
||||||
*
|
*
|
||||||
|
|||||||
+11
@@ -63,4 +63,15 @@ public class LocalCacheStore implements CacheStore {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭缓存存储,释放资源
|
||||||
|
* 建议在不再使用时调用,或在 Spring Bean 销毁时自动调用
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (cache instanceof ConCurrentExpiringMap) {
|
||||||
|
((ConCurrentExpiringMap<?, ?>) cache).destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-14
@@ -5,28 +5,34 @@ import lombok.Data;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class FontWrapper {
|
public class FontWrapper {
|
||||||
private Font font;
|
// private Font font;
|
||||||
private Float currentFontTopCoef;
|
// private Float currentFontTopCoef;
|
||||||
|
|
||||||
|
private Map<Float, Font> fontCache = new ConcurrentHashMap<>(4);
|
||||||
|
private Font baseFont;
|
||||||
|
public static final int DEFAULT_FONT_SIZE = 70;
|
||||||
|
|
||||||
|
|
||||||
|
public Font getFont() {
|
||||||
|
return getFont(DEFAULT_FONT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Font getFont(float size) {
|
||||||
|
return fontCache.computeIfAbsent(size, k -> baseFont.deriveFont(Font.BOLD, size));
|
||||||
|
}
|
||||||
|
|
||||||
public FontWrapper(Font font) {
|
public FontWrapper(Font font) {
|
||||||
this(font, 70);
|
this.baseFont = font;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FontWrapper(Font font, int fontSize) {
|
public float getFontTopCoef(Font font) {
|
||||||
this.font = font;
|
return 0.14645833f * font.getSize() + 0.39583333f;
|
||||||
this.font = font.deriveFont(Font.BOLD, fontSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getCurrentFontTopCoef() {
|
|
||||||
if (currentFontTopCoef != null) {
|
|
||||||
return currentFontTopCoef;
|
|
||||||
}
|
|
||||||
currentFontTopCoef = 0.14645833f * font.getSize() + 0.39583333f;
|
|
||||||
return currentFontTopCoef;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -65,6 +65,9 @@ public class GenerateParam {
|
|||||||
}
|
}
|
||||||
return param.remove(key);
|
return param.remove(key);
|
||||||
}
|
}
|
||||||
|
public <T> Object removeParam(ParamKey<T> paramKey) {
|
||||||
|
return removeParam(paramKey.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
public Object getOrDefault(String key, Object defaultValue) {
|
public Object getOrDefault(String key, Object defaultValue) {
|
||||||
if (param == null) {
|
if (param == null) {
|
||||||
|
|||||||
+4
@@ -15,6 +15,10 @@ public class ParamKeyEnum<T> implements ParamKey<T> {
|
|||||||
public static final ParamKey<Integer> CLICK_INTERFERENCE_COUNT = new ParamKeyEnum<>("interferenceCount");
|
public static final ParamKey<Integer> CLICK_INTERFERENCE_COUNT = new ParamKeyEnum<>("interferenceCount");
|
||||||
/** 读取字体时,可指定字体TAG,可用于给不同的验证码指定不同的字体包.*/
|
/** 读取字体时,可指定字体TAG,可用于给不同的验证码指定不同的字体包.*/
|
||||||
public static final ParamKey<String> FONT_TAG = new ParamKeyEnum<>("fontTag");
|
public static final ParamKey<String> FONT_TAG = new ParamKeyEnum<>("fontTag");
|
||||||
|
|
||||||
|
/** 验证码ID,内部使用.*/
|
||||||
|
public static final ParamKey<String> ID = new ParamKeyEnum<>("_id");
|
||||||
|
|
||||||
private String key;
|
private String key;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-1
@@ -4,6 +4,7 @@ import lombok.SneakyThrows;
|
|||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
import java.awt.geom.Area;
|
import java.awt.geom.Area;
|
||||||
import java.awt.geom.CubicCurve2D;
|
import java.awt.geom.CubicCurve2D;
|
||||||
import java.awt.geom.QuadCurve2D;
|
import java.awt.geom.QuadCurve2D;
|
||||||
@@ -387,6 +388,7 @@ public class CaptchaImageUtils {
|
|||||||
int interferencePointNum) {
|
int interferencePointNum) {
|
||||||
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
Graphics2D g = bufferedImage.createGraphics();
|
Graphics2D g = bufferedImage.createGraphics();
|
||||||
|
try {
|
||||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||||
g.setFont(font);
|
g.setFont(font);
|
||||||
char[] chars = data.toCharArray();
|
char[] chars = data.toCharArray();
|
||||||
@@ -405,6 +407,10 @@ public class CaptchaImageUtils {
|
|||||||
drawBesselLine(interferenceLineNum, null, g, width, height, random);
|
drawBesselLine(interferenceLineNum, null, g, width, height, random);
|
||||||
}
|
}
|
||||||
return bufferedImage;
|
return bufferedImage;
|
||||||
|
} finally {
|
||||||
|
// fixme #IDIG16
|
||||||
|
g.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -520,5 +526,81 @@ public class CaptchaImageUtils {
|
|||||||
return TYPE_PNG.equalsIgnoreCase(type);
|
return TYPE_PNG.equalsIgnoreCase(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* 绘制水印
|
||||||
|
* @param bgImage 背景图片
|
||||||
|
* @param watermark 水印文字
|
||||||
|
* @param x x坐标起始位置
|
||||||
|
* @param y y坐标起始位置
|
||||||
|
* @param color 水印颜色
|
||||||
|
* @param font 水印字体
|
||||||
|
*/
|
||||||
|
public static void drawWatermark(BufferedImage bgImage, String watermark, int x, int y, Color color, Font font) {
|
||||||
|
// 参数校验
|
||||||
|
if (bgImage == null || watermark == null || watermark.isEmpty() || font == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Graphics2D对象
|
||||||
|
Graphics2D g2d = bgImage.createGraphics();
|
||||||
|
try {
|
||||||
|
// 设置渲染质量
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
g2d.setColor(color);
|
||||||
|
|
||||||
|
// 设置字体
|
||||||
|
g2d.setFont(font);
|
||||||
|
|
||||||
|
// 计算文字尺寸
|
||||||
|
FontMetrics fontMetrics = g2d.getFontMetrics();
|
||||||
|
int textWidth = fontMetrics.stringWidth(watermark);
|
||||||
|
int textHeight = fontMetrics.getHeight();
|
||||||
|
|
||||||
|
// 计算旋转角度(45度角倾斜)
|
||||||
|
double angle = Math.toRadians(45);
|
||||||
|
|
||||||
|
// 计算水印间距,确保铺满整个图片
|
||||||
|
int spacingX = textWidth + 50;
|
||||||
|
int spacingY = textHeight + 50;
|
||||||
|
|
||||||
|
// 获取图片尺寸
|
||||||
|
int imageWidth = bgImage.getWidth();
|
||||||
|
int imageHeight = bgImage.getHeight();
|
||||||
|
|
||||||
|
// 计算需要绘制的水印数量
|
||||||
|
int rows = (int) Math.ceil((double) imageHeight / spacingY) + 2;
|
||||||
|
int cols = (int) Math.ceil((double) imageWidth / spacingX) + 2;
|
||||||
|
|
||||||
|
// 绘制水印,铺满整个图片
|
||||||
|
for (int row = 0; row < rows; row++) {
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
// 计算当前水印位置
|
||||||
|
int currentX = col * spacingX + x;
|
||||||
|
int currentY = row * spacingY + y;
|
||||||
|
|
||||||
|
// 保存当前坐标系
|
||||||
|
AffineTransform originalTransform = g2d.getTransform();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 平移到当前位置
|
||||||
|
g2d.translate(currentX, currentY);
|
||||||
|
|
||||||
|
// 旋转文字
|
||||||
|
g2d.rotate(angle);
|
||||||
|
|
||||||
|
// 绘制文字
|
||||||
|
g2d.drawString(watermark, 0, fontMetrics.getAscent());
|
||||||
|
} finally {
|
||||||
|
// 恢复原始坐标系
|
||||||
|
g2d.setTransform(originalTransform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 释放资源
|
||||||
|
g2d.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+13
-1
@@ -34,10 +34,22 @@ public class ImgWriter {
|
|||||||
if (ObjectUtils.isEmpty(imageType)) {
|
if (ObjectUtils.isEmpty(imageType)) {
|
||||||
imageType = CaptchaImageUtils.TYPE_JPG;
|
imageType = CaptchaImageUtils.TYPE_JPG;
|
||||||
}
|
}
|
||||||
ImageOutputStream imageOutputStream = transformImageOutputStream(destImageStream);
|
ImageOutputStream imageOutputStream = null;
|
||||||
|
try {
|
||||||
|
imageOutputStream = transformImageOutputStream(destImageStream);
|
||||||
final BufferedImage bufferedImage = CaptchaImageUtils.toBufferedImage(image, imageType);
|
final BufferedImage bufferedImage = CaptchaImageUtils.toBufferedImage(image, imageType);
|
||||||
final ImageWriter writer = getWriter(bufferedImage, imageType);
|
final ImageWriter writer = getWriter(bufferedImage, imageType);
|
||||||
return write(bufferedImage, writer, imageOutputStream, quality);
|
return write(bufferedImage, writer, imageOutputStream, quality);
|
||||||
|
} finally {
|
||||||
|
// 关闭 ImageOutputStream 防止资源泄露
|
||||||
|
if (imageOutputStream != null) {
|
||||||
|
try {
|
||||||
|
imageOutputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 忽略关闭异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+24
@@ -212,4 +212,28 @@ public class CacheImageCaptchaGenerator implements ImageCaptchaGenerator {
|
|||||||
target.setInterceptor(interceptor);
|
target.setInterceptor(interceptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁方法,关闭定时任务线程池并清理缓存
|
||||||
|
* 应该在不再使用此 Generator 时调用,以防止线程和内存泄露
|
||||||
|
*/
|
||||||
|
public void destroy() {
|
||||||
|
try {
|
||||||
|
scheduledExecutor.shutdown();
|
||||||
|
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
log.warn("CacheImageCaptchaGenerator 定时任务线程池未能正常关闭");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.warn("CacheImageCaptchaGenerator 定时任务线程池关闭时被中断", e);
|
||||||
|
}
|
||||||
|
// 清理缓存
|
||||||
|
queueMap.clear();
|
||||||
|
posMap.clear();
|
||||||
|
lastUpdateMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -113,11 +113,15 @@ public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGen
|
|||||||
int type = fixedImage.getColorModel().getTransparency();
|
int type = fixedImage.getColorModel().getTransparency();
|
||||||
BufferedImage image = new BufferedImage(width, height, type);
|
BufferedImage image = new BufferedImage(width, height, type);
|
||||||
Graphics2D graphics = image.createGraphics();
|
Graphics2D graphics = image.createGraphics();
|
||||||
|
try {
|
||||||
// 透明度
|
// 透明度
|
||||||
double alpha = ThreadLocalRandom.current().nextDouble(0.5, 0.8);
|
double alpha = ThreadLocalRandom.current().nextDouble(0.5, 0.8);
|
||||||
AlphaComposite alphaComposite = AlphaComposite.Src.derive((float) alpha);
|
AlphaComposite alphaComposite = AlphaComposite.Src.derive((float) alpha);
|
||||||
graphics.setComposite(alphaComposite);
|
graphics.setComposite(alphaComposite);
|
||||||
graphics.drawImage(fixedImage, 0, 0, width, height, null);
|
graphics.drawImage(fixedImage, 0, 0, width, height, null);
|
||||||
|
} finally {
|
||||||
|
graphics.dispose();
|
||||||
|
}
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-26
@@ -35,11 +35,7 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
|
|||||||
// @Getter
|
// @Getter
|
||||||
// @Setter
|
// @Setter
|
||||||
// protected List<FontWrapper> fonts = new ArrayList<>();
|
// protected List<FontWrapper> fonts = new ArrayList<>();
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
protected Integer clickImgWidth = 100;
|
protected Integer clickImgWidth = 100;
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
protected Integer clickImgHeight = 100;
|
protected Integer clickImgHeight = 100;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -95,6 +91,33 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
|
|||||||
return tipList;
|
return tipList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClickImageCheckDefinition.ImgWrapper getClickImg(GenerateParam param, Resource tip, Color randomColor) {
|
||||||
|
if (randomColor == null) {
|
||||||
|
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||||
|
randomColor = CaptchaImageUtils.getRandomColor(random);
|
||||||
|
}
|
||||||
|
// 随机角度
|
||||||
|
int randomDeg = randomInt(0, 85);
|
||||||
|
// 缩放
|
||||||
|
double factor = 600d;
|
||||||
|
|
||||||
|
FontWrapper fontWrapper = randomFont(param);
|
||||||
|
Font font = fontWrapper.getFont((float) (FontWrapper.DEFAULT_FONT_SIZE * factor));
|
||||||
|
float currentFontTopCoef = fontWrapper.getFontTopCoef(font);
|
||||||
|
// 图片点击宽度
|
||||||
|
int clickImgWidth = (int) (font.getSize() * 1.428571428571429);
|
||||||
|
|
||||||
|
BufferedImage fontImage = CaptchaImageUtils.drawWordImg(randomColor,
|
||||||
|
tip.getData(),
|
||||||
|
font,
|
||||||
|
currentFontTopCoef,
|
||||||
|
clickImgWidth,
|
||||||
|
clickImgWidth,
|
||||||
|
randomDeg);
|
||||||
|
return new ClickImageCheckDefinition.ImgWrapper(fontImage, tip, randomColor);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doInit() {
|
protected void doInit() {
|
||||||
// if (CollectionUtils.isEmpty(fonts)) {
|
// if (CollectionUtils.isEmpty(fonts)) {
|
||||||
@@ -115,10 +138,12 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
|
|||||||
throw new ImageCaptchaException("随机获取字体失败, resource中没有读到字体包, resource=" + resource);
|
throw new ImageCaptchaException("随机获取字体失败, resource中没有读到字体包, resource=" + resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public ClickImageCheckDefinition.ImgWrapper genTipImage(List<ClickImageCheckDefinition> imageCheckDefinitions, GenerateParam param) {
|
public ClickImageCheckDefinition.ImgWrapper genTipImage(List<ClickImageCheckDefinition> imageCheckDefinitions, GenerateParam param) {
|
||||||
FontWrapper fontWrapper = randomFont(param);
|
FontWrapper fontWrapper = randomFont(param);
|
||||||
Font font = fontWrapper.getFont();
|
Font font = fontWrapper.getFont();
|
||||||
float currentFontTopCoef = fontWrapper.getCurrentFontTopCoef();
|
float currentFontTopCoef = fontWrapper.getFontTopCoef(font);
|
||||||
String tips = imageCheckDefinitions.stream().map(c -> c.getTip().getData()).collect(Collectors.joining());
|
String tips = imageCheckDefinitions.stream().map(c -> c.getTip().getData()).collect(Collectors.joining());
|
||||||
// 生成随机颜色
|
// 生成随机颜色
|
||||||
int fontWidth = tips.length() * font.getSize();
|
int fontWidth = tips.length() * font.getSize();
|
||||||
@@ -140,27 +165,6 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClickImageCheckDefinition.ImgWrapper getClickImg(GenerateParam param, Resource tip, Color randomColor) {
|
|
||||||
if (randomColor == null) {
|
|
||||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
|
||||||
randomColor = CaptchaImageUtils.getRandomColor(random);
|
|
||||||
}
|
|
||||||
// 随机角度
|
|
||||||
int randomDeg = randomInt(0, 85);
|
|
||||||
FontWrapper fontWrapper = randomFont(param);
|
|
||||||
Font font = fontWrapper.getFont();
|
|
||||||
float currentFontTopCoef = fontWrapper.getCurrentFontTopCoef();
|
|
||||||
BufferedImage fontImage = CaptchaImageUtils.drawWordImg(randomColor,
|
|
||||||
tip.getData(),
|
|
||||||
font,
|
|
||||||
currentFontTopCoef,
|
|
||||||
clickImgWidth,
|
|
||||||
clickImgHeight,
|
|
||||||
randomDeg);
|
|
||||||
return new ClickImageCheckDefinition.ImgWrapper(fontImage, tip, randomColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<ClickImageCheckDefinition> filterAndSortClickImageCheckDefinition(CaptchaExchange captchaExchange, List<ClickImageCheckDefinition> allCheckDefinitionList) {
|
protected List<ClickImageCheckDefinition> filterAndSortClickImageCheckDefinition(CaptchaExchange captchaExchange, List<ClickImageCheckDefinition> allCheckDefinitionList) {
|
||||||
GenerateParam param = captchaExchange.getParam();
|
GenerateParam param = captchaExchange.getParam();
|
||||||
|
|||||||
+8
@@ -29,6 +29,7 @@ public class Base64ImageTransform implements ImageTransform {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
if (CaptchaImageUtils.isPng(transformType) || CaptchaImageUtils.isJpeg(transformType)) {
|
if (CaptchaImageUtils.isPng(transformType) || CaptchaImageUtils.isJpeg(transformType)) {
|
||||||
// 如果是 jpg 或者 png图片的话 用hutool的生成
|
// 如果是 jpg 或者 png图片的话 用hutool的生成
|
||||||
ImgWriter.write(bufferedImage, transformType, byteArrayOutputStream, -1);
|
ImgWriter.write(bufferedImage, transformType, byteArrayOutputStream, -1);
|
||||||
@@ -39,6 +40,13 @@ public class Base64ImageTransform implements ImageTransform {
|
|||||||
byte[] data = byteArrayOutputStream.toByteArray();
|
byte[] data = byteArrayOutputStream.toByteArray();
|
||||||
String base64 = Base64.getEncoder().encodeToString(data);
|
String base64 = Base64.getEncoder().encodeToString(data);
|
||||||
return "data:image/" + transformType + ";base64,".concat(base64);
|
return "data:image/" + transformType + ";base64,".concat(base64);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
byteArrayOutputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 忽略关闭异常
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String beforeTransform(BufferedImage bufferedImage, String formatType) {
|
public String beforeTransform(BufferedImage bufferedImage, String formatType) {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ 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 cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@@ -24,15 +22,14 @@ public class FontCache implements ResourceStore {
|
|||||||
|
|
||||||
|
|
||||||
public static final String FONT_TYPE = "font";
|
public static final String FONT_TYPE = "font";
|
||||||
|
/**
|
||||||
|
* 字体缓存最大数量,防止内存泄露
|
||||||
|
*/
|
||||||
|
private static final int MAX_FONT_CACHE_SIZE = 100;
|
||||||
private final Map<String, FontWrapper> fontMap = new ConcurrentHashMap<>();
|
private final Map<String, FontWrapper> fontMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private ResourceStore resourceStore;
|
private ResourceStore resourceStore;
|
||||||
private ImageCaptchaResourceManager resourceManager;
|
private ImageCaptchaResourceManager resourceManager;
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
private int fontSize = 70;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public FontCache(ResourceStore resourceStore) {
|
public FontCache(ResourceStore resourceStore) {
|
||||||
this.resourceStore = resourceStore;
|
this.resourceStore = resourceStore;
|
||||||
@@ -48,7 +45,7 @@ public class FontCache implements ResourceStore {
|
|||||||
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);
|
||||||
return new FontWrapper(font, fontSize);
|
return new FontWrapper(font);
|
||||||
} catch (FontFormatException | IOException e) {
|
} catch (FontFormatException | IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
@@ -60,11 +57,25 @@ public class FontCache implements ResourceStore {
|
|||||||
return resource.getType() + "_" + resource.getData();
|
return resource.getType() + "_" + resource.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存大小,如果超过限制则清理
|
||||||
|
*/
|
||||||
|
private void checkCacheSize() {
|
||||||
|
if (fontMap.size() > MAX_FONT_CACHE_SIZE) {
|
||||||
|
log.warn("字体缓存超过限制大小: {},执行清理", fontMap.size());
|
||||||
|
// 简单清理策略:清空缓存
|
||||||
|
// 如果需要更精细的 LRU 策略,可以使用 LinkedHashMap 或第三方缓存库
|
||||||
|
fontMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
|
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
|
||||||
List<Resource> resources = resourceStore.randomGetResourceByTypeAndTag(type, tag, quantity);
|
List<Resource> resources = resourceStore.randomGetResourceByTypeAndTag(type, tag, quantity);
|
||||||
// 字体增强
|
// 字体增强
|
||||||
if (FONT_TYPE.equalsIgnoreCase(type)) {
|
if (FONT_TYPE.equalsIgnoreCase(type)) {
|
||||||
|
// 在添加新字体前检查缓存大小
|
||||||
|
checkCacheSize();
|
||||||
for (Resource resource : resources) {
|
for (Resource resource : resources) {
|
||||||
FontWrapper fontWrapper = fontMap.computeIfAbsent(calcId(resource), v -> getFont(resource));
|
FontWrapper fontWrapper = fontMap.computeIfAbsent(calcId(resource), v -> getFont(resource));
|
||||||
resource.setExtra(fontWrapper);
|
resource.setExtra(fontWrapper);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class ApplicationTest {
|
|||||||
|
|
||||||
ImageCaptchaProperties prop = new ImageCaptchaProperties();
|
ImageCaptchaProperties prop = new ImageCaptchaProperties();
|
||||||
// application 验证码封装, prop为所需的一些扩展参数
|
// application 验证码封装, prop为所需的一些扩展参数
|
||||||
ImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, imageCaptchaValidator, cacheStore, prop, group, null);
|
ImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, imageCaptchaValidator, cacheStore, prop, group);
|
||||||
return application;
|
return application;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user