重构系统项目结构, 将 tianai-captcha
tianai-captcha-springboot-starter tianai-captcha-web-sdk tianai-captcha-solon-plugin 整合到一块
@@ -0,0 +1,27 @@
|
||||
package cloud.tianai.captcha.application;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/24 16:01
|
||||
* @Description 验证码图片类型
|
||||
*/
|
||||
@Getter
|
||||
public enum CaptchaImageType {
|
||||
|
||||
/** webp类型. */
|
||||
WEBP,
|
||||
/** jpg+png类型. */
|
||||
JPEG_PNG;
|
||||
|
||||
public static CaptchaImageType getType(String bgImageType, String sliderImageType) {
|
||||
if ("webp".equalsIgnoreCase(bgImageType) && "webp".equalsIgnoreCase(sliderImageType)) {
|
||||
return WEBP;
|
||||
}
|
||||
if (("jpeg".equalsIgnoreCase(bgImageType) || "jpg".equalsIgnoreCase(bgImageType)) && "png".equalsIgnoreCase(sliderImageType)) {
|
||||
return JPEG_PNG;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package cloud.tianai.captcha.application;
|
||||
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.cache.CacheStore;
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.common.response.ApiResponseStatusConstant;
|
||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @Date 2020/5/29 8:52
|
||||
* @Description 默认的 图片验证码应用程序
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
|
||||
private CaptchaInterceptor captchaInterceptor;
|
||||
/** 图片验证码生成器. */
|
||||
private ImageCaptchaGenerator captchaGenerator;
|
||||
/** 图片验证码校验器. */
|
||||
private ImageCaptchaValidator imageCaptchaValidator;
|
||||
/** 缓冲存储. */
|
||||
private CacheStore cacheStore;
|
||||
/** 验证码配置属性. */
|
||||
private final ImageCaptchaProperties prop;
|
||||
/** 默认的过期时间. */
|
||||
private long defaultExpire = 20000L;
|
||||
|
||||
public static final String ID_SPLIT = "_";
|
||||
|
||||
public DefaultImageCaptchaApplication(ImageCaptchaGenerator captchaGenerator,
|
||||
ImageCaptchaValidator imageCaptchaValidator,
|
||||
CacheStore cacheStore,
|
||||
ImageCaptchaProperties prop,
|
||||
CaptchaInterceptor captchaInterceptor) {
|
||||
this.prop = prop;
|
||||
|
||||
setImageCaptchaValidator(imageCaptchaValidator);
|
||||
setCacheStore(cacheStore);
|
||||
// 默认过期时间
|
||||
Long defaultExpire = prop.getExpire().get("default");
|
||||
if (defaultExpire != null && defaultExpire > 0) {
|
||||
this.defaultExpire = defaultExpire;
|
||||
}
|
||||
if (captchaInterceptor == null) {
|
||||
this.captchaInterceptor = EmptyCaptchaInterceptor.INSTANCE;
|
||||
} else {
|
||||
this.captchaInterceptor = captchaInterceptor;
|
||||
}
|
||||
captchaGenerator.setInterceptor(this.captchaInterceptor);
|
||||
if (prop.isLocalCacheEnabled()) {
|
||||
captchaGenerator = new CacheImageCaptchaGenerator(captchaGenerator,
|
||||
prop.getLocalCacheSize(), prop.getLocalCacheWaitTime(),
|
||||
prop.getLocalCachePeriod(), prop.getLocalCacheExpireTime());
|
||||
}
|
||||
// 初始化生成器
|
||||
captchaGenerator.init();
|
||||
setImageCaptchaGenerator(captchaGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha() {
|
||||
// 生成滑块验证码
|
||||
return generateCaptcha(CaptchaTypeConstant.SLIDER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
|
||||
GenerateParam generateParam = new GenerateParam();
|
||||
generateParam.setType(type);
|
||||
return generateCaptcha(generateParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
|
||||
ApiResponse<ImageCaptchaVO> captchaResponse = beforeGenerateCaptcha(param);
|
||||
if (captchaResponse != null) {
|
||||
return captchaResponse;
|
||||
}
|
||||
ImageCaptchaInfo imageCaptchaInfo = getImageCaptchaGenerator().generateCaptchaImage(param);
|
||||
captchaResponse = convertToCaptchaResponse(imageCaptchaInfo);
|
||||
afterGenerateCaptcha(imageCaptchaInfo, captchaResponse);
|
||||
return captchaResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
|
||||
return generateCaptcha(CaptchaTypeConstant.SLIDER, captchaImageType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
|
||||
GenerateParam param = new GenerateParam();
|
||||
if (CaptchaImageType.WEBP.equals(captchaImageType)) {
|
||||
param.setBackgroundFormatName("webp");
|
||||
param.setTemplateFormatName("webp");
|
||||
} else {
|
||||
param.setBackgroundFormatName("jpeg");
|
||||
param.setTemplateFormatName("png");
|
||||
}
|
||||
param.setType(type);
|
||||
return generateCaptcha(param);
|
||||
}
|
||||
|
||||
|
||||
public ApiResponse<ImageCaptchaVO> convertToCaptchaResponse(ImageCaptchaInfo imageCaptchaInfo) {
|
||||
if (imageCaptchaInfo == null) {
|
||||
// 要是生成失败
|
||||
throw new ImageCaptchaException("生成验证码失败,验证码生成为空");
|
||||
}
|
||||
// 生成ID
|
||||
String id = generatorId(imageCaptchaInfo);
|
||||
ApiResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData(imageCaptchaInfo);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
// 生成校验数据
|
||||
AnyMap validData = getImageCaptchaValidator().generateImageCaptchaValidData(imageCaptchaInfo);
|
||||
afterGenerateImageCaptchaValidData(imageCaptchaInfo, validData);
|
||||
if (!CollectionUtils.isEmpty(validData)) {
|
||||
// 存到缓存里
|
||||
cacheVerification(id, imageCaptchaInfo.getType(), validData);
|
||||
}
|
||||
ImageCaptchaVO verificationVO = new ImageCaptchaVO();
|
||||
verificationVO.setType(imageCaptchaInfo.getType());
|
||||
verificationVO.setBackgroundImage(imageCaptchaInfo.getBackgroundImage());
|
||||
verificationVO.setTemplateImage(imageCaptchaInfo.getTemplateImage());
|
||||
verificationVO.setBackgroundImageTag(imageCaptchaInfo.getBackgroundImageTag());
|
||||
verificationVO.setTemplateImageTag(imageCaptchaInfo.getTemplateImageTag());
|
||||
verificationVO.setBackgroundImageWidth(imageCaptchaInfo.getBackgroundImageWidth());
|
||||
verificationVO.setBackgroundImageHeight(imageCaptchaInfo.getBackgroundImageHeight());
|
||||
verificationVO.setTemplateImageWidth(imageCaptchaInfo.getTemplateImageWidth());
|
||||
verificationVO.setTemplateImageHeight(imageCaptchaInfo.getTemplateImageHeight());
|
||||
verificationVO.setData(imageCaptchaInfo.getData() == null ? null : imageCaptchaInfo.getData().getViewData());
|
||||
verificationVO.setId(id);
|
||||
return ApiResponse.ofSuccess(verificationVO);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> matching(String id, MatchParam matchParam) {
|
||||
AnyMap validData = getVerification(id);
|
||||
if (validData == null) {
|
||||
return ApiResponse.ofMessage(ApiResponseStatusConstant.EXPIRED);
|
||||
}
|
||||
ApiResponse<?> response = beforeValid(id, matchParam, validData);
|
||||
if (!response.isSuccess()) {
|
||||
return response;
|
||||
}
|
||||
ApiResponse<?> basicValid = getImageCaptchaValidator().valid(matchParam.getTrack(), validData);
|
||||
response = afterValid(id, matchParam, validData, basicValid);
|
||||
if (!response.isSuccess()) {
|
||||
return response;
|
||||
}
|
||||
return basicValid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> matching(String id, ImageCaptchaTrack track) {
|
||||
return matching(id, new MatchParam(track, null));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean matching(String id, Float percentage) {
|
||||
AnyMap cachePercentage = getVerification(id);
|
||||
if (cachePercentage == null) {
|
||||
return false;
|
||||
}
|
||||
ImageCaptchaValidator imageCaptchaValidator = getImageCaptchaValidator();
|
||||
if (!(imageCaptchaValidator instanceof SimpleImageCaptchaValidator)) {
|
||||
return false;
|
||||
}
|
||||
SimpleImageCaptchaValidator simpleImageCaptchaValidator = (SimpleImageCaptchaValidator) imageCaptchaValidator;
|
||||
Float oriPercentage = cachePercentage.getFloat(SimpleImageCaptchaValidator.PERCENTAGE_KEY);
|
||||
// 读容错值
|
||||
Float tolerant = cachePercentage.getFloat(SimpleImageCaptchaValidator.TOLERANT_KEY, simpleImageCaptchaValidator.getDefaultTolerant());
|
||||
return simpleImageCaptchaValidator.checkPercentage(percentage, oriPercentage, tolerant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCaptchaTypeById(String id) {
|
||||
String[] split = id.split(ID_SPLIT);
|
||||
if (split.length >= 2) {
|
||||
return split[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String generatorId(ImageCaptchaInfo imageCaptchaInfo) {
|
||||
return imageCaptchaInfo.getType() + ID_SPLIT + UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过缓存获取百分比
|
||||
*
|
||||
* @param id 验证码ID
|
||||
* @return AnyMap
|
||||
*/
|
||||
protected AnyMap getVerification(String id) {
|
||||
return getCacheStore().getAndRemoveCache(getKey(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存验证码
|
||||
*
|
||||
* @param id id
|
||||
* @param type
|
||||
* @param validData validData
|
||||
*/
|
||||
protected void cacheVerification(String id, String type, AnyMap validData) {
|
||||
Long expire = prop.getExpire().getOrDefault(type, defaultExpire);
|
||||
if (!getCacheStore().setCache(getKey(id), validData, expire, TimeUnit.MILLISECONDS)) {
|
||||
log.error("缓存验证码数据失败, id={}, validData={}", id, validData);
|
||||
throw new ImageCaptchaException("缓存验证码数据失败" + type);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getKey(String id) {
|
||||
return prop.getPrefix().concat(":").concat(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageCaptchaResourceManager() {
|
||||
return getImageCaptchaGenerator().getImageResourceManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageCaptchaValidator(ImageCaptchaValidator imageCaptchaValidator) {
|
||||
this.imageCaptchaValidator = imageCaptchaValidator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageCaptchaGenerator(ImageCaptchaGenerator imageCaptchaGenerator) {
|
||||
this.captchaGenerator = imageCaptchaGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaptchaInterceptor getCaptchaInterceptor() {
|
||||
return this.captchaInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCaptchaInterceptor(CaptchaInterceptor captchaInterceptor) {
|
||||
this.captchaInterceptor = captchaInterceptor;
|
||||
this.captchaGenerator.setInterceptor(captchaInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCacheStore(CacheStore cacheStore) {
|
||||
this.cacheStore = cacheStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaValidator getImageCaptchaValidator() {
|
||||
return this.imageCaptchaValidator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaGenerator getImageCaptchaGenerator() {
|
||||
return this.captchaGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheStore getCacheStore() {
|
||||
return this.cacheStore;
|
||||
}
|
||||
|
||||
// ============== 一些模板方法 ================
|
||||
|
||||
private void afterGenerateCaptcha(ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
|
||||
captchaInterceptor.afterGenerateCaptcha(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, captchaResponse);
|
||||
}
|
||||
|
||||
private ApiResponse<ImageCaptchaVO> beforeGenerateCaptcha(GenerateParam param) {
|
||||
return captchaInterceptor.beforeGenerateCaptcha(captchaInterceptor.createContext(), param.getType(), param);
|
||||
}
|
||||
|
||||
private ApiResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) {
|
||||
return captchaInterceptor.beforeGenerateImageCaptchaValidData(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo);
|
||||
}
|
||||
|
||||
private void afterGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo, AnyMap validData) {
|
||||
captchaInterceptor.afterGenerateImageCaptchaValidData(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, validData);
|
||||
}
|
||||
|
||||
private ApiResponse<?> beforeValid(String id, MatchParam matchParam, AnyMap validData) {
|
||||
return captchaInterceptor.beforeValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData);
|
||||
}
|
||||
|
||||
private ApiResponse<?> afterValid(String id, MatchParam matchParam, AnyMap validData, ApiResponse<?> basicValid) {
|
||||
return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData, basicValid);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package cloud.tianai.captcha.application;
|
||||
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.cache.CacheStore;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/3/2 14:22
|
||||
* @Description 用于SliderCaptchaApplication增加附属功能
|
||||
*/
|
||||
public class FilterImageCaptchaApplication implements ImageCaptchaApplication {
|
||||
|
||||
|
||||
protected ImageCaptchaApplication target;
|
||||
|
||||
public FilterImageCaptchaApplication(ImageCaptchaApplication target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha() {
|
||||
return target.generateCaptcha();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type) {
|
||||
return target.generateCaptcha(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType) {
|
||||
return target.generateCaptcha(captchaImageType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType) {
|
||||
return target.generateCaptcha(type, captchaImageType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param) {
|
||||
return target.generateCaptcha(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> matching(String id, MatchParam matchParam) {
|
||||
return target.matching(id, matchParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> matching(String id, ImageCaptchaTrack track) {
|
||||
return target.matching(id, track);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matching(String id, Float percentage) {
|
||||
return target.matching(id, percentage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCaptchaTypeById(String id) {
|
||||
return target.getCaptchaTypeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageCaptchaResourceManager() {
|
||||
return target.getImageCaptchaResourceManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageCaptchaValidator(ImageCaptchaValidator sliderCaptchaValidator) {
|
||||
target.setImageCaptchaValidator(sliderCaptchaValidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageCaptchaGenerator(ImageCaptchaGenerator imageCaptchaGenerator) {
|
||||
target.setImageCaptchaGenerator(imageCaptchaGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaptchaInterceptor getCaptchaInterceptor() {
|
||||
return target.getCaptchaInterceptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCaptchaInterceptor(CaptchaInterceptor captchaInterceptor) {
|
||||
target.setCaptchaInterceptor(captchaInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCacheStore(CacheStore cacheStore) {
|
||||
target.setCacheStore(cacheStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaValidator getImageCaptchaValidator() {
|
||||
return target.getImageCaptchaValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaGenerator getImageCaptchaGenerator() {
|
||||
return target.getImageCaptchaGenerator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheStore getCacheStore() {
|
||||
return target.getCacheStore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package cloud.tianai.captcha.application;
|
||||
|
||||
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.cache.CacheStore;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @Date 2020/5/29 8:33
|
||||
* @Description 滑块验证码应用程序
|
||||
*/
|
||||
public interface ImageCaptchaApplication {
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
ApiResponse<ImageCaptchaVO> generateCaptcha();
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @param type type类型
|
||||
* @return CaptchaResponse<SliderCaptchaVO>
|
||||
*/
|
||||
ApiResponse<ImageCaptchaVO> generateCaptcha(String type);
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @param captchaImageType 要生成webp还是jpg类型的图片
|
||||
* @return CaptchaResponse<SliderCaptchaVO>
|
||||
*/
|
||||
ApiResponse<ImageCaptchaVO> generateCaptcha(CaptchaImageType captchaImageType);
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @param type type
|
||||
* @param captchaImageType CaptchaImageType
|
||||
* @return CaptchaResponse<ImageCaptchaVO>
|
||||
*/
|
||||
ApiResponse<ImageCaptchaVO> generateCaptcha(String type, CaptchaImageType captchaImageType);
|
||||
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @param param param
|
||||
* @return CaptchaResponse<SliderCaptchaVO>
|
||||
*/
|
||||
ApiResponse<ImageCaptchaVO> generateCaptcha(GenerateParam param);
|
||||
|
||||
/**
|
||||
* 匹配
|
||||
*
|
||||
* @param id 验证码的ID
|
||||
* @param matchParam 匹配数据,包含鼠标轨迹,设备信息等
|
||||
* @return 匹配成功返回true, 否则返回false
|
||||
*/
|
||||
ApiResponse<?> matching(String id, MatchParam matchParam);
|
||||
|
||||
/**
|
||||
* 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, MatchParam)}
|
||||
*
|
||||
* @param id 验证码的ID
|
||||
* @param track 轨迹数据
|
||||
* @return 匹配成功返回true, 否则返回false
|
||||
*/
|
||||
ApiResponse<?> matching(String id, ImageCaptchaTrack track);
|
||||
|
||||
/**
|
||||
* 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, MatchParam)}
|
||||
*
|
||||
* @param id id
|
||||
* @param percentage 百分比数据
|
||||
* @return boolean
|
||||
*/
|
||||
@Deprecated
|
||||
boolean matching(String id, Float percentage);
|
||||
|
||||
/**
|
||||
* 查询该ID是属于哪个验证码类型
|
||||
*
|
||||
* @param id id
|
||||
* @return String
|
||||
*/
|
||||
String getCaptchaTypeById(String id);
|
||||
|
||||
/**
|
||||
* 获取验证码资源管理器
|
||||
*
|
||||
* @return SliderCaptchaResourceManager
|
||||
*/
|
||||
ImageCaptchaResourceManager getImageCaptchaResourceManager();
|
||||
|
||||
/**
|
||||
* 设置 SliderCaptchaValidator 验证码验证器
|
||||
*
|
||||
* @param imageCaptchaValidator imageCaptchaValidator
|
||||
*/
|
||||
void setImageCaptchaValidator(ImageCaptchaValidator imageCaptchaValidator);
|
||||
|
||||
/**
|
||||
* 设置 ImageCaptchaGenerator 验证码生成器
|
||||
*
|
||||
* @param imageCaptchaGenerator SliderCaptchaGenerator
|
||||
*/
|
||||
void setImageCaptchaGenerator(ImageCaptchaGenerator imageCaptchaGenerator);
|
||||
|
||||
/**
|
||||
* 获取拦截器
|
||||
*
|
||||
* @return CaptchaInterceptor
|
||||
*/
|
||||
CaptchaInterceptor getCaptchaInterceptor();
|
||||
|
||||
/**
|
||||
* 设置 拦截器
|
||||
*
|
||||
* @param captchaInterceptor captchaInterceptor
|
||||
*/
|
||||
void setCaptchaInterceptor(CaptchaInterceptor captchaInterceptor);
|
||||
|
||||
/**
|
||||
* 设置 缓存存储器
|
||||
*
|
||||
* @param cacheStore cacheStore
|
||||
*/
|
||||
void setCacheStore(CacheStore cacheStore);
|
||||
|
||||
/**
|
||||
* 获取验证码验证器
|
||||
*
|
||||
* @return SliderCaptchaValidator
|
||||
*/
|
||||
ImageCaptchaValidator getImageCaptchaValidator();
|
||||
|
||||
/**
|
||||
* 获取验证码生成器
|
||||
*
|
||||
* @return SliderCaptchaTemplate
|
||||
*/
|
||||
ImageCaptchaGenerator getImageCaptchaGenerator();
|
||||
|
||||
/**
|
||||
* 获取缓存存储器
|
||||
*
|
||||
* @return CacheStore
|
||||
*/
|
||||
CacheStore getCacheStore();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cloud.tianai.captcha.application;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2020/10/19 18:41
|
||||
* @Description 滑块验证码属性
|
||||
*/
|
||||
@Data
|
||||
public class ImageCaptchaProperties {
|
||||
/** 过期key prefix. */
|
||||
private String prefix = "captcha";
|
||||
/** 过期时间. */
|
||||
private Map<String, Long> expire = new HashMap<>();
|
||||
|
||||
// 本地提前缓存
|
||||
private boolean localCacheEnabled = false;
|
||||
private int localCacheSize = 10;
|
||||
private int localCacheWaitTime = 1000;
|
||||
private int localCachePeriod = 5000;
|
||||
private Long localCacheExpireTime;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package cloud.tianai.captcha.application;
|
||||
|
||||
import cloud.tianai.captcha.cache.CacheStore;
|
||||
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.*;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
|
||||
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
|
||||
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2024/7/14 16:41
|
||||
* @Description 一个构建ImageCaptchaApplication的工具, 免去一些繁琐的配置,方便新手用户一键使用
|
||||
*/
|
||||
public class TACBuilder {
|
||||
|
||||
private CacheStore cacheStore;
|
||||
private ImageCaptchaGenerator generator;
|
||||
private ImageCaptchaValidator validator;
|
||||
private CaptchaInterceptor interceptor = EmptyCaptchaInterceptor.INSTANCE;
|
||||
private ImageCaptchaProperties prop = new ImageCaptchaProperties();
|
||||
private ResourceStore resourceStore;
|
||||
private ImageTransform imageTransform;
|
||||
// private List<FontWrapper> fontWrappers = new ArrayList<>();
|
||||
|
||||
public static TACBuilder builder() {
|
||||
return TACBuilder.builder(new LocalMemoryResourceStore());
|
||||
}
|
||||
|
||||
public static TACBuilder builder(ResourceStore resourceStore) {
|
||||
TACBuilder builder = new TACBuilder(resourceStore);
|
||||
builder.prop = new ImageCaptchaProperties();
|
||||
return builder;
|
||||
}
|
||||
|
||||
private TACBuilder(ResourceStore resourceStore) {
|
||||
this.resourceStore = resourceStore;
|
||||
}
|
||||
|
||||
public TACBuilder addDefaultTemplate(String defaultPathPrefix) {
|
||||
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix);
|
||||
defaultBuiltInResources.addDefaultTemplate(resourceStore);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder addDefaultTemplate() {
|
||||
return addDefaultTemplate(DefaultBuiltInResources.PATH_PREFIX);
|
||||
}
|
||||
|
||||
public TACBuilder setCacheStore(CacheStore cacheStore) {
|
||||
this.cacheStore = cacheStore;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder setGenerator(ImageCaptchaGenerator generator) {
|
||||
this.generator = generator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder setValidator(ImageCaptchaValidator validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder setInterceptor(CaptchaInterceptor interceptor) {
|
||||
this.interceptor = interceptor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder addFont(Resource resource) {
|
||||
this.addResource(FontCache.FONT_TYPE, resource);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TACBuilder cached(int size, int waitTime, int period, Long expireTime) {
|
||||
prop.setLocalCacheEnabled(true);
|
||||
prop.setLocalCacheSize(size);
|
||||
prop.setLocalCacheWaitTime(waitTime);
|
||||
prop.setLocalCachePeriod(period);
|
||||
prop.setLocalCacheExpireTime(expireTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder prefix(String prefix) {
|
||||
this.prop.setPrefix(prefix);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder expire(String captchaType, Long expireTime) {
|
||||
prop.getExpire().put(captchaType, expireTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder setProp(ImageCaptchaProperties prop) {
|
||||
this.prop = prop;
|
||||
return this;
|
||||
}
|
||||
|
||||
// public TACBuilder setResourceStore(ResourceStore resourceStore) {
|
||||
// this.resourceStore = resourceStore;
|
||||
// return this;
|
||||
// }
|
||||
|
||||
|
||||
public TACBuilder addResource(String captchaType, Resource imageResource) {
|
||||
if (resourceStore instanceof CrudResourceStore) {
|
||||
((CrudResourceStore) resourceStore).addResource(captchaType, imageResource);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) {
|
||||
if (resourceStore instanceof CrudResourceStore) {
|
||||
((CrudResourceStore) resourceStore).addTemplate(captchaType, resourceMap);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TACBuilder setTransform(ImageTransform imageTransform) {
|
||||
this.imageTransform = imageTransform;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageCaptchaApplication build() {
|
||||
if (cacheStore == null) {
|
||||
cacheStore = new LocalCacheStore();
|
||||
}
|
||||
if (generator == null) {
|
||||
ResourceProviders resourceProviders = new ResourceProviders();
|
||||
DefaultImageCaptchaResourceManager resourceManager = new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders);
|
||||
generator = new MultiImageCaptchaGenerator(resourceManager, imageTransform);
|
||||
}
|
||||
// if (generator instanceof MultiImageCaptchaGenerator) {
|
||||
// ((MultiImageCaptchaGenerator) generator).setFontWrappers(fontWrappers);
|
||||
// }
|
||||
if (validator == null) {
|
||||
validator = new SimpleImageCaptchaValidator();
|
||||
}
|
||||
if (interceptor == null) {
|
||||
interceptor = EmptyCaptchaInterceptor.INSTANCE;
|
||||
}
|
||||
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor);
|
||||
return application;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cloud.tianai.captcha.application.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ImageCaptchaVO implements Serializable {
|
||||
|
||||
/** ID.*/
|
||||
private String id;
|
||||
/** 验证码类型.*/
|
||||
private String type;
|
||||
/** 背景图.*/
|
||||
private String backgroundImage;
|
||||
/** 移动图.*/
|
||||
private String templateImage;
|
||||
/** 背景图片所属标签. */
|
||||
private String backgroundImageTag;
|
||||
/** 模板图片所属标签. */
|
||||
private String templateImageTag;
|
||||
/** 背景图片宽度.*/
|
||||
private Integer backgroundImageWidth;
|
||||
/** 背景图片高度.*/
|
||||
private Integer backgroundImageHeight;
|
||||
/** 滑动图片宽度.*/
|
||||
private Integer templateImageWidth;
|
||||
/** 滑动图片高度.*/
|
||||
private Integer templateImageHeight;
|
||||
/** data 扩展数据.*/
|
||||
private Object data;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cloud.tianai.captcha.cache;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/3/2 14:35
|
||||
* @Description 提取出用于缓存的接口
|
||||
*/
|
||||
public interface CacheStore {
|
||||
|
||||
/**
|
||||
* 读取缓存数据通过key
|
||||
*
|
||||
* @param key key
|
||||
* @return AnyMap
|
||||
*/
|
||||
AnyMap getCache(String key);
|
||||
|
||||
/**
|
||||
* 获取并删除数据 通过key
|
||||
*
|
||||
* @param key key
|
||||
* @return AnyMap
|
||||
*/
|
||||
AnyMap getAndRemoveCache(String key);
|
||||
|
||||
/**
|
||||
* 添加缓存数据
|
||||
*
|
||||
* @param key key
|
||||
* @param data data
|
||||
* @param expire 过期时间
|
||||
* @param timeUnit 过期时间单位
|
||||
* @return boolean
|
||||
*/
|
||||
boolean setCache(String key, AnyMap data, Long expire, TimeUnit timeUnit);
|
||||
|
||||
|
||||
/**
|
||||
* incr 数字
|
||||
*
|
||||
* @param key key
|
||||
* @param delta 境量
|
||||
* @param expire 过期时间
|
||||
* @param timeUnit 过期时间单位
|
||||
* @return Long
|
||||
*/
|
||||
Long incr(String key, long delta, Long expire, TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* get 数字
|
||||
*
|
||||
* @param key key
|
||||
* @return Long
|
||||
*/
|
||||
Long getLong(String key);
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package cloud.tianai.captcha.cache.impl;
|
||||
|
||||
|
||||
import cloud.tianai.captcha.common.util.NamedThreadFactory;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2020/10/12 10:02
|
||||
* @Description 给予本人以前写的 expiring-map(redis淘汰策略的java实现) 项目进行改造
|
||||
*/
|
||||
@Slf4j
|
||||
@Accessors(chain = true)
|
||||
public class ConCurrentExpiringMap<K, V> implements ExpiringMap<K, V> {
|
||||
|
||||
private ConcurrentHashMap<K, TimeMapEntity<K, V>> storage;
|
||||
private SortedMap<Long, LinkedList<K>> sortedMap = new ConcurrentSkipListMap<>();
|
||||
private final ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("expiring-map-expire"));
|
||||
public static final int LIMIT = 500;
|
||||
|
||||
public ConCurrentExpiringMap() {
|
||||
this(128);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
scheduledExecutor.scheduleAtFixedRate(new ExpireThread(), 5, 5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public ConCurrentExpiringMap(Integer initialCapacity) {
|
||||
storage = new ConcurrentHashMap<>(initialCapacity);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeMapEntity<K, V> put(K k, V v, Long expire, TimeUnit timeUnit) {
|
||||
if (expire == null || expire < 1) {
|
||||
expire = DEFAULT_EXPIRE;
|
||||
}
|
||||
TimeMapEntity<K, V> entity;
|
||||
if (expire != null && expire > 0) {
|
||||
entity = new TimeMapEntity<>(k, v, timeUnit.toNanos(expire), System.nanoTime());
|
||||
sortedMap.computeIfAbsent(entity.getTimeout(), (k1) -> new LinkedList<>()).add(k);
|
||||
} else {
|
||||
entity = new TimeMapEntity<>(k, v, DEFAULT_EXPIRE, System.nanoTime());
|
||||
}
|
||||
TimeMapEntity<K, V> old = storage.put(k, entity);
|
||||
return old;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TimeMapEntity<K, V>> getData(K k) {
|
||||
return Optional.ofNullable(storage.get(k));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getExpire(K k) {
|
||||
return getData(k).map(TimeMapEntity::getExpire).orElse(DEFAULT_EXPIRE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incr(K k, Long expire, TimeUnit timeUnit) {
|
||||
Optional<TimeMapEntity<K, V>> entityOptional = getData(k);
|
||||
if (!entityOptional.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
synchronized (k) {
|
||||
// 双重校验
|
||||
entityOptional = getData(k);
|
||||
if (!entityOptional.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
TimeMapEntity<K, V> entity = entityOptional.get();
|
||||
|
||||
TimeMapEntity<K, V> newEntity = entity;
|
||||
newEntity.setExpire(entity.getExpire() + expire);
|
||||
if (expire != null && expire > 0) {
|
||||
sortedMap.getOrDefault(k, new LinkedList<>()).add(k);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return storage.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return storage.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return storage.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
Collection<TimeMapEntity<K, V>> values = storage.values();
|
||||
Optional<TimeMapEntity<K, V>> any = values.stream().filter(v -> v.getValue().equals(value)).findAny();
|
||||
return any.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object key) {
|
||||
TimeMapEntity<K, V> timeMapEntity = storage.get(key);
|
||||
if (isTimeout(timeMapEntity)) {
|
||||
removeData(key);
|
||||
return null;
|
||||
}
|
||||
return timeMapEntity.getValue();
|
||||
}
|
||||
|
||||
protected boolean isTimeout(K key) {
|
||||
Optional<TimeMapEntity<K, V>> data = getData(key);
|
||||
return isTimeout(data.orElse(null));
|
||||
}
|
||||
|
||||
protected boolean isTimeout(TimeMapEntity<K, V> timeMapEntity) {
|
||||
if (timeMapEntity == null || timeMapEntity.getExpire() < 1) {
|
||||
return true;
|
||||
}
|
||||
long currentTimeMillis = System.nanoTime();
|
||||
long timeout = timeMapEntity.getTimeout();
|
||||
return timeout < currentTimeMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) {
|
||||
return put(key, value, DEFAULT_EXPIRE, null).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(Object key) {
|
||||
return removeData(key).map(TimeMapEntity::getValue).orElse(null);
|
||||
}
|
||||
|
||||
protected Optional<TimeMapEntity<K, V>> removeData(Object key) {
|
||||
synchronized (key) {
|
||||
TimeMapEntity<K, V> oldValue = storage.get(key);
|
||||
if (oldValue != null) {
|
||||
TimeMapEntity<K, V> entity = storage.remove(key);
|
||||
Long expire = oldValue.getExpire();
|
||||
if (expire != null && expire > 0) {
|
||||
LinkedList<K> ks = sortedMap.get(expire);
|
||||
if (ks != null) {
|
||||
ks.remove(key);
|
||||
}
|
||||
}
|
||||
if (entity != null) {
|
||||
return Optional.of(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends K, ? extends V> m) {
|
||||
m.forEach(this::put);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
Map<K, TimeMapEntity<K, V>> copyStorage = new HashMap<>(storage);
|
||||
storage.clear();
|
||||
sortedMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个可能会消耗点cpu
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
return storage.keySet()
|
||||
.stream()
|
||||
.parallel()
|
||||
.filter(k -> !isTimeout(k))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<V> values() {
|
||||
return storage.values().stream().map(TimeMapEntity::getValue).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<K, V>> entrySet() {
|
||||
throw new IllegalArgumentException("timemap not impl entrySet.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时执行任务
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
private class ExpireThread implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
SortedMap<Long, LinkedList<K>> expireMap = ConCurrentExpiringMap.this.sortedMap;
|
||||
int limit = ConCurrentExpiringMap.LIMIT;
|
||||
//1.判断是否为空
|
||||
if (expireMap == null || expireMap.size() < 1) {
|
||||
return;
|
||||
}
|
||||
log.debug("storage-size: {}", ConCurrentExpiringMap.this.storage.size());
|
||||
log.debug("expire-size: {}", expireMap.size());
|
||||
//2. 获取 key 进行处理
|
||||
int count = 0;
|
||||
LinkedList<Long> removeKeys = null;
|
||||
// 删除的逻辑处理
|
||||
long currentTime = System.nanoTime();
|
||||
if (currentTime < expireMap.firstKey()) {
|
||||
return;
|
||||
}
|
||||
for (Entry<Long, LinkedList<K>> entry : expireMap.entrySet()) {
|
||||
final Long expireAt = entry.getKey();
|
||||
LinkedList<K> expireKeys = entry.getValue();
|
||||
// 判断队列是否为空
|
||||
if (expireKeys == null || expireKeys.size() < 1) {
|
||||
if (removeKeys == null) {
|
||||
removeKeys = new LinkedList<>();
|
||||
}
|
||||
removeKeys.add(expireAt);
|
||||
continue;
|
||||
}
|
||||
if (count >= limit) {
|
||||
// 检索数量达到z最大值,直接跳出
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentTime >= expireAt) {
|
||||
Iterator<K> iterator = expireKeys.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
K key = iterator.next();
|
||||
// 先移除本身
|
||||
iterator.remove();
|
||||
// 再移除缓存,后续可以通过惰性删除做补偿
|
||||
ConCurrentExpiringMap.this.get(key);
|
||||
if (removeKeys == null) {
|
||||
removeKeys = new LinkedList<>();
|
||||
}
|
||||
removeKeys.add(expireAt);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removeKeys != null && removeKeys.size() > 0) {
|
||||
for (Long removeKey : removeKeys) {
|
||||
expireMap.remove(removeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cloud.tianai.captcha.cache.impl;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public interface ExpiringMap<K, V> extends Map<K, V> {
|
||||
/**
|
||||
* 默认-1 无超时时间.
|
||||
*/
|
||||
Long DEFAULT_EXPIRE = -1L;
|
||||
|
||||
/**
|
||||
* 添加值
|
||||
* @param k key
|
||||
* @param v value
|
||||
* @param timeout 超时时间,
|
||||
* @param timeUnit 超时时间单位
|
||||
* @return 返回旧的数据,如果没有,返回null
|
||||
*/
|
||||
TimeMapEntity<K, V> put(K k, V v, Long timeout, TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* 获取value值
|
||||
* @param k key
|
||||
* @return
|
||||
*/
|
||||
Optional<TimeMapEntity<K, V>> getData(K k);
|
||||
|
||||
/**
|
||||
* 获取某个key的过期时间
|
||||
* @param k key
|
||||
* @return 单位毫秒
|
||||
*/
|
||||
Long getExpire(K k);
|
||||
|
||||
/**
|
||||
* 增加过期时间
|
||||
* @param k key
|
||||
* @param expire 过期时间
|
||||
* @param timeUnit 超时时间单位
|
||||
* @return
|
||||
*/
|
||||
boolean incr(K k, Long expire, TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
void init();
|
||||
|
||||
@Data
|
||||
class TimeMapEntity<K, V> {
|
||||
private K key;
|
||||
private V value;
|
||||
private Long expire;
|
||||
private Long createTime;
|
||||
private long timeout = -1;
|
||||
|
||||
TimeMapEntity(K k, V value, Long expire, Long createTime) {
|
||||
this.key = k;
|
||||
this.value = value;
|
||||
this.expire = expire;
|
||||
this.createTime = createTime;
|
||||
if (expire > 0) {
|
||||
this.timeout = createTime + expire;
|
||||
}
|
||||
}
|
||||
|
||||
public TimeMapEntity(TimeMapEntity<K, V> entity) {
|
||||
this.key = entity.getKey();
|
||||
this.value = entity.getValue();
|
||||
this.expire = entity.getExpire();
|
||||
this.createTime = entity.getCreateTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cloud.tianai.captcha.cache.impl;
|
||||
|
||||
|
||||
import cloud.tianai.captcha.cache.CacheStore;
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/3/2 14:39
|
||||
* @Description 本地缓存
|
||||
*/
|
||||
public class LocalCacheStore implements CacheStore {
|
||||
|
||||
protected ExpiringMap<String, AnyMap> cache;
|
||||
|
||||
public LocalCacheStore() {
|
||||
cache = new ConCurrentExpiringMap<>();
|
||||
cache.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnyMap getCache(String key) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnyMap getAndRemoveCache(String key) {
|
||||
return cache.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setCache(String key, AnyMap data, Long expire, TimeUnit timeUnit) {
|
||||
cache.remove(key);
|
||||
cache.put(key, data, expire, timeUnit);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long incr(String key, long delta, Long expire, TimeUnit timeUnit) {
|
||||
Map<String, Object> value = cache.remove(key);
|
||||
if (value != null) {
|
||||
Long incr = (Long) value.get("___incr___");
|
||||
if (incr == null) {
|
||||
incr = 0L;
|
||||
}
|
||||
incr += delta;
|
||||
cache.put(key, AnyMap.of(Collections.singletonMap("___incr___", incr)), expire, timeUnit);
|
||||
return incr;
|
||||
}
|
||||
cache.put(key, AnyMap.of(Collections.singletonMap("___incr___", delta)), expire, timeUnit);
|
||||
return delta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getLong(String key) {
|
||||
Map<String, Object> stringObjectMap = cache.get(key);
|
||||
if (stringObjectMap != null) {
|
||||
return (Long) stringObjectMap.get("___incr___");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package cloud.tianai.captcha.common;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class AnyMap implements Map<String, Object> {
|
||||
|
||||
private Map<String, Object> target;
|
||||
|
||||
public AnyMap() {
|
||||
target = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
public AnyMap(Map<String, Object> map) {
|
||||
this.target = map;
|
||||
}
|
||||
|
||||
public Float getFloat(String key) {
|
||||
return getFloat(key, null);
|
||||
}
|
||||
|
||||
public Float getFloat(String key, Float defaultData) {
|
||||
Object data = get(key);
|
||||
if (data != null) {
|
||||
if (data instanceof Number) {
|
||||
return ((Number) data).floatValue();
|
||||
}
|
||||
try {
|
||||
if (data instanceof String) {
|
||||
return Float.parseFloat((String) data);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return defaultData;
|
||||
}
|
||||
|
||||
public Integer getInt(String key, Integer defaultData) {
|
||||
Object data = get(key);
|
||||
if (data != null) {
|
||||
if (data instanceof Number) {
|
||||
return ((Number) data).intValue();
|
||||
}
|
||||
try {
|
||||
if (data instanceof String) {
|
||||
return Integer.parseInt((String) data);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return defaultData;
|
||||
}
|
||||
|
||||
public String getString(String key, String defaultData) {
|
||||
Object data = get(key);
|
||||
if (data != null) {
|
||||
if (data instanceof String) {
|
||||
return (String) data;
|
||||
}
|
||||
return String.valueOf(data);
|
||||
}
|
||||
return defaultData;
|
||||
}
|
||||
|
||||
|
||||
public static AnyMap of(Map<String, Object> map) {
|
||||
return new AnyMap(map);
|
||||
}
|
||||
|
||||
// ================== implement Map =======================
|
||||
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return target.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return target.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return target.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return target.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
return target.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
return target.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object remove(Object key) {
|
||||
return target.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ?> m) {
|
||||
target.putAll(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
target.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return target.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Object> values() {
|
||||
return target.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
return target.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getOrDefault(Object key, Object defaultValue) {
|
||||
return target.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(BiConsumer<? super String, ? super Object> action) {
|
||||
target.forEach(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceAll(BiFunction<? super String, ? super Object, ?> function) {
|
||||
target.replaceAll(function);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object putIfAbsent(String key, Object value) {
|
||||
return target.putIfAbsent(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object key, Object value) {
|
||||
return target.remove(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replace(String key, Object oldValue, Object newValue) {
|
||||
return target.replace(key, oldValue, newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object replace(String key, Object value) {
|
||||
return target.replace(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object computeIfAbsent(String key, Function<? super String, ?> mappingFunction) {
|
||||
return target.computeIfAbsent(key, mappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object computeIfPresent(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
|
||||
return target.computeIfPresent(key, remappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object compute(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) {
|
||||
return target.compute(key, remappingFunction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object merge(String key, Object value, BiFunction<? super Object, ? super Object, ?> remappingFunction) {
|
||||
return target.merge(key, value, remappingFunction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cloud.tianai.captcha.common.constant;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 17:14
|
||||
* @Description 滑块类型
|
||||
*/
|
||||
public interface CaptchaTypeConstant {
|
||||
|
||||
/** 滑块. */
|
||||
String SLIDER = "SLIDER";
|
||||
/** 旋转. */
|
||||
String ROTATE = "ROTATE";
|
||||
/** 拼接. */
|
||||
String CONCAT = "CONCAT";
|
||||
/** 文字图片点选. */
|
||||
String WORD_IMAGE_CLICK = "WORD_IMAGE_CLICK";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cloud.tianai.captcha.common.constant;
|
||||
|
||||
public interface CommonConstant {
|
||||
|
||||
String DEFAULT_TAG = "default";
|
||||
|
||||
|
||||
/** 图标点选资源存储类型. */
|
||||
String IMAGE_ICON = "ICON";
|
||||
/** 蜂窝点选.*/
|
||||
String HONEYCOMB_CLICK_ICON = "HONEYCOMB_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资源文件路径.
|
||||
*/
|
||||
String DEFAULT_SLIDER_IMAGE_RESOURCE_PATH = "META-INF/cut-image/resource";
|
||||
/**
|
||||
* 默认的template资源文件路径.
|
||||
*/
|
||||
String DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH = "META-INF/cut-image/template";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cloud.tianai.captcha.common.exception;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/5/7 9:04
|
||||
* @Description 图片验证码异常
|
||||
*/
|
||||
public class ImageCaptchaException extends RuntimeException{
|
||||
public ImageCaptchaException() {
|
||||
}
|
||||
|
||||
public ImageCaptchaException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ImageCaptchaException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ImageCaptchaException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ImageCaptchaException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cloud.tianai.captcha.common.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/4/20 9:53
|
||||
* @Description API统一返回格式类
|
||||
*/
|
||||
@Data
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public class ApiResponse<T> implements Serializable {
|
||||
|
||||
public static final ApiResponse<?> SUCCESS;
|
||||
|
||||
static {
|
||||
CodeDefinition definition = ApiResponseStatusConstant.SUCCESS;
|
||||
SUCCESS = new ApiResponse(definition.getCode(), definition.getMessage(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* code码.
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 信息.
|
||||
*/
|
||||
private String msg;
|
||||
/**
|
||||
* 成功时返回的数据.
|
||||
*/
|
||||
private T data;
|
||||
|
||||
public ApiResponse(Integer code, String errMsg, T data) {
|
||||
this.code = code;
|
||||
this.msg = errMsg;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public ApiResponse(CodeDefinition definition, T data) {
|
||||
this.code = definition.getCode();
|
||||
this.msg = definition.getMessage();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public ApiResponse() {
|
||||
CodeDefinition definition = ApiResponseStatusConstant.SUCCESS;
|
||||
this.code = definition.getCode();
|
||||
this.msg = definition.getMessage();
|
||||
}
|
||||
|
||||
public <R> ApiResponse<R> convert() {
|
||||
ApiResponse<R> result = new ApiResponse<>();
|
||||
result.setCode(this.getCode());
|
||||
result.setMsg(this.getMsg());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public boolean isSuccess() {
|
||||
return ApiResponseStatusConstant.SUCCESS.getCode().equals(getCode());
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> of(Integer code, String msg, T data) {
|
||||
return new ApiResponse(code, msg, data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> of(CodeDefinition definition, T data) {
|
||||
return new ApiResponse(definition.getCode(), definition.getMessage(), data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> ofMessage(CodeDefinition definition) {
|
||||
return new ApiResponse(definition.getCode(), definition.getMessage(), null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> ofError(String message) {
|
||||
return new ApiResponse(ApiResponseStatusConstant.INTERNAL_SERVER_ERROR.getCode(), message, null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> ofError(String message, Object obj) {
|
||||
return new ApiResponse(ApiResponseStatusConstant.INTERNAL_SERVER_ERROR.getCode(), message, obj);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> ofCheckError(String message) {
|
||||
return new ApiResponse(ApiResponseStatusConstant.NOT_VALID_PARAM.getCode(), message, null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> ofSuccess(T data) {
|
||||
CodeDefinition definition = ApiResponseStatusConstant.SUCCESS;
|
||||
return new ApiResponse(definition.getCode(), definition.getMessage(), data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> ofSuccess() {
|
||||
return (ApiResponse<T>) SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cloud.tianai.captcha.common.response;
|
||||
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @Date 2020/5/26 17:58
|
||||
* @Description 统一返回错误码, 详见 阿里巴巴开发规范 错误码列表
|
||||
* <p>
|
||||
* 该枚举定义了一些公共的code码,自定义code码数据需在自己业务中编写
|
||||
*/
|
||||
public interface ApiResponseStatusConstant {
|
||||
|
||||
/**
|
||||
* 成功.
|
||||
*/
|
||||
CodeDefinition SUCCESS = new CodeDefinition(200, "OK");
|
||||
|
||||
CodeDefinition NOT_VALID_PARAM = new CodeDefinition(403, "无效参数");
|
||||
|
||||
CodeDefinition INTERNAL_SERVER_ERROR = new CodeDefinition(500, "未知的内部错误");
|
||||
|
||||
CodeDefinition EXPIRED = new CodeDefinition(4000, "已失效");
|
||||
|
||||
CodeDefinition BASIC_CHECK_FAIL = new CodeDefinition(4001, "基础校验失败");
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cloud.tianai.captcha.common.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/13 12:37
|
||||
* @Description code 定义
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class CodeDefinition {
|
||||
|
||||
private Integer code;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package cloud.tianai.captcha.common.util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/11/2 9:22
|
||||
* @Description 验证码类型分类
|
||||
*/
|
||||
public class CaptchaTypeClassifier {
|
||||
|
||||
private static final Set<String> SLIDER_CAPTCHA_TYPES = new HashSet<>();
|
||||
private static final Set<String> CLICK_CAPTCHA_TYPES = new HashSet<>();
|
||||
private static final Set<String> JIGSAW_CAPTCHA_TYPES = new HashSet<>();
|
||||
|
||||
public static void addSliderCaptchaType(String type) {
|
||||
SLIDER_CAPTCHA_TYPES.add(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static void addClickCaptchaType(String type) {
|
||||
CLICK_CAPTCHA_TYPES.add(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static boolean isSliderCaptcha(String type) {
|
||||
return SLIDER_CAPTCHA_TYPES.contains(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static boolean isClickCaptcha(String type) {
|
||||
return CLICK_CAPTCHA_TYPES.contains(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static Set<String> getSliderCaptchaTypes() {
|
||||
return SLIDER_CAPTCHA_TYPES;
|
||||
}
|
||||
|
||||
public static Set<String> getClickCaptchaTypes() {
|
||||
return CLICK_CAPTCHA_TYPES;
|
||||
}
|
||||
|
||||
public static void removeSliderCaptchaType(String type) {
|
||||
SLIDER_CAPTCHA_TYPES.remove(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static void removeClickCaptchaType(String type) {
|
||||
CLICK_CAPTCHA_TYPES.remove(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static boolean isJigsawCaptcha(String type) {
|
||||
return JIGSAW_CAPTCHA_TYPES.contains(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static void addJigsawCaptchaType(String type) {
|
||||
JIGSAW_CAPTCHA_TYPES.add(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static void removeJigsawCaptchaType(String type) {
|
||||
JIGSAW_CAPTCHA_TYPES.remove(type.toUpperCase());
|
||||
}
|
||||
|
||||
public static Set<String> getJigsawCaptchaTypes() {
|
||||
return JIGSAW_CAPTCHA_TYPES;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cloud.tianai.captcha.common.util;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* 拷贝spring
|
||||
* Miscellaneous collection utility methods.
|
||||
* Mainly for internal use within the framework.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rob Harrop
|
||||
* @author Arjen Poutsma
|
||||
* @since 1.1.3
|
||||
*/
|
||||
public abstract class CollectionUtils {
|
||||
|
||||
/**
|
||||
* Return {@code true} if the supplied Collection is {@code null} or empty.
|
||||
* Otherwise, return {@code false}.
|
||||
* @param collection the Collection to check
|
||||
* @return whether the given Collection is empty
|
||||
*/
|
||||
public static boolean isEmpty(Collection<?> collection) {
|
||||
return (collection == null || collection.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the supplied Map is {@code null} or empty.
|
||||
* Otherwise, return {@code false}.
|
||||
* @param map the Map to check
|
||||
* @return whether the given Map is empty
|
||||
*/
|
||||
public static boolean isEmpty(Map<?, ?> map) {
|
||||
return (map == null || map.isEmpty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Merge the given Properties instance into the given Map,
|
||||
* copying all properties (key-value pairs) over.
|
||||
* <p>Uses {@code Properties.propertyNames()} to even catch
|
||||
* default properties linked into the original Properties instance.
|
||||
* @param props the Properties instance to merge (may be {@code null})
|
||||
* @param map the target Map to merge the properties into
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <K, V> void mergePropertiesIntoMap(Properties props, Map<K, V> map) {
|
||||
if (props != null) {
|
||||
for (Enumeration<?> en = props.propertyNames(); en.hasMoreElements();) {
|
||||
String key = (String) en.nextElement();
|
||||
Object value = props.get(key);
|
||||
if (value == null) {
|
||||
// Allow for defaults fallback or potentially overridden accessor...
|
||||
value = props.getProperty(key);
|
||||
}
|
||||
map.put((K) key, (V) value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the given Iterator contains the given element.
|
||||
* @param iterator the Iterator to check
|
||||
* @param element the element to look for
|
||||
* @return {@code true} if found, {@code false} otherwise
|
||||
*/
|
||||
public static boolean contains(Iterator<?> iterator, Object element) {
|
||||
if (iterator != null) {
|
||||
while (iterator.hasNext()) {
|
||||
Object candidate = iterator.next();
|
||||
if (ObjectUtils.nullSafeEquals(candidate, element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given Enumeration contains the given element.
|
||||
* @param enumeration the Enumeration to check
|
||||
* @param element the element to look for
|
||||
* @return {@code true} if found, {@code false} otherwise
|
||||
*/
|
||||
public static boolean contains(Enumeration<?> enumeration, Object element) {
|
||||
if (enumeration != null) {
|
||||
while (enumeration.hasMoreElements()) {
|
||||
Object candidate = enumeration.nextElement();
|
||||
if (ObjectUtils.nullSafeEquals(candidate, element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given Collection contains the given element instance.
|
||||
* <p>Enforces the given instance to be present, rather than returning
|
||||
* {@code true} for an equal element as well.
|
||||
* @param collection the Collection to check
|
||||
* @param element the element to look for
|
||||
* @return {@code true} if found, {@code false} otherwise
|
||||
*/
|
||||
public static boolean containsInstance(Collection<?> collection, Object element) {
|
||||
if (collection != null) {
|
||||
for (Object candidate : collection) {
|
||||
if (candidate == element) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if any element in '{@code candidates}' is
|
||||
* contained in '{@code source}'; otherwise returns {@code false}.
|
||||
* @param source the source Collection
|
||||
* @param candidates the candidates to search for
|
||||
* @return whether any of the candidates has been found
|
||||
*/
|
||||
public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
|
||||
return findFirstMatch(source, candidates) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first element in '{@code candidates}' that is contained in
|
||||
* '{@code source}'. If no element in '{@code candidates}' is present in
|
||||
* '{@code source}' returns {@code null}. Iteration order is
|
||||
* {@link Collection} implementation specific.
|
||||
* @param source the source Collection
|
||||
* @param candidates the candidates to search for
|
||||
* @return the first present object, or {@code null} if not found
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <E> E findFirstMatch(Collection<?> source, Collection<E> candidates) {
|
||||
if (isEmpty(source) || isEmpty(candidates)) {
|
||||
return null;
|
||||
}
|
||||
for (Object candidate : candidates) {
|
||||
if (source.contains(candidate)) {
|
||||
return (E) candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single value of the given type in the given Collection.
|
||||
* @param collection the Collection to search
|
||||
* @param type the type to look for
|
||||
* @return a value of the given type found if there is a clear match,
|
||||
* or {@code null} if none or more than one such value found
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T findValueOfType(Collection<?> collection, Class<T> type) {
|
||||
if (isEmpty(collection)) {
|
||||
return null;
|
||||
}
|
||||
T value = null;
|
||||
for (Object element : collection) {
|
||||
if (type == null || type.isInstance(element)) {
|
||||
if (value != null) {
|
||||
// More than one value found... no clear single value.
|
||||
return null;
|
||||
}
|
||||
value = (T) element;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single value of one of the given types in the given Collection:
|
||||
* searching the Collection for a value of the first type, then
|
||||
* searching for a value of the second type, etc.
|
||||
* @param collection the collection to search
|
||||
* @param types the types to look for, in prioritized order
|
||||
* @return a value of one of the given types found if there is a clear match,
|
||||
* or {@code null} if none or more than one such value found
|
||||
*/
|
||||
public static Object findValueOfType(Collection<?> collection, Class<?>[] types) {
|
||||
if (isEmpty(collection) || ObjectUtils.isEmpty(types)) {
|
||||
return null;
|
||||
}
|
||||
for (Class<?> type : types) {
|
||||
Object value = findValueOfType(collection, type);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given Collection only contains a single unique object.
|
||||
* @param collection the Collection to check
|
||||
* @return {@code true} if the collection contains a single reference or
|
||||
* multiple references to the same instance, {@code false} otherwise
|
||||
*/
|
||||
public static boolean hasUniqueObject(Collection<?> collection) {
|
||||
if (isEmpty(collection)) {
|
||||
return false;
|
||||
}
|
||||
boolean hasCandidate = false;
|
||||
Object candidate = null;
|
||||
for (Object elem : collection) {
|
||||
if (!hasCandidate) {
|
||||
hasCandidate = true;
|
||||
candidate = elem;
|
||||
}
|
||||
else if (candidate != elem) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the common element type of the given Collection, if any.
|
||||
* @param collection the Collection to check
|
||||
* @return the common element type, or {@code null} if no clear
|
||||
* common type has been found (or the collection was empty)
|
||||
*/
|
||||
public static Class<?> findCommonElementType(Collection<?> collection) {
|
||||
if (isEmpty(collection)) {
|
||||
return null;
|
||||
}
|
||||
Class<?> candidate = null;
|
||||
for (Object val : collection) {
|
||||
if (val != null) {
|
||||
if (candidate == null) {
|
||||
candidate = val.getClass();
|
||||
}
|
||||
else if (candidate != val.getClass()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first element of the given Set, using {@link SortedSet#first()}
|
||||
* or otherwise using the iterator.
|
||||
* @param set the Set to check (may be {@code null} or empty)
|
||||
* @return the first element, or {@code null} if none
|
||||
* @since 5.2.3
|
||||
* @see SortedSet
|
||||
* @see LinkedHashMap#keySet()
|
||||
* @see LinkedHashSet
|
||||
*/
|
||||
public static <T> T firstElement(Set<T> set) {
|
||||
if (isEmpty(set)) {
|
||||
return null;
|
||||
}
|
||||
if (set instanceof SortedSet) {
|
||||
return ((SortedSet<T>) set).first();
|
||||
}
|
||||
|
||||
Iterator<T> it = set.iterator();
|
||||
T first = null;
|
||||
if (it.hasNext()) {
|
||||
first = it.next();
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first element of the given List, accessing the zero index.
|
||||
* @param list the List to check (may be {@code null} or empty)
|
||||
* @return the first element, or {@code null} if none
|
||||
* @since 5.2.3
|
||||
*/
|
||||
public static <T> T firstElement(List<T> list) {
|
||||
if (isEmpty(list)) {
|
||||
return null;
|
||||
}
|
||||
return list.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the last element of the given Set, using {@link SortedSet#last()}
|
||||
* or otherwise iterating over all elements (assuming a linked set).
|
||||
* @param set the Set to check (may be {@code null} or empty)
|
||||
* @return the last element, or {@code null} if none
|
||||
* @since 5.0.3
|
||||
* @see SortedSet
|
||||
* @see LinkedHashMap#keySet()
|
||||
* @see LinkedHashSet
|
||||
*/
|
||||
public static <T> T lastElement(Set<T> set) {
|
||||
if (isEmpty(set)) {
|
||||
return null;
|
||||
}
|
||||
if (set instanceof SortedSet) {
|
||||
return ((SortedSet<T>) set).last();
|
||||
}
|
||||
|
||||
// Full iteration necessary...
|
||||
Iterator<T> it = set.iterator();
|
||||
T last = null;
|
||||
while (it.hasNext()) {
|
||||
last = it.next();
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the last element of the given List, accessing the highest index.
|
||||
* @param list the List to check (may be {@code null} or empty)
|
||||
* @return the last element, or {@code null} if none
|
||||
* @since 5.0.3
|
||||
*/
|
||||
public static <T> T lastElement(List<T> list) {
|
||||
if (isEmpty(list)) {
|
||||
return null;
|
||||
}
|
||||
return list.get(list.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshal the elements from the given enumeration into an array of the given type.
|
||||
* Enumeration elements must be assignable to the type of the given array. The array
|
||||
* returned will be a different instance than the array given.
|
||||
*/
|
||||
public static <A, E extends A> A[] toArray(Enumeration<E> enumeration, A[] array) {
|
||||
ArrayList<A> elements = new ArrayList<>();
|
||||
while (enumeration.hasMoreElements()) {
|
||||
elements.add(enumeration.nextElement());
|
||||
}
|
||||
return elements.toArray(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt an {@link Enumeration} to an {@link Iterator}.
|
||||
* @param enumeration the original {@code Enumeration}
|
||||
* @return the adapted {@code Iterator}
|
||||
*/
|
||||
public static <E> Iterator<E> toIterator(Enumeration<E> enumeration) {
|
||||
return (enumeration != null ? new EnumerationIterator<>(enumeration) : Collections.emptyIterator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator wrapping an Enumeration.
|
||||
*/
|
||||
private static class EnumerationIterator<E> implements Iterator<E> {
|
||||
|
||||
private final Enumeration<E> enumeration;
|
||||
|
||||
public EnumerationIterator(Enumeration<E> enumeration) {
|
||||
this.enumeration = enumeration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return this.enumeration.hasMoreElements();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E next() {
|
||||
return this.enumeration.nextElement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() throws UnsupportedOperationException {
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cloud.tianai.captcha.common.util;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/27 11:34
|
||||
* @Description 字体工具包
|
||||
*/
|
||||
public class FontUtils {
|
||||
|
||||
/**
|
||||
* 获取随机文字
|
||||
*
|
||||
* @param random 随机数生成器
|
||||
* @return String
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static String getRandomChar(Random random) {
|
||||
Integer heightPos, lowPos; // 定义高低位
|
||||
heightPos = (176 + Math.abs(random.nextInt(39)));
|
||||
lowPos = (161 + Math.abs(random.nextInt(93)));
|
||||
byte[] bytes = new byte[2];
|
||||
bytes[0] = heightPos.byteValue();
|
||||
bytes[1] = lowPos.byteValue();
|
||||
return new String(bytes, "GBK");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cloud.tianai.captcha.common.util;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* A ThreadFactory that allows for custom thread names.
|
||||
*/
|
||||
public class NamedThreadFactory implements ThreadFactory {
|
||||
|
||||
private static final AtomicInteger THREAD_INDEX = new AtomicInteger(0);
|
||||
|
||||
private final String basename;
|
||||
private final boolean daemon;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the factory.
|
||||
*
|
||||
* @param basename Basename of a new tread created by this factory.
|
||||
*/
|
||||
public NamedThreadFactory(final String basename) {
|
||||
this(basename, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the factory.
|
||||
*
|
||||
* @param basename Basename of a new tread created by this factory.
|
||||
* @param daemon If true, marks new thread as a daemon thread
|
||||
*/
|
||||
public NamedThreadFactory(final String basename, final boolean daemon) {
|
||||
|
||||
this.basename = basename;
|
||||
this.daemon = daemon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(final Runnable runnable) {
|
||||
|
||||
final Thread thread = new Thread(runnable, basename + "-" + THREAD_INDEX.getAndIncrement());
|
||||
thread.setDaemon(daemon);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,905 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cloud.tianai.captcha.common.util;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 拷贝spring
|
||||
*
|
||||
* Miscellaneous object utility methods.
|
||||
*
|
||||
* <p>Mainly for internal use within the framework.
|
||||
*
|
||||
* <p>Thanks to Alex Ruiz for contributing several enhancements to this class!
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Keith Donald
|
||||
* @author Rod Johnson
|
||||
* @author Rob Harrop
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
* @since 19.03.2004
|
||||
* @see CollectionUtils
|
||||
*/
|
||||
public abstract class ObjectUtils {
|
||||
|
||||
private static final int INITIAL_HASH = 7;
|
||||
private static final int MULTIPLIER = 31;
|
||||
|
||||
private static final String EMPTY_STRING = "";
|
||||
private static final String NULL_STRING = "null";
|
||||
private static final String ARRAY_START = "{";
|
||||
private static final String ARRAY_END = "}";
|
||||
private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END;
|
||||
private static final String ARRAY_ELEMENT_SEPARATOR = ", ";
|
||||
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
|
||||
|
||||
|
||||
/**
|
||||
* Return whether the given throwable is a checked exception:
|
||||
* that is, neither a RuntimeException nor an Error.
|
||||
* @param ex the throwable to check
|
||||
* @return whether the throwable is a checked exception
|
||||
* @see Exception
|
||||
* @see RuntimeException
|
||||
* @see Error
|
||||
*/
|
||||
public static boolean isCheckedException(Throwable ex) {
|
||||
return !(ex instanceof RuntimeException || ex instanceof Error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given exception is compatible with the specified
|
||||
* exception types, as declared in a throws clause.
|
||||
* @param ex the exception to check
|
||||
* @param declaredExceptions the exception types declared in the throws clause
|
||||
* @return whether the given exception is compatible
|
||||
*/
|
||||
public static boolean isCompatibleWithThrowsClause(Throwable ex, Class<?>... declaredExceptions) {
|
||||
if (!isCheckedException(ex)) {
|
||||
return true;
|
||||
}
|
||||
if (declaredExceptions != null) {
|
||||
for (Class<?> declaredException : declaredExceptions) {
|
||||
if (declaredException.isInstance(ex)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given object is an array:
|
||||
* either an Object array or a primitive array.
|
||||
* @param obj the object to check
|
||||
*/
|
||||
public static boolean isArray(Object obj) {
|
||||
return (obj != null && obj.getClass().isArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given array is empty:
|
||||
* i.e. {@code null} or of zero length.
|
||||
* @param array the array to check
|
||||
* @see #isEmpty(Object)
|
||||
*/
|
||||
public static boolean isEmpty(Object[] array) {
|
||||
return (array == null || array.length == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given object is empty.
|
||||
* <p>This method supports the following object types.
|
||||
* <ul>
|
||||
* <li>{@code Optional}: considered empty if {@link Optional#empty()}</li>
|
||||
* <li>{@code Array}: considered empty if its length is zero</li>
|
||||
* <li>{@link CharSequence}: considered empty if its length is zero</li>
|
||||
* <li>{@link Collection}: delegates to {@link Collection#isEmpty()}</li>
|
||||
* <li>{@link Map}: delegates to {@link Map#isEmpty()}</li>
|
||||
* </ul>
|
||||
* <p>If the given object is non-null and not one of the aforementioned
|
||||
* supported types, this method returns {@code false}.
|
||||
* @param obj the object to check
|
||||
* @return {@code true} if the object is {@code null} or <em>empty</em>
|
||||
* @since 4.2
|
||||
* @see Optional#isPresent()
|
||||
* @see ObjectUtils#isEmpty(Object[])
|
||||
* @see CollectionUtils#isEmpty(Collection)
|
||||
* @see CollectionUtils#isEmpty(Map)
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static boolean isEmpty(Object obj) {
|
||||
if (obj == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj instanceof Optional) {
|
||||
return !((Optional) obj).isPresent();
|
||||
}
|
||||
if (obj instanceof CharSequence) {
|
||||
return ((CharSequence) obj).length() == 0;
|
||||
}
|
||||
if (obj.getClass().isArray()) {
|
||||
return Array.getLength(obj) == 0;
|
||||
}
|
||||
if (obj instanceof Collection) {
|
||||
return ((Collection) obj).isEmpty();
|
||||
}
|
||||
if (obj instanceof Map) {
|
||||
return ((Map) obj).isEmpty();
|
||||
}
|
||||
|
||||
// else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap the given object which is potentially a {@link Optional}.
|
||||
* @param obj the candidate object
|
||||
* @return either the value held within the {@code Optional}, {@code null}
|
||||
* if the {@code Optional} is empty, or simply the given object as-is
|
||||
* @since 5.0
|
||||
*/
|
||||
public static Object unwrapOptional(Object obj) {
|
||||
if (obj instanceof Optional) {
|
||||
Optional<?> optional = (Optional<?>) obj;
|
||||
if (!optional.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
Object result = optional.get();
|
||||
return result;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given array contains the given element.
|
||||
* @param array the array to check (may be {@code null},
|
||||
* in which case the return value will always be {@code false})
|
||||
* @param element the element to check for
|
||||
* @return whether the element has been found in the given array
|
||||
*/
|
||||
public static boolean containsElement(Object[] array, Object element) {
|
||||
if (array == null) {
|
||||
return false;
|
||||
}
|
||||
for (Object arrayEle : array) {
|
||||
if (nullSafeEquals(arrayEle, element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given array of enum constants contains a constant with the given name,
|
||||
* ignoring case when determining a match.
|
||||
* @param enumValues the enum values to check, typically obtained via {@code MyEnum.values()}
|
||||
* @param constant the constant name to find (must not be null or empty string)
|
||||
* @return whether the constant has been found in the given array
|
||||
*/
|
||||
public static boolean containsConstant(Enum<?>[] enumValues, String constant) {
|
||||
return containsConstant(enumValues, constant, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given array of enum constants contains a constant with the given name.
|
||||
* @param enumValues the enum values to check, typically obtained via {@code MyEnum.values()}
|
||||
* @param constant the constant name to find (must not be null or empty string)
|
||||
* @param caseSensitive whether case is significant in determining a match
|
||||
* @return whether the constant has been found in the given array
|
||||
*/
|
||||
public static boolean containsConstant(Enum<?>[] enumValues, String constant, boolean caseSensitive) {
|
||||
for (Enum<?> candidate : enumValues) {
|
||||
if (caseSensitive ? candidate.toString().equals(constant) :
|
||||
candidate.toString().equalsIgnoreCase(constant)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Case insensitive alternative to {@link Enum#valueOf(Class, String)}.
|
||||
* @param <E> the concrete Enum type
|
||||
* @param enumValues the array of all Enum constants in question, usually per {@code Enum.values()}
|
||||
* @param constant the constant to get the enum value of
|
||||
* @throws IllegalArgumentException if the given constant is not found in the given array
|
||||
* of enum values. Use {@link #containsConstant(Enum[], String)} as a guard to avoid this exception.
|
||||
*/
|
||||
public static <E extends Enum<?>> E caseInsensitiveValueOf(E[] enumValues, String constant) {
|
||||
for (E candidate : enumValues) {
|
||||
if (candidate.toString().equalsIgnoreCase(constant)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Constant [" + constant + "] does not exist in enum type " +
|
||||
enumValues.getClass().getComponentType().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the given object to the given array, returning a new array
|
||||
* consisting of the input array contents plus the given object.
|
||||
* @param array the array to append to (can be {@code null})
|
||||
* @param obj the object to append
|
||||
* @return the new array (of the same component type; never {@code null})
|
||||
*/
|
||||
public static <A, O extends A> A[] addObjectToArray(A[] array, O obj) {
|
||||
Class<?> compType = Object.class;
|
||||
if (array != null) {
|
||||
compType = array.getClass().getComponentType();
|
||||
}
|
||||
else if (obj != null) {
|
||||
compType = obj.getClass();
|
||||
}
|
||||
int newArrLength = (array != null ? array.length + 1 : 1);
|
||||
@SuppressWarnings("unchecked")
|
||||
A[] newArr = (A[]) Array.newInstance(compType, newArrLength);
|
||||
if (array != null) {
|
||||
System.arraycopy(array, 0, newArr, 0, array.length);
|
||||
}
|
||||
newArr[newArr.length - 1] = obj;
|
||||
return newArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given array (which may be a primitive array) to an
|
||||
* object array (if necessary of primitive wrapper objects).
|
||||
* <p>A {@code null} source value will be converted to an
|
||||
* empty Object array.
|
||||
* @param source the (potentially primitive) array
|
||||
* @return the corresponding object array (never {@code null})
|
||||
* @throws IllegalArgumentException if the parameter is not an array
|
||||
*/
|
||||
public static Object[] toObjectArray(Object source) {
|
||||
if (source instanceof Object[]) {
|
||||
return (Object[]) source;
|
||||
}
|
||||
if (source == null) {
|
||||
return EMPTY_OBJECT_ARRAY;
|
||||
}
|
||||
if (!source.getClass().isArray()) {
|
||||
throw new IllegalArgumentException("Source is not an array: " + source);
|
||||
}
|
||||
int length = Array.getLength(source);
|
||||
if (length == 0) {
|
||||
return EMPTY_OBJECT_ARRAY;
|
||||
}
|
||||
Class<?> wrapperType = Array.get(source, 0).getClass();
|
||||
Object[] newArray = (Object[]) Array.newInstance(wrapperType, length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
newArray[i] = Array.get(source, i);
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Convenience methods for content-based equality/hash-code handling
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determine if the given objects are equal, returning {@code true} if
|
||||
* both are {@code null} or {@code false} if only one is {@code null}.
|
||||
* <p>Compares arrays with {@code Arrays.equals}, performing an equality
|
||||
* check based on the array elements rather than the array reference.
|
||||
* @param o1 first Object to compare
|
||||
* @param o2 second Object to compare
|
||||
* @return whether the given objects are equal
|
||||
* @see Object#equals(Object)
|
||||
* @see Arrays#equals
|
||||
*/
|
||||
public static boolean nullSafeEquals(Object o1, Object o2) {
|
||||
if (o1 == o2) {
|
||||
return true;
|
||||
}
|
||||
if (o1 == null || o2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (o1.equals(o2)) {
|
||||
return true;
|
||||
}
|
||||
if (o1.getClass().isArray() && o2.getClass().isArray()) {
|
||||
return arrayEquals(o1, o2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the given arrays with {@code Arrays.equals}, performing an equality
|
||||
* check based on the array elements rather than the array reference.
|
||||
* @param o1 first array to compare
|
||||
* @param o2 second array to compare
|
||||
* @return whether the given objects are equal
|
||||
* @see #nullSafeEquals(Object, Object)
|
||||
* @see Arrays#equals
|
||||
*/
|
||||
private static boolean arrayEquals(Object o1, Object o2) {
|
||||
if (o1 instanceof Object[] && o2 instanceof Object[]) {
|
||||
return Arrays.equals((Object[]) o1, (Object[]) o2);
|
||||
}
|
||||
if (o1 instanceof boolean[] && o2 instanceof boolean[]) {
|
||||
return Arrays.equals((boolean[]) o1, (boolean[]) o2);
|
||||
}
|
||||
if (o1 instanceof byte[] && o2 instanceof byte[]) {
|
||||
return Arrays.equals((byte[]) o1, (byte[]) o2);
|
||||
}
|
||||
if (o1 instanceof char[] && o2 instanceof char[]) {
|
||||
return Arrays.equals((char[]) o1, (char[]) o2);
|
||||
}
|
||||
if (o1 instanceof double[] && o2 instanceof double[]) {
|
||||
return Arrays.equals((double[]) o1, (double[]) o2);
|
||||
}
|
||||
if (o1 instanceof float[] && o2 instanceof float[]) {
|
||||
return Arrays.equals((float[]) o1, (float[]) o2);
|
||||
}
|
||||
if (o1 instanceof int[] && o2 instanceof int[]) {
|
||||
return Arrays.equals((int[]) o1, (int[]) o2);
|
||||
}
|
||||
if (o1 instanceof long[] && o2 instanceof long[]) {
|
||||
return Arrays.equals((long[]) o1, (long[]) o2);
|
||||
}
|
||||
if (o1 instanceof short[] && o2 instanceof short[]) {
|
||||
return Arrays.equals((short[]) o1, (short[]) o2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return as hash code for the given object; typically the value of
|
||||
* {@code Object#hashCode()}}. If the object is an array,
|
||||
* this method will delegate to any of the {@code nullSafeHashCode}
|
||||
* methods for arrays in this class. If the object is {@code null},
|
||||
* this method returns 0.
|
||||
* @see Object#hashCode()
|
||||
* @see #nullSafeHashCode(Object[])
|
||||
* @see #nullSafeHashCode(boolean[])
|
||||
* @see #nullSafeHashCode(byte[])
|
||||
* @see #nullSafeHashCode(char[])
|
||||
* @see #nullSafeHashCode(double[])
|
||||
* @see #nullSafeHashCode(float[])
|
||||
* @see #nullSafeHashCode(int[])
|
||||
* @see #nullSafeHashCode(long[])
|
||||
* @see #nullSafeHashCode(short[])
|
||||
*/
|
||||
public static int nullSafeHashCode(Object obj) {
|
||||
if (obj == null) {
|
||||
return 0;
|
||||
}
|
||||
if (obj.getClass().isArray()) {
|
||||
if (obj instanceof Object[]) {
|
||||
return nullSafeHashCode((Object[]) obj);
|
||||
}
|
||||
if (obj instanceof boolean[]) {
|
||||
return nullSafeHashCode((boolean[]) obj);
|
||||
}
|
||||
if (obj instanceof byte[]) {
|
||||
return nullSafeHashCode((byte[]) obj);
|
||||
}
|
||||
if (obj instanceof char[]) {
|
||||
return nullSafeHashCode((char[]) obj);
|
||||
}
|
||||
if (obj instanceof double[]) {
|
||||
return nullSafeHashCode((double[]) obj);
|
||||
}
|
||||
if (obj instanceof float[]) {
|
||||
return nullSafeHashCode((float[]) obj);
|
||||
}
|
||||
if (obj instanceof int[]) {
|
||||
return nullSafeHashCode((int[]) obj);
|
||||
}
|
||||
if (obj instanceof long[]) {
|
||||
return nullSafeHashCode((long[]) obj);
|
||||
}
|
||||
if (obj instanceof short[]) {
|
||||
return nullSafeHashCode((short[]) obj);
|
||||
}
|
||||
}
|
||||
return obj.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(Object[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (Object element : array) {
|
||||
hash = MULTIPLIER * hash + nullSafeHashCode(element);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(boolean[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (boolean element : array) {
|
||||
hash = MULTIPLIER * hash + Boolean.hashCode(element);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(byte[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (byte element : array) {
|
||||
hash = MULTIPLIER * hash + element;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(char[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (char element : array) {
|
||||
hash = MULTIPLIER * hash + element;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(double[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (double element : array) {
|
||||
hash = MULTIPLIER * hash + Double.hashCode(element);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(float[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (float element : array) {
|
||||
hash = MULTIPLIER * hash + Float.hashCode(element);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(int[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (int element : array) {
|
||||
hash = MULTIPLIER * hash + element;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(long[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (long element : array) {
|
||||
hash = MULTIPLIER * hash + Long.hashCode(element);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code based on the contents of the specified array.
|
||||
* If {@code array} is {@code null}, this method returns 0.
|
||||
*/
|
||||
public static int nullSafeHashCode(short[] array) {
|
||||
if (array == null) {
|
||||
return 0;
|
||||
}
|
||||
int hash = INITIAL_HASH;
|
||||
for (short element : array) {
|
||||
hash = MULTIPLIER * hash + element;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the same value as {@link Boolean#hashCode(boolean)}}.
|
||||
* @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant
|
||||
*/
|
||||
@Deprecated
|
||||
public static int hashCode(boolean bool) {
|
||||
return Boolean.hashCode(bool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the same value as {@link Double#hashCode(double)}}.
|
||||
* @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant
|
||||
*/
|
||||
@Deprecated
|
||||
public static int hashCode(double dbl) {
|
||||
return Double.hashCode(dbl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the same value as {@link Float#hashCode(float)}}.
|
||||
* @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant
|
||||
*/
|
||||
@Deprecated
|
||||
public static int hashCode(float flt) {
|
||||
return Float.hashCode(flt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the same value as {@link Long#hashCode(long)}}.
|
||||
* @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant
|
||||
*/
|
||||
@Deprecated
|
||||
public static int hashCode(long lng) {
|
||||
return Long.hashCode(lng);
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Convenience methods for toString output
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return a String representation of an object's overall identity.
|
||||
* @param obj the object (may be {@code null})
|
||||
* @return the object's identity as String representation,
|
||||
* or an empty String if the object was {@code null}
|
||||
*/
|
||||
public static String identityToString(Object obj) {
|
||||
if (obj == null) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
String className = obj.getClass().getName();
|
||||
String identityHexString = getIdentityHexString(obj);
|
||||
return className + '@' + identityHexString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hex String form of an object's identity hash code.
|
||||
* @param obj the object
|
||||
* @return the object's identity code in hex notation
|
||||
*/
|
||||
public static String getIdentityHexString(Object obj) {
|
||||
return Integer.toHexString(System.identityHashCode(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a content-based String representation if {@code obj} is
|
||||
* not {@code null}; otherwise returns an empty String.
|
||||
* <p>Differs from {@link #nullSafeToString(Object)} in that it returns
|
||||
* an empty String rather than "null" for a {@code null} value.
|
||||
* @param obj the object to build a display String for
|
||||
* @return a display String representation of {@code obj}
|
||||
* @see #nullSafeToString(Object)
|
||||
*/
|
||||
public static String getDisplayString(Object obj) {
|
||||
if (obj == null) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
return nullSafeToString(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the class name for the given object.
|
||||
* <p>Returns a {@code "null"} String if {@code obj} is {@code null}.
|
||||
* @param obj the object to introspect (may be {@code null})
|
||||
* @return the corresponding class name
|
||||
*/
|
||||
public static String nullSafeClassName(Object obj) {
|
||||
return (obj != null ? obj.getClass().getName() : NULL_STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the specified Object.
|
||||
* <p>Builds a String representation of the contents in case of an array.
|
||||
* Returns a {@code "null"} String if {@code obj} is {@code null}.
|
||||
* @param obj the object to build a String representation for
|
||||
* @return a String representation of {@code obj}
|
||||
*/
|
||||
public static String nullSafeToString(Object obj) {
|
||||
if (obj == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
if (obj instanceof String) {
|
||||
return (String) obj;
|
||||
}
|
||||
if (obj instanceof Object[]) {
|
||||
return nullSafeToString((Object[]) obj);
|
||||
}
|
||||
if (obj instanceof boolean[]) {
|
||||
return nullSafeToString((boolean[]) obj);
|
||||
}
|
||||
if (obj instanceof byte[]) {
|
||||
return nullSafeToString((byte[]) obj);
|
||||
}
|
||||
if (obj instanceof char[]) {
|
||||
return nullSafeToString((char[]) obj);
|
||||
}
|
||||
if (obj instanceof double[]) {
|
||||
return nullSafeToString((double[]) obj);
|
||||
}
|
||||
if (obj instanceof float[]) {
|
||||
return nullSafeToString((float[]) obj);
|
||||
}
|
||||
if (obj instanceof int[]) {
|
||||
return nullSafeToString((int[]) obj);
|
||||
}
|
||||
if (obj instanceof long[]) {
|
||||
return nullSafeToString((long[]) obj);
|
||||
}
|
||||
if (obj instanceof short[]) {
|
||||
return nullSafeToString((short[]) obj);
|
||||
}
|
||||
String str = obj.toString();
|
||||
return (str != null ? str : EMPTY_STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(Object[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (Object o : array) {
|
||||
stringJoiner.add(String.valueOf(o));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(boolean[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (boolean b : array) {
|
||||
stringJoiner.add(String.valueOf(b));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(byte[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (byte b : array) {
|
||||
stringJoiner.add(String.valueOf(b));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(char[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (char c : array) {
|
||||
stringJoiner.add('\'' + String.valueOf(c) + '\'');
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(double[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (double d : array) {
|
||||
stringJoiner.add(String.valueOf(d));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(float[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (float f : array) {
|
||||
stringJoiner.add(String.valueOf(f));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(int[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (int i : array) {
|
||||
stringJoiner.add(String.valueOf(i));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(long[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (long l : array) {
|
||||
stringJoiner.add(String.valueOf(l));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a String representation of the contents of the specified array.
|
||||
* <p>The String representation consists of a list of the array's elements,
|
||||
* enclosed in curly braces ({@code "{}"}). Adjacent elements are separated
|
||||
* by the characters {@code ", "} (a comma followed by a space).
|
||||
* Returns a {@code "null"} String if {@code array} is {@code null}.
|
||||
* @param array the array to build a String representation for
|
||||
* @return a String representation of {@code array}
|
||||
*/
|
||||
public static String nullSafeToString(short[] array) {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
for (short s : array) {
|
||||
stringJoiner.add(String.valueOf(s));
|
||||
}
|
||||
return stringJoiner.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cloud.tianai.captcha.common.util;
|
||||
|
||||
public class UUIDUtils {
|
||||
|
||||
public static String getUUID() {
|
||||
return java.util.UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package cloud.tianai.captcha.generator;
|
||||
|
||||
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
|
||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CustomData;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
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 lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/22 16:30
|
||||
* @Description 抽象的验证码生成器
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractImageCaptchaGenerator implements ImageCaptchaGenerator {
|
||||
public static String DEFAULT_BG_IMAGE_TYPE = "jpeg";
|
||||
public static String DEFAULT_SLIDER_IMAGE_TYPE = "png";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
/** 默认背景图片类型. */
|
||||
public String defaultBgImageType = DEFAULT_BG_IMAGE_TYPE;
|
||||
@Getter
|
||||
@Setter
|
||||
/** 默认滑块图片类型. */
|
||||
public String defaultSliderImageType = DEFAULT_SLIDER_IMAGE_TYPE;
|
||||
|
||||
/** 资源管理器. */
|
||||
protected ImageCaptchaResourceManager imageCaptchaResourceManager;
|
||||
|
||||
/** 图片转换器. */
|
||||
protected ImageTransform imageTransform;
|
||||
|
||||
protected CaptchaInterceptor interceptor;
|
||||
|
||||
@Getter
|
||||
private boolean init = false;
|
||||
|
||||
public AbstractImageCaptchaGenerator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaGenerator init() {
|
||||
if (init) {
|
||||
return this;
|
||||
}
|
||||
init = true;
|
||||
try {
|
||||
log.info("图片验证码[{}]初始化...", this.getClass().getSimpleName());
|
||||
// 设置默认图片转换器
|
||||
if (getImageTransform() == null) {
|
||||
setImageTransform(new Base64ImageTransform());
|
||||
}
|
||||
doInit();
|
||||
} catch (Exception e) {
|
||||
init = false;
|
||||
log.error("[{}]初始化失败,ex", this.getClass().getSimpleName(), e);
|
||||
throw e;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbstractImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type) {
|
||||
return generateCaptchaImage(type, defaultBgImageType, defaultSliderImageType);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type, String backgroundFormatName, String templateFormatName) {
|
||||
return generateCaptchaImage(GenerateParam.builder()
|
||||
.type(type)
|
||||
.backgroundFormatName(backgroundFormatName)
|
||||
.templateFormatName(templateFormatName)
|
||||
.obfuscate(false)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
assertInit();
|
||||
CustomData data = new CustomData();
|
||||
CaptchaExchange captchaExchange = CaptchaExchange.create(data, param);
|
||||
ImageCaptchaInfo imageCaptchaInfo = beforeGenerate(captchaExchange);
|
||||
if (imageCaptchaInfo != null) {
|
||||
return imageCaptchaInfo;
|
||||
}
|
||||
doGenerateCaptchaImage(captchaExchange);
|
||||
beforeWrapImageCaptchaInfo(captchaExchange);
|
||||
imageCaptchaInfo = wrapImageCaptchaInfo(captchaExchange);
|
||||
afterGenerateCaptchaImage(captchaExchange, imageCaptchaInfo);
|
||||
return imageCaptchaInfo;
|
||||
}
|
||||
|
||||
protected void afterGenerateCaptchaImage(CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo) {
|
||||
if (interceptor != null) {
|
||||
interceptor.afterGenerateCaptchaImage(interceptor.createContext(), captchaExchange, imageCaptchaInfo, this);
|
||||
}
|
||||
}
|
||||
|
||||
protected void beforeWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
|
||||
if (interceptor != null) {
|
||||
interceptor.beforeWrapImageCaptchaInfo(interceptor.createContext(), captchaExchange, this);
|
||||
}
|
||||
}
|
||||
|
||||
protected ImageCaptchaInfo beforeGenerate(CaptchaExchange captchaExchange) {
|
||||
if (interceptor != null) {
|
||||
return interceptor.beforeGenerateCaptchaImage(interceptor.createContext(), captchaExchange, this);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ImageCaptchaInfo wrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
|
||||
ImageCaptchaInfo imageCaptchaInfo = doWrapImageCaptchaInfo(captchaExchange);
|
||||
imageCaptchaInfo.setData(captchaExchange.getCustomData());
|
||||
return imageCaptchaInfo;
|
||||
}
|
||||
|
||||
protected ResourceMap requiredRandomGetTemplate(String type, String tag) {
|
||||
ResourceMap templateMap = imageCaptchaResourceManager.randomGetTemplate(type, tag);
|
||||
if (templateMap == null || CollectionUtils.isEmpty(templateMap.getResourceMap())) {
|
||||
throw new ImageCaptchaException("随机获取模板资源失败, 获取到的资源为空, type=" + type + ",tag=" + tag);
|
||||
}
|
||||
return templateMap;
|
||||
}
|
||||
|
||||
protected Resource requiredRandomGetResource(String type, String tag) {
|
||||
Resource resource = imageCaptchaResourceManager.randomGetResource(type, tag);
|
||||
if (resource == null) {
|
||||
throw new ImageCaptchaException("随机获取资源失败, 获取到的资源为空, type=" + type + ",tag=" + tag);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
||||
protected InputStream getTemplateFile(ResourceMap templateImages, String imageName) {
|
||||
Resource resource = templateImages.get(imageName);
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException("查找模板异常, 该模板下未找到 ".concat(imageName));
|
||||
}
|
||||
return getResourceInputStream(resource, null);
|
||||
}
|
||||
|
||||
protected BufferedImage getTemplateImage(ResourceMap templateImages, String imageName) {
|
||||
InputStream stream = getTemplateFile(templateImages, imageName);
|
||||
BufferedImage bufferedImage = CaptchaImageUtils.wrapFile2BufferedImage(stream);
|
||||
closeStream(stream);
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
|
||||
protected BufferedImage getResourceImage(Resource resource) {
|
||||
InputStream stream = getResourceInputStream(resource, null);
|
||||
BufferedImage bufferedImage = CaptchaImageUtils.wrapFile2BufferedImage(stream);
|
||||
closeStream(stream);
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
protected int randomInt(int origin, int bound) {
|
||||
return ThreadLocalRandom.current().nextInt(origin, bound);
|
||||
}
|
||||
|
||||
protected boolean randomBoolean() {
|
||||
return ThreadLocalRandom.current().nextBoolean();
|
||||
}
|
||||
|
||||
protected int randomInt(int bound) {
|
||||
return ThreadLocalRandom.current().nextInt(bound);
|
||||
}
|
||||
|
||||
public void closeStream(InputStream stream) {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream getResourceInputStream(Resource resource, Collection<InputStream> inputStreams) {
|
||||
InputStream stream = getImageResourceManager().getResourceInputStream(resource);
|
||||
if (stream != null && inputStreams != null) {
|
||||
inputStreams.add(stream);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
protected Optional<BufferedImage> getTemplateImageOfOptional(ResourceMap templateImages, String imageName) {
|
||||
Optional<InputStream> optional = getTemplateFileOfOptional(templateImages, imageName);
|
||||
if (optional.isPresent()) {
|
||||
InputStream inputStream = optional.get();
|
||||
BufferedImage bufferedImage = CaptchaImageUtils.wrapFile2BufferedImage(inputStream);
|
||||
closeStream(inputStream);
|
||||
return Optional.ofNullable(bufferedImage);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
protected Optional<InputStream> getTemplateFileOfOptional(ResourceMap templateImages, String imageName) {
|
||||
Resource resource = templateImages.get(imageName);
|
||||
if (resource == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(getResourceInputStream(resource, null));
|
||||
}
|
||||
|
||||
protected void assertInit() {
|
||||
if (!init) {
|
||||
throw new IllegalStateException("请先调用 init(...) 初始化方法进行初始化");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
/**
|
||||
* 生成验证码方法
|
||||
*
|
||||
* @param captchaExchange captchaExchange
|
||||
* @return ImageCaptchaInfo
|
||||
*/
|
||||
protected abstract void doGenerateCaptchaImage(CaptchaExchange captchaExchange);
|
||||
|
||||
protected abstract ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange);
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return imageCaptchaResourceManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageResourceManager(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
this.imageCaptchaResourceManager = imageCaptchaResourceManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageTransform getImageTransform() {
|
||||
return imageTransform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageTransform(ImageTransform imageTransform) {
|
||||
this.imageTransform = imageTransform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaptchaInterceptor getInterceptor() {
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInterceptor(CaptchaInterceptor interceptor) {
|
||||
this.interceptor = interceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package cloud.tianai.captcha.generator;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2020/10/19 18:37
|
||||
* @Description 图片验证码生成器
|
||||
*/
|
||||
public interface ImageCaptchaGenerator {
|
||||
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*
|
||||
* @return ImageCaptchaGenerator
|
||||
*/
|
||||
ImageCaptchaGenerator init();
|
||||
|
||||
/**
|
||||
* 生成验证码图片
|
||||
*
|
||||
* @param type 类型 {@link CaptchaTypeConstant}
|
||||
* @return SliderCaptchaInfo
|
||||
*/
|
||||
ImageCaptchaInfo generateCaptchaImage(String type);
|
||||
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*
|
||||
* @param type type {@link CaptchaTypeConstant}
|
||||
* @param targetFormatName jpeg或者webp格式
|
||||
* @param matrixFormatName png或者webp格式
|
||||
* @return SliderCaptchaInfo
|
||||
*/
|
||||
ImageCaptchaInfo generateCaptchaImage(String type, String targetFormatName, String matrixFormatName);
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @param param 生成参数
|
||||
* @return SliderCaptchaInfo
|
||||
*/
|
||||
ImageCaptchaInfo generateCaptchaImage(GenerateParam param);
|
||||
|
||||
|
||||
/**
|
||||
* 获取滑块验证码资源管理器
|
||||
*
|
||||
* @return SliderCaptchaResourceManager
|
||||
*/
|
||||
ImageCaptchaResourceManager getImageResourceManager();
|
||||
|
||||
/**
|
||||
* 设置滑块验证码资源管理器
|
||||
*
|
||||
* @param imageCaptchaResourceManager
|
||||
*/
|
||||
void setImageResourceManager(ImageCaptchaResourceManager imageCaptchaResourceManager);
|
||||
|
||||
/**
|
||||
* 获取图片转换器
|
||||
*
|
||||
* @return ImageTransform
|
||||
*/
|
||||
ImageTransform getImageTransform();
|
||||
|
||||
/**
|
||||
* 设置图片转换器
|
||||
*
|
||||
* @param imageTransform imageTransform
|
||||
* @return ImageTransform
|
||||
*/
|
||||
void setImageTransform(ImageTransform imageTransform);
|
||||
|
||||
|
||||
CaptchaInterceptor getInterceptor();
|
||||
|
||||
void setInterceptor(CaptchaInterceptor interceptor);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cloud.tianai.captcha.generator;
|
||||
|
||||
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/5/19 14:45
|
||||
* @Description ImageCaptchaGenerator 提供者
|
||||
*/
|
||||
public interface ImageCaptchaGeneratorProvider {
|
||||
|
||||
/**
|
||||
* 生成/获取 ImageCaptchaGenerator
|
||||
*
|
||||
* @param resourceManager resourceManager
|
||||
* @param imageTransform imageTransform
|
||||
* @return ImageCaptchaGenerator
|
||||
*/
|
||||
ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor);
|
||||
|
||||
/**
|
||||
* 验证码类型
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
default String getType() {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cloud.tianai.captcha.generator;
|
||||
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CustomData;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageTransformData;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/8/25 10:21
|
||||
* @Description 图片转换为字符串, 扩展接口, 可以转换为文件地址等
|
||||
*/
|
||||
public interface ImageTransform {
|
||||
|
||||
/**
|
||||
* 转换
|
||||
*
|
||||
* @param backgroundImage 背景图片
|
||||
* @param param 参数
|
||||
* @param backgroundResource 背景资源对象
|
||||
* @param data 自定义透传数据
|
||||
* @return ImageTransformData
|
||||
*/
|
||||
default ImageTransformData transform(GenerateParam param, BufferedImage backgroundImage, Resource backgroundResource, CustomData data) {
|
||||
return transform(param, backgroundImage, null, backgroundResource, null, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换
|
||||
*
|
||||
* @param backgroundImage 背景图片
|
||||
* @param templateImage 模板图片(可能为空)
|
||||
* @param param 参数
|
||||
* @param backgroundResource 背景资源对象
|
||||
* @param templateResource 模板资源对象(可能为空)
|
||||
* @param data 自定义透传数据
|
||||
* @return String
|
||||
*/
|
||||
ImageTransformData transform(GenerateParam param,
|
||||
BufferedImage backgroundImage,
|
||||
BufferedImage templateImage,
|
||||
Object backgroundResource,
|
||||
Object templateResource,
|
||||
CustomData data);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cloud.tianai.captcha.generator.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FontWrapper {
|
||||
private Font font;
|
||||
private Float currentFontTopCoef;
|
||||
|
||||
public FontWrapper(Font font) {
|
||||
this(font, 70);
|
||||
}
|
||||
|
||||
public FontWrapper(Font font, int fontSize) {
|
||||
this.font = font;
|
||||
this.font = font.deriveFont(Font.BOLD, fontSize);
|
||||
}
|
||||
|
||||
public float getCurrentFontTopCoef() {
|
||||
if (currentFontTopCoef != null) {
|
||||
return currentFontTopCoef;
|
||||
}
|
||||
currentFontTopCoef = 0.14645833f * font.getSize() + 0.39583333f;
|
||||
return currentFontTopCoef;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||
import lombok.Data;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/4/24 15:02
|
||||
* @Description 传输用
|
||||
*/
|
||||
@Data
|
||||
public class CaptchaExchange {
|
||||
/** 模板对象. */
|
||||
private ResourceMap templateResource;
|
||||
/** 资源对象. */
|
||||
private Resource resourceImage;
|
||||
/** 生成好的背景图片. */
|
||||
private BufferedImage backgroundImage;
|
||||
/** 生成好的模板图片. */
|
||||
private BufferedImage templateImage;
|
||||
/** 最终要回调给验证器的自定义对象. */
|
||||
private CustomData customData;
|
||||
/** 用户传来的生成参数. */
|
||||
private GenerateParam param;
|
||||
/** 传输对象,扩展自定义. */
|
||||
private Object transferData;
|
||||
|
||||
public static CaptchaExchange create(CustomData customData, GenerateParam param) {
|
||||
CaptchaExchange captchaExchange = new CaptchaExchange();
|
||||
captchaExchange.setCustomData(customData);
|
||||
captchaExchange.setParam(param);
|
||||
return captchaExchange;
|
||||
}
|
||||
|
||||
public static CaptchaExchange create(GenerateParam param) {
|
||||
return create(new CustomData(), param);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/28 16:51
|
||||
* @Description 点击图片校验描述
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClickImageCheckDefinition {
|
||||
/** 提示. */
|
||||
private Resource tip;
|
||||
private ImgWrapper tipImage;
|
||||
/** x. */
|
||||
private Integer x;
|
||||
/** y. */
|
||||
private Integer y;
|
||||
/** 宽. */
|
||||
private Integer width;
|
||||
/** 高. */
|
||||
private Integer height;
|
||||
/** 颜色. */
|
||||
private Color imageColor;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/28 14:26
|
||||
* @Description 点击图片包装
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ImgWrapper {
|
||||
/** 图片. */
|
||||
private BufferedImage image;
|
||||
/** 提示. */
|
||||
private Resource tip;
|
||||
/** 图片颜色. */
|
||||
private Color imageColor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/4/24 10:27
|
||||
* @Description 自定义扩展数据
|
||||
*/
|
||||
@Data
|
||||
public class CustomData {
|
||||
|
||||
/** 透传字段,用于传给前端. */
|
||||
private AnyMap viewData;
|
||||
/** 内部使用的字段数据. */
|
||||
private AnyMap data;
|
||||
/**
|
||||
* 扩展字段
|
||||
*/
|
||||
public Object expand;
|
||||
|
||||
public void putViewData(String key, Object data) {
|
||||
if (this.viewData == null) {
|
||||
this.viewData = new AnyMap();
|
||||
}
|
||||
this.viewData.put(key, data);
|
||||
}
|
||||
|
||||
public void putData(String key, Object data) {
|
||||
if (this.data == null) {
|
||||
this.data = new AnyMap();
|
||||
}
|
||||
this.data.put(key, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/11 9:44
|
||||
* @Description 生成参数
|
||||
*/
|
||||
@Data
|
||||
// param作为扩展字段暂时将param从equals和toString中移除掉 以适应 CacheImageCaptchaGenerator
|
||||
@EqualsAndHashCode(exclude = "param")
|
||||
public class GenerateParam {
|
||||
|
||||
/**
|
||||
* 背景格式化类型.
|
||||
*/
|
||||
private String backgroundFormatName = "jpeg";
|
||||
/**
|
||||
* 模板图片格式化类型.
|
||||
*/
|
||||
private String templateFormatName = "png";
|
||||
/**
|
||||
* 是否混淆.
|
||||
*/
|
||||
private Boolean obfuscate = false;
|
||||
/**
|
||||
* 类型.
|
||||
*/
|
||||
private String type = CaptchaTypeConstant.SLIDER;
|
||||
/**
|
||||
* 背景图片标签, 用户二级过滤背景图片,或指定某背景图片.
|
||||
*/
|
||||
private String backgroundImageTag;
|
||||
/**
|
||||
* 滑动图片标签,用户二级过滤模板图片,或指定某模板图片..
|
||||
*/
|
||||
private String templateImageTag;
|
||||
/**
|
||||
* 扩展参数.
|
||||
*/
|
||||
private AnyMap param = new AnyMap();
|
||||
|
||||
public void addParam(String key, Object value) {
|
||||
doGetOrCreateParam().put(key, value);
|
||||
}
|
||||
|
||||
public Object getParam(String key) {
|
||||
return param == null ? null : param.get(key);
|
||||
}
|
||||
|
||||
private AnyMap doGetOrCreateParam() {
|
||||
if (param == null) {
|
||||
param = new AnyMap();
|
||||
}
|
||||
return param;
|
||||
}
|
||||
|
||||
public Object removeParam(String key) {
|
||||
if (param == null) {
|
||||
return null;
|
||||
}
|
||||
return param.remove(key);
|
||||
}
|
||||
|
||||
public Object getOrDefault(String key, Object defaultValue) {
|
||||
if (param == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return param.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
|
||||
public Object putIfAbsent(String key, Object value) {
|
||||
return doGetOrCreateParam().putIfAbsent(key, value);
|
||||
}
|
||||
|
||||
|
||||
public <T> void addParam(ParamKey<T> paramKey, T value) {
|
||||
addParam(paramKey.getKey(), value);
|
||||
}
|
||||
|
||||
public <T> T getParam(ParamKey<T> paramKey) {
|
||||
return (T) getParam(paramKey.getKey());
|
||||
}
|
||||
|
||||
public <T> T getOrDefault(ParamKey<T> paramKey, T defaultValue) {
|
||||
return (T) getOrDefault(paramKey.getKey(), defaultValue);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String backgroundFormatName = "jpeg";
|
||||
private String templateFormatName = "png";
|
||||
private Boolean obfuscate = false;
|
||||
private String type = CaptchaTypeConstant.SLIDER;
|
||||
private String backgroundImageTag;
|
||||
private String templateImageTag;
|
||||
private AnyMap param = new AnyMap();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder backgroundFormatName(String backgroundFormatName) {
|
||||
this.backgroundFormatName = backgroundFormatName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder templateFormatName(String templateFormatName) {
|
||||
this.templateFormatName = templateFormatName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder obfuscate(Boolean obfuscate) {
|
||||
this.obfuscate = obfuscate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder backgroundImageTag(String backgroundImageTag) {
|
||||
this.backgroundImageTag = backgroundImageTag;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder templateImageTag(String templateImageTag) {
|
||||
this.templateImageTag = templateImageTag;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder param(AnyMap param) {
|
||||
this.param = param;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GenerateParam build() {
|
||||
GenerateParam generateParam = new GenerateParam();
|
||||
generateParam.backgroundFormatName = backgroundFormatName;
|
||||
generateParam.templateFormatName = templateFormatName;
|
||||
generateParam.obfuscate = obfuscate;
|
||||
generateParam.type = type;
|
||||
generateParam.backgroundImageTag = backgroundImageTag;
|
||||
generateParam.templateImageTag = templateImageTag;
|
||||
generateParam.param = param;
|
||||
return generateParam;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @Date 2020/5/29 8:04
|
||||
* @Description 滑块验证码
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ImageCaptchaInfo {
|
||||
|
||||
/** 背景图. */
|
||||
private String backgroundImage;
|
||||
/** 模板图. */
|
||||
private String templateImage;
|
||||
/** 背景图片所属标签. */
|
||||
private String backgroundImageTag;
|
||||
/** 模板图片所属标签. */
|
||||
private String templateImageTag;
|
||||
/** 背景图片宽度. */
|
||||
private Integer backgroundImageWidth;
|
||||
/** 背景图片高度. */
|
||||
private Integer backgroundImageHeight;
|
||||
/** 滑块图片宽度. */
|
||||
private Integer templateImageWidth;
|
||||
/** 滑块图片高度. */
|
||||
private Integer templateImageHeight;
|
||||
/** 随机值. */
|
||||
private Integer randomX;
|
||||
/** 容错值, 可以为空 默认 0.02容错,校验的时候用. */
|
||||
private Float tolerant;
|
||||
/** 验证码类型. */
|
||||
private String type;
|
||||
private CustomData data;
|
||||
|
||||
public ImageCaptchaInfo(String backgroundImage,
|
||||
String templateImage,
|
||||
String backgroundImageTag,
|
||||
String templateImageTag,
|
||||
Integer backgroundImageWidth,
|
||||
Integer backgroundImageHeight,
|
||||
Integer templateImageWidth,
|
||||
Integer templateImageHeight,
|
||||
Integer randomX,
|
||||
String type) {
|
||||
this.backgroundImage = backgroundImage;
|
||||
this.templateImage = templateImage;
|
||||
this.backgroundImageTag = backgroundImageTag;
|
||||
this.templateImageTag = templateImageTag;
|
||||
this.backgroundImageWidth = backgroundImageWidth;
|
||||
this.backgroundImageHeight = backgroundImageHeight;
|
||||
this.templateImageWidth = templateImageWidth;
|
||||
this.templateImageHeight = templateImageHeight;
|
||||
this.randomX = randomX;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static ImageCaptchaInfo of(String backgroundImage,
|
||||
String templateImage,
|
||||
String backgroundImageTag,
|
||||
String templateImageTag,
|
||||
Integer backgroundImageWidth,
|
||||
Integer backgroundImageHeight,
|
||||
Integer templateImageWidth,
|
||||
Integer templateImageHeight,
|
||||
Integer randomX,
|
||||
String type) {
|
||||
return new ImageCaptchaInfo(backgroundImage,
|
||||
templateImage,
|
||||
backgroundImageTag,
|
||||
templateImageTag,
|
||||
backgroundImageWidth,
|
||||
backgroundImageHeight,
|
||||
templateImageWidth,
|
||||
templateImageHeight,
|
||||
randomX, type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/1/5 11:39
|
||||
* @Description 图片转换成url后的对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class ImageTransformData {
|
||||
/** 背景图. */
|
||||
private String backgroundImageUrl;
|
||||
/** 模板图. */
|
||||
private String templateImageUrl;
|
||||
/** 留一个扩展数据. */
|
||||
private Object data;
|
||||
|
||||
public ImageTransformData(String backgroundImageUrl, String templateImageUrl) {
|
||||
this.backgroundImageUrl = backgroundImageUrl;
|
||||
this.templateImageUrl = templateImageUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2024/11/20 11:34
|
||||
* @Description 此接口的作用是在给 {@link GenerateParam} 添加/获取参数时做一个类型限制和转换
|
||||
*/
|
||||
public interface ParamKey<T> {
|
||||
|
||||
String getKey();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class ParamKeyEnum<T> implements ParamKey<T> {
|
||||
|
||||
|
||||
/** 点选验证码参与校验的数量. 值为Integer */
|
||||
public static final ParamKey<Integer> CLICK_CHECK_CLICK_COUNT = new ParamKeyEnum<>("checkClickCount");
|
||||
/** 点选验证码干扰数量. 值为Integer */
|
||||
public static final ParamKey<Integer> CLICK_INTERFERENCE_COUNT = new ParamKeyEnum<>("interferenceCount");
|
||||
/** 读取字体时,可指定字体TAG,可用于给不同的验证码指定不同的字体包.*/
|
||||
public static final ParamKey<String> FONT_TAG = new ParamKeyEnum<>("fontTag");
|
||||
private String key;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/22 15:49
|
||||
* @Description 旋转图片
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class RotateImageCaptchaInfo extends ImageCaptchaInfo {
|
||||
/**
|
||||
* 旋转多少度
|
||||
*/
|
||||
private Double degree;
|
||||
/** 旋转图片的容错值大一点. */
|
||||
public static final Float DEFAULT_TOLERANT = 0.03F;
|
||||
|
||||
public static RotateImageCaptchaInfo of(Double degree,
|
||||
Integer randomX,
|
||||
String backgroundImage,
|
||||
String templateImage,
|
||||
String backgroundImageTag,
|
||||
String templateImageTag,
|
||||
Integer bgImageWidth,
|
||||
Integer bgImageHeight,
|
||||
Integer templateImageWidth,
|
||||
Integer templateImageHeight) {
|
||||
RotateImageCaptchaInfo rotateImageCaptchaInfo = new RotateImageCaptchaInfo();
|
||||
rotateImageCaptchaInfo.setDegree(degree);
|
||||
rotateImageCaptchaInfo.setRandomX(randomX);
|
||||
rotateImageCaptchaInfo.setBackgroundImage(backgroundImage);
|
||||
rotateImageCaptchaInfo.setBackgroundImageTag(backgroundImageTag);
|
||||
rotateImageCaptchaInfo.setTemplateImageTag(templateImageTag);
|
||||
rotateImageCaptchaInfo.setTolerant(DEFAULT_TOLERANT);
|
||||
rotateImageCaptchaInfo.setTemplateImage(templateImage);
|
||||
rotateImageCaptchaInfo.setBackgroundImageWidth(bgImageWidth);
|
||||
rotateImageCaptchaInfo.setBackgroundImageHeight(bgImageHeight);
|
||||
rotateImageCaptchaInfo.setTemplateImageWidth(templateImageWidth);
|
||||
rotateImageCaptchaInfo.setTemplateImageHeight(templateImageHeight);
|
||||
// 类型为旋转图片验证码
|
||||
rotateImageCaptchaInfo.setType(CaptchaTypeConstant.ROTATE);
|
||||
return rotateImageCaptchaInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cloud.tianai.captcha.generator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class SliderImageCaptchaInfo extends ImageCaptchaInfo {
|
||||
/**
|
||||
* x轴
|
||||
*/
|
||||
private Integer x;
|
||||
/**
|
||||
* y轴
|
||||
*/
|
||||
private Integer y;
|
||||
|
||||
|
||||
public static SliderImageCaptchaInfo of(Integer x,
|
||||
Integer y,
|
||||
String backgroundImage,
|
||||
String templateImage,
|
||||
String backgroundImageTag,
|
||||
String templateImageTag,
|
||||
Integer bgImageWidth,
|
||||
Integer bgImageHeight,
|
||||
Integer sliderImageWidth,
|
||||
Integer sliderImageHeight) {
|
||||
SliderImageCaptchaInfo sliderImageCaptchaInfo = new SliderImageCaptchaInfo();
|
||||
sliderImageCaptchaInfo.setX(x);
|
||||
sliderImageCaptchaInfo.setY(y);
|
||||
sliderImageCaptchaInfo.setRandomX(x);
|
||||
sliderImageCaptchaInfo.setBackgroundImage(backgroundImage);
|
||||
sliderImageCaptchaInfo.setTemplateImage(templateImage);
|
||||
sliderImageCaptchaInfo.setBackgroundImageTag(backgroundImageTag);
|
||||
sliderImageCaptchaInfo.setTemplateImageTag(templateImageTag);
|
||||
sliderImageCaptchaInfo.setBackgroundImageWidth(bgImageWidth);
|
||||
sliderImageCaptchaInfo.setBackgroundImageHeight(bgImageHeight);
|
||||
sliderImageCaptchaInfo.setTemplateImageWidth(sliderImageWidth);
|
||||
sliderImageCaptchaInfo.setTemplateImageHeight(sliderImageHeight);
|
||||
sliderImageCaptchaInfo.setType(CaptchaTypeConstant.SLIDER);
|
||||
return sliderImageCaptchaInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
package cloud.tianai.captcha.generator.common.util;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.CubicCurve2D;
|
||||
import java.awt.geom.QuadCurve2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.PixelGrabber;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/16 9:46
|
||||
* @Description image Utils
|
||||
*/
|
||||
public class CaptchaImageUtils {
|
||||
|
||||
public static final String TYPE_JPG = "jpg";
|
||||
public static final String TYPE_JPEG = "jpeg";
|
||||
public static final String TYPE_PNG = "png";
|
||||
|
||||
@SneakyThrows
|
||||
public static BufferedImage wrapFile2BufferedImage(URL resourceImage) {
|
||||
if (resourceImage == null) {
|
||||
throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空");
|
||||
}
|
||||
// 关闭磁盘缓存
|
||||
ImageIO.setUseCache(false);
|
||||
return ImageIO.read(resourceImage);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static BufferedImage wrapFile2BufferedImage(InputStream resource) {
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException("包装文件到 BufferedImage 失败, file不能为空");
|
||||
}
|
||||
// 关闭磁盘缓存
|
||||
ImageIO.setUseCache(false);
|
||||
return ImageIO.read(resource);
|
||||
}
|
||||
|
||||
public static BufferedImage createTransparentImage(int width, int height) {
|
||||
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 图片覆盖(覆盖图压缩到width*height大小,覆盖到底图上)
|
||||
*
|
||||
* @param baseBufferedImage 底图
|
||||
* @param coverBufferedImage 覆盖图
|
||||
* @param x 起始x轴
|
||||
* @param y 起始y轴
|
||||
*/
|
||||
public static void overlayImage(BufferedImage baseBufferedImage, BufferedImage coverBufferedImage,
|
||||
int x, int y) {
|
||||
// 创建Graphics2D对象,用在底图对象上绘图
|
||||
Graphics2D g2d = baseBufferedImage.createGraphics();
|
||||
// 绘制
|
||||
g2d.drawImage(coverBufferedImage, x, y, coverBufferedImage.getWidth(), coverBufferedImage.getHeight(), null);
|
||||
// 释放图形上下文使用的系统资源
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将Image图像中的透明/不透明部分转换为Shape图形
|
||||
*
|
||||
* @param img 图片信息
|
||||
* @param transparent 是否透明
|
||||
* @return Shape
|
||||
* @throws InterruptedException 异常
|
||||
*/
|
||||
public static Shape getImageShape(Image img, boolean transparent) throws InterruptedException {
|
||||
ArrayList<Integer> x = new ArrayList<>();
|
||||
ArrayList<Integer> y = new ArrayList<>();
|
||||
int width = img.getWidth(null);
|
||||
int height = img.getHeight(null);
|
||||
|
||||
// 首先获取图像所有的像素信息
|
||||
PixelGrabber pgr = new PixelGrabber(img, 0, 0, -1, -1, true);
|
||||
pgr.grabPixels();
|
||||
int[] pixels = (int[]) pgr.getPixels();
|
||||
|
||||
// 循环像素
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
// 筛选,将不透明的像素的坐标加入到坐标ArrayList x和y中
|
||||
int alpha = (pixels[i] >> 24) & 0xff;
|
||||
if (alpha != 0) {
|
||||
x.add(i % width > 0 ? i % width - 1 : 0);
|
||||
y.add(i % width == 0 ? (i == 0 ? 0 : i / width - 1) : i / width);
|
||||
}
|
||||
}
|
||||
|
||||
// 建立图像矩阵并初始化(0为透明,1为不透明)
|
||||
int[][] matrix = new int[height][width];
|
||||
for (int i = 0; i < height; i++) {
|
||||
for (int j = 0; j < width; j++) {
|
||||
matrix[i][j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 导入坐标ArrayList中的不透明坐标信息
|
||||
for (int c = 0; c < x.size(); c++) {
|
||||
matrix[y.get(c)][x.get(c)] = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 逐一水平"扫描"图像矩阵的每一行,将透明(这里也可以取不透明的)的像素生成为Rectangle,
|
||||
* 再将每一行的Rectangle通过Area类的rec对象进行合并, 最后形成一个完整的Shape图形
|
||||
*/
|
||||
Area rec = new Area();
|
||||
int temp = 0;
|
||||
//生成Shape时是1取透明区域还是取非透明区域的flag
|
||||
int flag = transparent ? 0 : 1;
|
||||
|
||||
for (int i = 0; i < height; i++) {
|
||||
for (int j = 0; j < width; j++) {
|
||||
if (matrix[i][j] == flag) {
|
||||
if (temp == 0) {
|
||||
temp = j;
|
||||
}
|
||||
} else {
|
||||
if (temp != 0) {
|
||||
rec.add(new Area(new Rectangle(temp, i, j - temp, 1)));
|
||||
temp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
temp = 0;
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过模板图片抠图(不透明部分)
|
||||
*
|
||||
* @param oriImage 源图片
|
||||
* @param templateImage 模板图片
|
||||
* @param xPos 坐标轴x
|
||||
* @param yPos 坐标轴y
|
||||
* @return BufferedImage
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static BufferedImage cutImage(BufferedImage oriImage, BufferedImage templateImage, int xPos, int yPos) {
|
||||
// 模板图像矩阵
|
||||
int bw = templateImage.getWidth(null);
|
||||
int bh = templateImage.getHeight(null);
|
||||
BufferedImage targetImage = new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB);
|
||||
// 透明色
|
||||
for (int y = 0; y < bh; y++) {
|
||||
for (int x = 0; x < bw; x++) {
|
||||
int rgb = templateImage.getRGB(x, y);
|
||||
int alpha = (rgb >> 24) & 0xff;
|
||||
// 透明度大于100才处理,过滤一下边缘过于透明的像素点
|
||||
if (alpha > 100) {
|
||||
int bgRgb = oriImage.getRGB(xPos + x, yPos + y);
|
||||
targetImage.setRGB(x, y, bgRgb);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return targetImage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 旋转图片
|
||||
*
|
||||
* @param bufferedImage
|
||||
* @param degree
|
||||
* @return
|
||||
*/
|
||||
public static BufferedImage rotateImage(final BufferedImage bufferedImage,
|
||||
final double degree) {
|
||||
// 得到图片宽度。
|
||||
int w = bufferedImage.getWidth();
|
||||
// 得到图片高度。
|
||||
int h = bufferedImage.getHeight();
|
||||
// 得到图片透明度。
|
||||
int type = bufferedImage.getColorModel().getTransparency();
|
||||
BufferedImage img;// 空的图片。
|
||||
Graphics2D graphics2d;// 空的画笔。
|
||||
(graphics2d = (img = new BufferedImage(w, h, type))
|
||||
.createGraphics()).setRenderingHint(
|
||||
RenderingHints.KEY_INTERPOLATION,
|
||||
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
// 旋转,degree是整型,度数,比如垂直90度。
|
||||
graphics2d.rotate(Math.toRadians(degree), w / 2, h / 2);
|
||||
// 从bufferedimagecopy图片至img,0,0是img的坐标。
|
||||
graphics2d.drawImage(bufferedImage, 0, 0, null);
|
||||
graphics2d.dispose();
|
||||
// 返回复制好的图片,原图片依然没有变,没有旋转,下次还可以使用。
|
||||
return img;
|
||||
}
|
||||
|
||||
public static void centerOverlayAndRotateImage(BufferedImage baseBufferedImage, BufferedImage coverBufferedImage,
|
||||
final double degree) {
|
||||
coverBufferedImage = rotateImage(coverBufferedImage, degree);
|
||||
int bw = baseBufferedImage.getWidth();
|
||||
int bh = baseBufferedImage.getHeight();
|
||||
int cw = coverBufferedImage.getWidth();
|
||||
int ch = coverBufferedImage.getHeight();
|
||||
overlayImage(baseBufferedImage, coverBufferedImage, bw / 2 - cw / 2, bh / 2 - ch / 2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 分隔图片
|
||||
*
|
||||
* @param pos 分隔点
|
||||
* @param direction true为水平方向, false为垂直方向
|
||||
* @param img 待分割的图片
|
||||
* @return BufferedImage[]
|
||||
*/
|
||||
public static BufferedImage[] splitImage(int pos, boolean direction, BufferedImage img) {
|
||||
int startImageWidth;
|
||||
int startImageHeight;
|
||||
int endImageWidth;
|
||||
int endImageHeight;
|
||||
int endScanX;
|
||||
int endScanY;
|
||||
if (direction) {
|
||||
startImageHeight = img.getHeight() - pos;
|
||||
startImageWidth = img.getWidth();
|
||||
endImageWidth = img.getWidth();
|
||||
endImageHeight = pos;
|
||||
endScanX = 0;
|
||||
endScanY = startImageHeight;
|
||||
} else {
|
||||
startImageWidth = pos;
|
||||
startImageHeight = img.getHeight();
|
||||
endImageWidth = img.getWidth() - startImageWidth;
|
||||
endImageHeight = img.getHeight();
|
||||
endScanX = pos;
|
||||
endScanY = 0;
|
||||
}
|
||||
|
||||
BufferedImage startImg = img.getSubimage(0, 0, startImageWidth, startImageHeight);
|
||||
BufferedImage endImg = img.getSubimage(endScanX, endScanY, endImageWidth, endImageHeight);
|
||||
|
||||
BufferedImage[] splitImageArr = new BufferedImage[2];
|
||||
splitImageArr[0] = startImg;
|
||||
splitImageArr[1] = endImg;
|
||||
return splitImageArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接图片
|
||||
*
|
||||
* @param direction rue为水平方向, false为垂直方向
|
||||
* @param width 拼接后图片宽度
|
||||
* @param height 拼接后图片高度
|
||||
* @param imgArr 拼接的图片数组
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage concatImage(boolean direction, int width, int height, BufferedImage... imgArr) {
|
||||
int pos = 0;
|
||||
BufferedImage newImage = new BufferedImage(width, height, imgArr[0].getColorModel().getTransparency());
|
||||
Graphics2D graphics = newImage.createGraphics();
|
||||
for (BufferedImage img : imgArr) {
|
||||
if (direction) {
|
||||
// 水平方向
|
||||
graphics.drawImage(img, pos, 0, img.getWidth(), img.getHeight(), null);
|
||||
pos += img.getWidth();
|
||||
} else {
|
||||
// 垂直方向
|
||||
graphics.drawImage(img, 0, pos, img.getWidth(), img.getHeight(), null);
|
||||
pos += img.getHeight();
|
||||
}
|
||||
}
|
||||
graphics.dispose();
|
||||
return newImage;
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public static BufferedImage drawWordImg(Color fontColor,
|
||||
String word,
|
||||
Font font,
|
||||
float fontTopCoef,
|
||||
int imgWidth,
|
||||
int imgHeight,
|
||||
float deg) {
|
||||
BufferedImage fillRect = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = fillRect.createGraphics();
|
||||
g.setColor(new Color(255, 255, 255, 0));
|
||||
g.fillRect(0, 0, imgWidth, imgHeight);
|
||||
g.setColor(fontColor);
|
||||
g.setFont(font);
|
||||
float left = (imgWidth - font.getSize()) / 2f;
|
||||
float top = (imgHeight - font.getSize()) / 2f + font.getSize() - fontTopCoef;
|
||||
g.rotate(Math.toRadians(deg), imgWidth / 2f, imgHeight / 2f);
|
||||
g.drawString(word, left, top);
|
||||
g.dispose();
|
||||
return fillRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机画干扰圆
|
||||
*
|
||||
* @param num 数量
|
||||
* @param color 颜色
|
||||
* @param g Graphics2D
|
||||
*/
|
||||
public static void drawOval(int num,
|
||||
Color color,
|
||||
Graphics2D g,
|
||||
int width,
|
||||
int height,
|
||||
Random random) {
|
||||
for (int i = 0; i < num; i++) {
|
||||
g.setColor(color == null ? getRandomColor(random) : color);
|
||||
int w = 5 + random.nextInt(10);
|
||||
int x = random.nextInt(width - 25);
|
||||
int y = random.nextInt(height - 25);
|
||||
g.drawOval(x, y, w, w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 随机画贝塞尔曲线
|
||||
*
|
||||
* @param num 数量
|
||||
* @param color 颜色
|
||||
* @param g Graphics2D
|
||||
*/
|
||||
public static void drawBesselLine(int num, Color color,
|
||||
Graphics2D g,
|
||||
int width,
|
||||
int height,
|
||||
ThreadLocalRandom random) {
|
||||
for (int i = 0; i < num; i++) {
|
||||
g.setColor(color == null ? getRandomColor(random) : color);
|
||||
int x1 = 5, y1 = random.nextInt(5, height / 2);
|
||||
int x2 = width - 5, y2 = random.nextInt(height / 2, height - 5);
|
||||
int ctrlx = random.nextInt(width / 4, width / 4 * 3);
|
||||
int ctrly = random.nextInt(5, height - 5);
|
||||
if (random.nextInt(2) == 0) {
|
||||
int ty = y1;
|
||||
y1 = y2;
|
||||
y2 = ty;
|
||||
}
|
||||
// 二阶贝塞尔曲线
|
||||
if (random.nextInt(2) == 0) {
|
||||
QuadCurve2D shape = new QuadCurve2D.Double();
|
||||
shape.setCurve(x1, y1, ctrlx, ctrly, x2, y2);
|
||||
g.draw(shape);
|
||||
} else { // 三阶贝塞尔曲线
|
||||
int ctrlx1 = random.nextInt(width / 4, width / 4 * 3);
|
||||
int ctrly1 = random.nextInt(5, height - 5);
|
||||
CubicCurve2D shape = new CubicCurve2D.Double(x1, y1, ctrlx, ctrly, ctrlx1, ctrly1, x2, y2);
|
||||
g.draw(shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简单的验证码图片
|
||||
*
|
||||
* @param data 验证码内容
|
||||
* @param font 字体包
|
||||
* @param width 验证码宽度
|
||||
* @param height 验证码高度
|
||||
* @param startX 起始X
|
||||
* @param startY 起始Y
|
||||
* @param interferenceLineNum 干扰线数量
|
||||
* @param interferencePointNum 干扰点数量
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage genSimpleImgCaptcha(String data,
|
||||
Font font,
|
||||
int width,
|
||||
int height,
|
||||
float startX,
|
||||
float startY,
|
||||
int interferenceLineNum,
|
||||
int interferencePointNum) {
|
||||
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = bufferedImage.createGraphics();
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
g.setFont(font);
|
||||
char[] chars = data.toCharArray();
|
||||
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
g.setColor(Color.gray);
|
||||
g.drawString(String.valueOf(chars[i]), startX + i * font.getSize(), startY);
|
||||
}
|
||||
// 干扰点
|
||||
if (interferencePointNum > 0) {
|
||||
drawOval(interferencePointNum, null, g, width, height, random);
|
||||
}
|
||||
if (interferencePointNum > 0) {
|
||||
g.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
|
||||
// 干扰线
|
||||
drawBesselLine(interferenceLineNum, null, g, width, height, random);
|
||||
}
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 随机获取颜色
|
||||
*
|
||||
* @return Color
|
||||
*/
|
||||
public static Color getRandomColor(Random random) {
|
||||
return new Color(
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
random.nextInt(255));
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换成指定类型的 BufferedImage
|
||||
*
|
||||
* @param image image
|
||||
* @param imageType imageType
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage toBufferedImage(Image image, String imageType) {
|
||||
final int type = TYPE_PNG.equalsIgnoreCase(imageType)
|
||||
? BufferedImage.TYPE_INT_ARGB
|
||||
: BufferedImage.TYPE_INT_RGB;
|
||||
return toBufferedImage(image, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成指定类型的 BufferedImage
|
||||
*
|
||||
* @param image image
|
||||
* @param imageType imageType
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage toBufferedImage(Image image, int imageType) {
|
||||
BufferedImage bufferedImage;
|
||||
if (image instanceof BufferedImage) {
|
||||
bufferedImage = (BufferedImage) image;
|
||||
if (imageType != bufferedImage.getType()) {
|
||||
bufferedImage = copyImage(image, imageType);
|
||||
}
|
||||
} else {
|
||||
bufferedImage = copyImage(image, imageType);
|
||||
}
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝图片
|
||||
*
|
||||
* @param img img
|
||||
* @param imageType imageType
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage copyImage(Image img, int imageType) {
|
||||
return copyImage(img, imageType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝图片
|
||||
*
|
||||
* @param img img
|
||||
* @param imageType imageType
|
||||
* @param backgroundColor backgroundColor
|
||||
* @return BufferedImage
|
||||
*/
|
||||
public static BufferedImage copyImage(Image img, int imageType, Color backgroundColor) {
|
||||
final BufferedImage bimage = new BufferedImage(img.getWidth(null), img.getHeight(null), imageType);
|
||||
final Graphics2D bGr = createGraphics(bimage, backgroundColor);
|
||||
bGr.drawImage(img, 0, 0, null);
|
||||
bGr.dispose();
|
||||
return bimage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建画板
|
||||
*
|
||||
* @param image image
|
||||
* @param color color
|
||||
* @return Graphics2D
|
||||
*/
|
||||
public static Graphics2D createGraphics(BufferedImage image, Color color) {
|
||||
final Graphics2D g = image.createGraphics();
|
||||
if (null != color) {
|
||||
// 填充背景
|
||||
g.setColor(color);
|
||||
g.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后缀是否是jpg
|
||||
*
|
||||
* @param type type
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isJpeg(String type) {
|
||||
return TYPE_JPG.equalsIgnoreCase(type) || TYPE_JPEG.equalsIgnoreCase(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后缀是否是 png
|
||||
*
|
||||
* @param type type
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isPng(String type) {
|
||||
return TYPE_PNG.equalsIgnoreCase(type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package cloud.tianai.captcha.generator.common.util;
|
||||
|
||||
import cloud.tianai.captcha.common.util.ObjectUtils;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/5/9 11:47
|
||||
* @Description 拷贝from hutool(https://gitee.com/dromara/hutool/blob/v5-master/hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java)
|
||||
* 为了不依赖更多无用包, 单独拷贝出来
|
||||
*/
|
||||
public class ImgWriter {
|
||||
|
||||
/**
|
||||
* 输出
|
||||
*
|
||||
* @param image image
|
||||
* @param imageType imageType
|
||||
* @param destImageStream destImageStream
|
||||
* @param quality quality 0~1
|
||||
* @return
|
||||
*/
|
||||
public static boolean write(Image image, String imageType, OutputStream destImageStream, float quality) {
|
||||
if (ObjectUtils.isEmpty(imageType)) {
|
||||
imageType = CaptchaImageUtils.TYPE_JPG;
|
||||
}
|
||||
ImageOutputStream imageOutputStream = transformImageOutputStream(destImageStream);
|
||||
final BufferedImage bufferedImage = CaptchaImageUtils.toBufferedImage(image, imageType);
|
||||
final ImageWriter writer = getWriter(bufferedImage, imageType);
|
||||
return write(bufferedImage, writer, imageOutputStream, quality);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出
|
||||
*
|
||||
* @param image image
|
||||
* @param writer writer
|
||||
* @param output output
|
||||
* @param quality quality
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean write(Image image, ImageWriter writer, ImageOutputStream output, float quality) {
|
||||
if (writer == null) {
|
||||
return false;
|
||||
}
|
||||
writer.setOutput(output);
|
||||
final RenderedImage renderedImage = toRenderedImage(image);
|
||||
// 设置质量
|
||||
ImageWriteParam imgWriteParams = null;
|
||||
if (quality > 0 && quality < 1) {
|
||||
imgWriteParams = writer.getDefaultWriteParam();
|
||||
if (imgWriteParams.canWriteCompressed()) {
|
||||
imgWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
imgWriteParams.setCompressionQuality(quality);
|
||||
final ColorModel colorModel = renderedImage.getColorModel();// ColorModel.getRGBdefault();
|
||||
imgWriteParams.setDestinationType(new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(16, 16)));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (null != imgWriteParams) {
|
||||
writer.write(null, new IIOImage(renderedImage, null, null), imgWriteParams);
|
||||
} else {
|
||||
writer.write(renderedImage);
|
||||
}
|
||||
output.flush();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
writer.dispose();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static RenderedImage toRenderedImage(Image img) {
|
||||
if (img instanceof RenderedImage) {
|
||||
return (RenderedImage) img;
|
||||
}
|
||||
return CaptchaImageUtils.copyImage(img, BufferedImage.TYPE_INT_RGB);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ImageWriter
|
||||
*
|
||||
* @param img img
|
||||
* @param formatName formatName
|
||||
* @return ImageWriter
|
||||
*/
|
||||
public static ImageWriter getWriter(Image img, String formatName) {
|
||||
final ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(CaptchaImageUtils.toBufferedImage(img, formatName));
|
||||
final Iterator<ImageWriter> iter = ImageIO.getImageWriters(type, formatName);
|
||||
return iter.hasNext() ? iter.next() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OutputStream 转换为 ImageOutputStream
|
||||
*
|
||||
* @param out out
|
||||
* @return ImageOutputStream
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
@SneakyThrows(IOException.class)
|
||||
public static ImageOutputStream transformImageOutputStream(OutputStream out) throws RuntimeException {
|
||||
ImageOutputStream result = ImageIO.createImageOutputStream(out);
|
||||
if (null == result) {
|
||||
throw new IllegalArgumentException("Image type is not supported!");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CommonConstant;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ClickImageCheckDefinition;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
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 lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/27 11:46
|
||||
* @Description 点选验证码 点选验证码分为点选文字和点选图标等
|
||||
*/
|
||||
public abstract class AbstractClickImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
public static final String CLICK_IMAGE_DISTORT_KEY = "clickImageDistort";
|
||||
|
||||
public AbstractClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
super(imageCaptchaResourceManager);
|
||||
}
|
||||
|
||||
public AbstractClickImageCaptchaGenerator() {
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
// 文字点选验证码不需要模板 只需要背景图
|
||||
Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
|
||||
|
||||
BufferedImage bgImage = getResourceImage(resourceImage);
|
||||
|
||||
List<ResourceMap> imgTips = randomGetClickImgTips(param);
|
||||
int allImages = imgTips.size();
|
||||
List<ClickImageCheckDefinition> clickImageCheckDefinitionList = new ArrayList<>(allImages);
|
||||
int avg = bgImage.getWidth() / allImages;
|
||||
if (allImages < imgTips.size()) {
|
||||
throw new IllegalStateException("随机生成点击图片小于请求数量, 请求生成数量=" + allImages + ",实际生成数量=" + imgTips.size());
|
||||
}
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
for (int i = 0; i < allImages; i++) {
|
||||
ResourceMap resourceMap = imgTips.get(i);
|
||||
Resource tipResource = resourceMap.get(CommonConstant.IMAGE_TIP_ICON);
|
||||
Resource clickResource = resourceMap.get(CommonConstant.IMAGE_CLICK_ICON);
|
||||
if (clickResource == null) {
|
||||
throw new IllegalStateException("随机生成点击图片失败,资源中必须包含[" + CommonConstant.IMAGE_CLICK_ICON + "]" + resourceMap);
|
||||
}
|
||||
if (tipResource == null) {
|
||||
tipResource = clickResource;
|
||||
}
|
||||
|
||||
// 随机获取点击图片
|
||||
ClickImageCheckDefinition.ImgWrapper imgWrapper = getClickImg(param, clickResource, null);
|
||||
BufferedImage image = imgWrapper.getImage();
|
||||
// 增加功能,是否需要扭曲图片
|
||||
image = obfuscateImage(image, param);
|
||||
int clickImgWidth = image.getWidth();
|
||||
int clickImgHeight = image.getHeight();
|
||||
if (i == 0) {
|
||||
// 假设每个icon的大小都是一样的, 按照宽高进行分块
|
||||
int w = clickImgWidth + clickImgWidth / 2;
|
||||
int h = clickImgHeight + clickImgHeight / 2;
|
||||
int xNum = (int) Math.floor((double) bgImage.getWidth() / w);
|
||||
int yNum = (int) Math.floor((double) bgImage.getHeight() / h);
|
||||
for (int x = 0; x < xNum; x++) {
|
||||
for (int y = 0; y < yNum; y++) {
|
||||
blocks.add(new Block(x * w + clickImgWidth / 2, clickImgWidth, y * h + clickImgHeight / 2, clickImgHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
Block block = blocks.remove(ThreadLocalRandom.current().nextInt(0, blocks.size()));
|
||||
// // 随机x
|
||||
// int randomX;
|
||||
// if (i == 0) {
|
||||
// randomX = 1;
|
||||
// } else {
|
||||
// randomX = avg * i;
|
||||
// }
|
||||
// // 随机y
|
||||
// int randomY = randomInt(10, bgImage.getHeight() - clickImgHeight);
|
||||
// 通过随机x和y 进行覆盖图片7
|
||||
CaptchaImageUtils.overlayImage(bgImage, image, block.startX, block.startY);
|
||||
ClickImageCheckDefinition clickImageCheckDefinition = new ClickImageCheckDefinition();
|
||||
clickImageCheckDefinition.setTip(tipResource);
|
||||
clickImageCheckDefinition.setTipImage(imgWrapper);
|
||||
clickImageCheckDefinition.setX(block.startX + clickImgWidth / 2);
|
||||
clickImageCheckDefinition.setY(block.startY + clickImgHeight / 2);
|
||||
clickImageCheckDefinition.setWidth(clickImgWidth);
|
||||
clickImageCheckDefinition.setHeight(clickImgHeight);
|
||||
clickImageCheckDefinition.setImageColor(imgWrapper.getImageColor());
|
||||
clickImageCheckDefinitionList.add(clickImageCheckDefinition);
|
||||
}
|
||||
List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList = filterAndSortClickImageCheckDefinition(captchaExchange, clickImageCheckDefinitionList);
|
||||
captchaExchange.setBackgroundImage(bgImage);
|
||||
captchaExchange.setTransferData(checkClickImageCheckDefinitionList);
|
||||
captchaExchange.setResourceImage(resourceImage);
|
||||
|
||||
|
||||
// // wrap
|
||||
// ImageCaptchaInfo imageCaptchaInfo = wrapClickImageCaptchaInfo(param, bgImage, checkClickImageCheckDefinitionList, resourceImage, data);
|
||||
// imageCaptchaInfo.setData(data);
|
||||
// return imageCaptchaInfo;
|
||||
|
||||
}
|
||||
|
||||
private BufferedImage obfuscateImage(BufferedImage image, GenerateParam param) {
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤并排序校验的图片点选顺序
|
||||
*
|
||||
* @param allCheckDefinitionList 总的点选图片
|
||||
* @return List<ClickImageCheckDefinition>
|
||||
*/
|
||||
protected abstract List<ClickImageCheckDefinition> filterAndSortClickImageCheckDefinition(CaptchaExchange captchaExchange, List<ClickImageCheckDefinition> allCheckDefinitionList);
|
||||
|
||||
/**
|
||||
* 随机获取一组数据用于生成随机图
|
||||
*
|
||||
* @return List<String>
|
||||
*/
|
||||
protected abstract List<ResourceMap> randomGetClickImgTips(GenerateParam param);
|
||||
|
||||
/**
|
||||
* 随机获取点击的图片
|
||||
*
|
||||
* @param tip 提示数据,根据改数据生成图片
|
||||
* @return ImgWrapper
|
||||
*/
|
||||
public abstract ClickImageCheckDefinition.ImgWrapper getClickImg(GenerateParam param, Resource tip, Color randomColor);
|
||||
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class Block {
|
||||
private int startX;
|
||||
private int width;
|
||||
private int startY;
|
||||
private int height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.util.NamedThreadFactory;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2020/10/20 9:23
|
||||
* @Description 滑块验证码缓冲器
|
||||
*/
|
||||
@Slf4j
|
||||
public class CacheImageCaptchaGenerator implements ImageCaptchaGenerator {
|
||||
|
||||
protected final ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("slider-captcha-queue"));
|
||||
protected Map<GenerateParam, ConcurrentLinkedQueue<ImageCaptchaInfo>> queueMap = new ConcurrentHashMap<>(8);
|
||||
protected Map<GenerateParam, AtomicInteger> posMap = new ConcurrentHashMap<>(8);
|
||||
protected Map<GenerateParam, Long> lastUpdateMap = new ConcurrentHashMap<>(8);
|
||||
protected ImageCaptchaGenerator target;
|
||||
protected int size;
|
||||
/** 等待时间,一般报错或者拉取为空时会休眠一段时间再试. */
|
||||
protected int waitTime = 1000;
|
||||
/** 调度器检查缓存的间隔时间. */
|
||||
protected int period = 5000;
|
||||
/** 10天内没有任何操作就删除已缓存的数据. */
|
||||
protected long expireTime = TimeUnit.DAYS.toMillis(10);
|
||||
@Getter
|
||||
@Setter
|
||||
protected boolean requiredGetCaptcha = true;
|
||||
|
||||
private boolean init = false;
|
||||
|
||||
public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size) {
|
||||
this.target = target;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size, int waitTime, int period) {
|
||||
this.target = target;
|
||||
this.size = size;
|
||||
this.waitTime = waitTime;
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size, int waitTime, int period, Long expireTime) {
|
||||
this.target = target;
|
||||
this.size = size;
|
||||
this.waitTime = waitTime;
|
||||
this.period = period;
|
||||
if (expireTime != null){
|
||||
this.expireTime = expireTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记的初始化调度器
|
||||
*/
|
||||
public void initSchedule() {
|
||||
init(size);
|
||||
}
|
||||
|
||||
private void init(int z) {
|
||||
if (init) {
|
||||
return;
|
||||
}
|
||||
this.size = z;
|
||||
// 初始化一个队列扫描
|
||||
scheduledExecutor.scheduleAtFixedRate(() -> queueMap.forEach((k, queue) -> {
|
||||
try {
|
||||
AtomicInteger pos = posMap.computeIfAbsent(k, k1 -> new AtomicInteger(0));
|
||||
int addCount = 0;
|
||||
while (pos.get() < this.size) {
|
||||
if (pos.get() >= size) {
|
||||
return;
|
||||
}
|
||||
ImageCaptchaInfo slideImageInfo = target.generateCaptchaImage(k);
|
||||
if (slideImageInfo != null) {
|
||||
boolean addStatus = queue.offer(slideImageInfo);
|
||||
addCount++;
|
||||
if (addStatus) {
|
||||
// 添加记录
|
||||
pos.incrementAndGet();
|
||||
}
|
||||
} else {
|
||||
sleep();
|
||||
}
|
||||
}
|
||||
if (addCount == 0) {
|
||||
// 没有添加,检测最新更新时间 如果时间过长,直接清除数据
|
||||
Long lastUpdate = lastUpdateMap.get(k);
|
||||
if (lastUpdate != null && System.currentTimeMillis() - lastUpdate > expireTime) {
|
||||
queueMap.remove(k);
|
||||
posMap.remove(k);
|
||||
lastUpdateMap.remove(k);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// cache所有
|
||||
log.error("缓存队列扫描时出错, ex", e);
|
||||
// 删掉它
|
||||
queueMap.remove(k);
|
||||
posMap.remove(k);
|
||||
lastUpdateMap.remove(k);
|
||||
// 休眠
|
||||
sleep();
|
||||
}
|
||||
}), 0, period, TimeUnit.MILLISECONDS);
|
||||
init = true;
|
||||
}
|
||||
|
||||
private void sleep() {
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(waitTime);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaGenerator init() {
|
||||
ImageCaptchaGenerator captchaGenerator = target.init();
|
||||
// 初始化缓存
|
||||
init(size);;
|
||||
return captchaGenerator;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type) {
|
||||
GenerateParam generateParam = new GenerateParam();
|
||||
generateParam.setType(type);
|
||||
return generateCaptchaImage(generateParam, this.requiredGetCaptcha);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam generateParam, boolean requiredGetCaptcha) {
|
||||
ConcurrentLinkedQueue<ImageCaptchaInfo> queue = queueMap.get(generateParam);
|
||||
ImageCaptchaInfo captchaInfo = null;
|
||||
if (queue != null) {
|
||||
captchaInfo = queue.poll();
|
||||
if (captchaInfo == null) {
|
||||
log.warn("滑块验证码缓存不足, genParam:{}", generateParam);
|
||||
} else {
|
||||
AtomicInteger pos = posMap.get(generateParam);
|
||||
if (pos != null) {
|
||||
pos.decrementAndGet();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
queueMap.putIfAbsent(generateParam, new ConcurrentLinkedQueue<>());
|
||||
posMap.putIfAbsent(generateParam, new AtomicInteger(0));
|
||||
}
|
||||
if (captchaInfo == null && requiredGetCaptcha) {
|
||||
// 直接生成 不走缓存
|
||||
captchaInfo = target.generateCaptchaImage(generateParam);
|
||||
}
|
||||
if (captchaInfo != null) {
|
||||
// 记录最新时间
|
||||
lastUpdateMap.put(generateParam, System.currentTimeMillis());
|
||||
}
|
||||
return captchaInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(String type, String targetFormatName, String matrixFormatName) {
|
||||
return generateCaptchaImage(GenerateParam.builder().type(type).backgroundFormatName(targetFormatName).templateFormatName(matrixFormatName).build(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
return generateCaptchaImage(param, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaResourceManager getImageResourceManager() {
|
||||
return target.getImageResourceManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageResourceManager(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
target.setImageResourceManager(imageCaptchaResourceManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageTransform getImageTransform() {
|
||||
return target.getImageTransform();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageTransform(ImageTransform imageTransform) {
|
||||
target.setImageTransform(imageTransform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaptchaInterceptor getInterceptor() {
|
||||
return target.getInterceptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInterceptor(CaptchaInterceptor interceptor) {
|
||||
target.setInterceptor(interceptor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.util.ObjectUtils;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGeneratorProvider;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.impl.provider.CommonImageCaptchaGeneratorProvider;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static cloud.tianai.captcha.common.constant.CaptchaTypeConstant.*;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/24 9:27
|
||||
* @Description 根据type 匹配对应的验证码生成器
|
||||
*/
|
||||
public class MultiImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
protected Map<String, ImageCaptchaGenerator> imageCaptchaGeneratorMap = new ConcurrentHashMap<>(4);
|
||||
protected Map<String, ImageCaptchaGeneratorProvider> imageCaptchaGeneratorProviderMap = new HashMap<>(4);
|
||||
// 点选类验证码字体
|
||||
// @Setter
|
||||
// @Getter
|
||||
// protected List<FontWrapper> fontWrappers;
|
||||
@Setter
|
||||
@Getter
|
||||
private String defaultCaptcha = SLIDER;
|
||||
|
||||
public MultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
super(imageCaptchaResourceManager);
|
||||
}
|
||||
|
||||
public MultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 滑块验证码
|
||||
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SLIDER, StandardSliderImageCaptchaGenerator::new));
|
||||
// 旋转验证码
|
||||
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(ROTATE, StandardRotateImageCaptchaGenerator::new));
|
||||
// 拼接验证码
|
||||
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CONCAT, StandardConcatImageCaptchaGenerator::new));
|
||||
// 点选文字验证码
|
||||
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(WORD_IMAGE_CLICK, StandardWordClickImageCaptchaGenerator::new));
|
||||
}
|
||||
|
||||
public void addImageCaptchaGeneratorProvider(ImageCaptchaGeneratorProvider provider) {
|
||||
imageCaptchaGeneratorProviderMap.put(provider.getType(), provider);
|
||||
}
|
||||
|
||||
public ImageCaptchaGeneratorProvider removeImageCaptchaGeneratorProvider(String type) {
|
||||
return imageCaptchaGeneratorProviderMap.remove(type);
|
||||
}
|
||||
|
||||
public ImageCaptchaGeneratorProvider getImageCaptchaGeneratorProvider(String type) {
|
||||
return imageCaptchaGeneratorProviderMap.get(type);
|
||||
}
|
||||
|
||||
public void addImageCaptchaGenerator(String key, ImageCaptchaGenerator captchaGenerator) {
|
||||
imageCaptchaGeneratorMap.put(key, captchaGenerator);
|
||||
}
|
||||
|
||||
public ImageCaptchaGenerator removeImageCaptchaGenerator(String key) {
|
||||
return imageCaptchaGeneratorMap.remove(key);
|
||||
}
|
||||
|
||||
public ImageCaptchaGenerator getImageCaptchaGenerator(String key) {
|
||||
return imageCaptchaGeneratorMap.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
|
||||
String type = param.getType();
|
||||
if (ObjectUtils.isEmpty(type)) {
|
||||
param.setType(defaultCaptcha);
|
||||
type = defaultCaptcha;
|
||||
}
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = requireGetCaptchaGenerator(type);
|
||||
return imageCaptchaGenerator.generateCaptchaImage(param);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ImageCaptchaGenerator requireGetCaptchaGenerator(String type) {
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = imageCaptchaGeneratorMap.computeIfAbsent(type, t -> {
|
||||
ImageCaptchaGeneratorProvider provider = imageCaptchaGeneratorProviderMap.get(t);
|
||||
if (provider == null) {
|
||||
throw new IllegalArgumentException("生成验证码失败,错误的type类型:" + t);
|
||||
}
|
||||
return provider.get(getImageResourceManager(), getImageTransform(), getInterceptor()).init();
|
||||
});
|
||||
return imageCaptchaGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageResourceManager(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
super.setImageResourceManager(imageCaptchaResourceManager);
|
||||
for (ImageCaptchaGenerator imageCaptchaGenerator : imageCaptchaGeneratorMap.values()) {
|
||||
imageCaptchaGenerator.setImageResourceManager(imageCaptchaResourceManager);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageTransform(ImageTransform imageTransform) {
|
||||
super.setImageTransform(imageTransform);
|
||||
for (ImageCaptchaGenerator imageCaptchaGenerator : imageCaptchaGeneratorMap.values()) {
|
||||
imageCaptchaGenerator.setImageTransform(imageTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.*;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.concatImage;
|
||||
import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.splitImage;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/25 15:44
|
||||
* @Description 图片拼接滑动验证码生成器
|
||||
*/
|
||||
public class StandardConcatImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
super(imageCaptchaResourceManager);
|
||||
}
|
||||
|
||||
public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
}
|
||||
|
||||
public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
setInterceptor(interceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
// 拼接验证码不需要模板 只需要背景图
|
||||
Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
|
||||
BufferedImage bgImage = getResourceImage(resourceImage);
|
||||
int spacingY = bgImage.getHeight() / 4;
|
||||
int randomY = randomInt(spacingY, bgImage.getHeight() - spacingY);
|
||||
BufferedImage[] bgImageSplit = splitImage(randomY, true, bgImage);
|
||||
int spacingX = bgImage.getWidth() / 8;
|
||||
int randomX = randomInt(spacingX, bgImage.getWidth() - bgImage.getWidth() / 5);
|
||||
BufferedImage[] bgImageTopSplit = splitImage(randomX, false, bgImageSplit[0]);
|
||||
|
||||
BufferedImage sliderImage = concatImage(true,
|
||||
bgImageTopSplit[0].getWidth()
|
||||
+ bgImageTopSplit[1].getWidth(), bgImageTopSplit[0].getHeight(), bgImageTopSplit[1], bgImageTopSplit[0]);
|
||||
bgImage = concatImage(false, bgImageSplit[1].getWidth(), sliderImage.getHeight() + bgImageSplit[1].getHeight(),
|
||||
sliderImage, bgImageSplit[1]);
|
||||
Data data = new Data();
|
||||
data.x = randomX;
|
||||
data.y = randomY;
|
||||
|
||||
captchaExchange.setTransferData(data);
|
||||
captchaExchange.setBackgroundImage(bgImage);
|
||||
captchaExchange.setResourceImage(resourceImage);
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
int x;
|
||||
int y;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
BufferedImage bgImage = captchaExchange.getBackgroundImage();
|
||||
Resource resourceImage = captchaExchange.getResourceImage();
|
||||
CustomData customData = captchaExchange.getCustomData();
|
||||
ImageTransformData transform = getImageTransform().transform(param, bgImage, resourceImage, customData);
|
||||
Data data = (Data) captchaExchange.getTransferData();
|
||||
ImageCaptchaInfo imageCaptchaInfo = ImageCaptchaInfo.of(transform.getBackgroundImageUrl(),
|
||||
null,
|
||||
resourceImage.getTag(),
|
||||
null,
|
||||
bgImage.getWidth(),
|
||||
bgImage.getHeight(),
|
||||
null,
|
||||
null,
|
||||
data.x,
|
||||
CaptchaTypeConstant.CONCAT);
|
||||
customData.putViewData("randomY", data.y);
|
||||
imageCaptchaInfo.setTolerant(0.05F);
|
||||
return imageCaptchaInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.*;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
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 lombok.SneakyThrows;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/22 16:43
|
||||
* @Description 旋转图片验证码生成器
|
||||
*/
|
||||
public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
/** 模板滑块固定名称. */
|
||||
public static String TEMPLATE_ACTIVE_IMAGE_NAME = "active.png";
|
||||
/** 模板凹槽固定名称. */
|
||||
public static String TEMPLATE_FIXED_IMAGE_NAME = "fixed.png";
|
||||
/** 模板蒙版. */
|
||||
public static String TEMPLATE_MASK_IMAGE_NAME = "mask.png";
|
||||
|
||||
public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
super(imageCaptchaResourceManager);
|
||||
}
|
||||
|
||||
public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
}
|
||||
|
||||
public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
setInterceptor(interceptor);
|
||||
}
|
||||
@Override
|
||||
protected void doInit() {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
CustomData data = new CustomData();
|
||||
ResourceMap templateResource = requiredRandomGetTemplate(param.getType(), param.getTemplateImageTag());
|
||||
Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
|
||||
BufferedImage background = getResourceImage(resourceImage);
|
||||
|
||||
BufferedImage fixedTemplate = getTemplateImage(templateResource, TEMPLATE_FIXED_IMAGE_NAME);
|
||||
BufferedImage activeTemplate = getTemplateImage(templateResource, TEMPLATE_ACTIVE_IMAGE_NAME);
|
||||
BufferedImage maskTemplate = fixedTemplate;
|
||||
Optional<BufferedImage> maskTemplateOptional = getTemplateImageOfOptional(templateResource, TEMPLATE_MASK_IMAGE_NAME);
|
||||
if (maskTemplateOptional.isPresent()) {
|
||||
maskTemplate = maskTemplateOptional.get();
|
||||
}
|
||||
|
||||
// 算出居中的x和y
|
||||
int x = background.getWidth() / 2 - fixedTemplate.getWidth() / 2;
|
||||
int y = background.getHeight() / 2 - fixedTemplate.getHeight() / 2;
|
||||
|
||||
// 抠图部分
|
||||
BufferedImage cutImage = CaptchaImageUtils.cutImage(background, maskTemplate, x, y);
|
||||
BufferedImage rotateFixed = fixedTemplate;
|
||||
BufferedImage rotateActive = activeTemplate;
|
||||
if (param.getObfuscate()) {
|
||||
int randomDegree = randomInt(10, 350);
|
||||
rotateFixed = CaptchaImageUtils.rotateImage(fixedTemplate, randomDegree);
|
||||
randomDegree = randomInt(10, 350);
|
||||
rotateActive = CaptchaImageUtils.rotateImage(activeTemplate, randomDegree);
|
||||
}
|
||||
CaptchaImageUtils.overlayImage(background, rotateFixed, x, y);
|
||||
CaptchaImageUtils.overlayImage(cutImage, rotateActive, 0, 0);
|
||||
// 随机旋转抠图部分
|
||||
// 随机x, 转换为角度
|
||||
int randomX = randomInt(fixedTemplate.getWidth() + 10, background.getWidth() - 10);
|
||||
double degree = 360d - randomX / ((background.getWidth()) / 360d);
|
||||
// 旋转的透明图片是一张正方形的
|
||||
BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(cutImage.getWidth(), background.getHeight());
|
||||
CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree);
|
||||
|
||||
RotateData rotateData = new RotateData();
|
||||
rotateData.degree = degree;
|
||||
rotateData.randomX = randomX;
|
||||
captchaExchange.setTransferData(rotateData);
|
||||
captchaExchange.setBackgroundImage(background);
|
||||
captchaExchange.setTemplateImage(matrixTemplate);
|
||||
captchaExchange.setTemplateResource(templateResource);
|
||||
captchaExchange.setResourceImage(resourceImage);
|
||||
|
||||
// return wrapRotateCaptchaInfo(degree, randomX, background, matrixTemplate, param, templateResource, resourceImage, data);
|
||||
}
|
||||
|
||||
public static class RotateData {
|
||||
double degree;
|
||||
int randomX;
|
||||
}
|
||||
|
||||
private String getObfuscateTag(String templateTag) {
|
||||
if (templateTag == null) {
|
||||
return "obfuscate";
|
||||
}
|
||||
return templateTag + "_" + "obfuscate";
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
BufferedImage backgroundImage = captchaExchange.getBackgroundImage();
|
||||
BufferedImage sliderImage = captchaExchange.getTemplateImage();
|
||||
Resource resourceImage = captchaExchange.getResourceImage();
|
||||
ResourceMap templateResource = captchaExchange.getTemplateResource();
|
||||
CustomData data = captchaExchange.getCustomData();
|
||||
RotateData rotateData = (RotateData) captchaExchange.getTransferData();
|
||||
ImageTransformData transform = getImageTransform().transform(param, backgroundImage, sliderImage, resourceImage, templateResource, data);
|
||||
RotateImageCaptchaInfo imageCaptchaInfo = RotateImageCaptchaInfo.of(rotateData.degree,
|
||||
rotateData.randomX,
|
||||
transform.getBackgroundImageUrl(),
|
||||
transform.getTemplateImageUrl(),
|
||||
resourceImage.getTag(),
|
||||
templateResource.getTag(),
|
||||
backgroundImage.getWidth(), backgroundImage.getHeight(),
|
||||
sliderImage.getWidth(), sliderImage.getHeight()
|
||||
);
|
||||
imageCaptchaInfo.setData(data);
|
||||
return imageCaptchaInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.*;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
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 lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @Date 2020/5/29 8:06
|
||||
* @Description 滑块验证码模板
|
||||
*/
|
||||
@Slf4j
|
||||
public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
|
||||
|
||||
|
||||
/** 模板滑块固定名称. */
|
||||
public static String TEMPLATE_ACTIVE_IMAGE_NAME = "active.png";
|
||||
/** 模板凹槽固定名称. */
|
||||
public static String TEMPLATE_FIXED_IMAGE_NAME = "fixed.png";
|
||||
/** 模板蒙版. */
|
||||
public static String TEMPLATE_MASK_IMAGE_NAME = "mask.png";
|
||||
/** 混淆的凹槽. */
|
||||
public static String OBFUSCATE_TEMPLATE_FIXED_IMAGE_NAME = "obfuscate_" + TEMPLATE_FIXED_IMAGE_NAME;
|
||||
|
||||
|
||||
public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
super(imageCaptchaResourceManager);
|
||||
}
|
||||
|
||||
public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
}
|
||||
|
||||
public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
setInterceptor(interceptor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
Boolean obfuscate = param.getObfuscate();
|
||||
ResourceMap templateResource = requiredRandomGetTemplate(param.getType(), param.getTemplateImageTag());
|
||||
Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
|
||||
BufferedImage background = getResourceImage(resourceImage);
|
||||
BufferedImage fixedTemplate = getTemplateImage(templateResource, TEMPLATE_FIXED_IMAGE_NAME);
|
||||
BufferedImage activeTemplate = getTemplateImage(templateResource, TEMPLATE_ACTIVE_IMAGE_NAME);
|
||||
BufferedImage maskTemplate = fixedTemplate;
|
||||
Optional<BufferedImage> maskTemplateOptional = getTemplateImageOfOptional(templateResource, TEMPLATE_MASK_IMAGE_NAME);
|
||||
if (maskTemplateOptional.isPresent()) {
|
||||
maskTemplate = maskTemplateOptional.get();
|
||||
}
|
||||
// 获取随机的 x 和 y 轴
|
||||
int randomX = randomInt(fixedTemplate.getWidth() + 5, background.getWidth() - fixedTemplate.getWidth() - 10);
|
||||
int randomY = randomInt(background.getHeight() - fixedTemplate.getHeight());
|
||||
|
||||
BufferedImage cutImage = CaptchaImageUtils.cutImage(background, maskTemplate, randomX, randomY);
|
||||
CaptchaImageUtils.overlayImage(background, fixedTemplate, randomX, randomY);
|
||||
if (obfuscate) {
|
||||
Optional<BufferedImage> obfuscateFixedTemplate = getTemplateImageOfOptional(templateResource, OBFUSCATE_TEMPLATE_FIXED_IMAGE_NAME);
|
||||
BufferedImage obfuscateImage = obfuscateFixedTemplate.orElseGet(() -> createObfuscate(fixedTemplate));
|
||||
int obfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), background.getWidth());
|
||||
CaptchaImageUtils.overlayImage(background, obfuscateImage, obfuscateX, randomY);
|
||||
}
|
||||
CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0);
|
||||
// 这里创建一张png透明图片
|
||||
BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(activeTemplate.getWidth(), background.getHeight());
|
||||
CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY);
|
||||
|
||||
captchaExchange.setBackgroundImage(background);
|
||||
captchaExchange.setTemplateImage(matrixTemplate);
|
||||
captchaExchange.setTemplateResource(templateResource);
|
||||
captchaExchange.setResourceImage(resourceImage);
|
||||
captchaExchange.setTransferData(new Point(randomX,randomY));
|
||||
// 后处理
|
||||
// applyPostProcessorBeforeWrapImageCaptchaInfo(captchaExchange, this);
|
||||
// imageCaptchaInfo = wrapSliderCaptchaInfo(randomX, randomY, captchaExchange);
|
||||
// applyPostProcessorAfterGenerateCaptchaImage(captchaExchange, imageCaptchaInfo, this);
|
||||
// return imageCaptchaInfo;
|
||||
}
|
||||
|
||||
protected BufferedImage createObfuscate(BufferedImage fixedImage) {
|
||||
// 随机拉伸或缩放宽高, 每次只拉伸高或者宽
|
||||
int width = fixedImage.getWidth();
|
||||
int height = fixedImage.getHeight();
|
||||
int window = randomInt(-3, 4);
|
||||
if (randomBoolean()) {
|
||||
height = height + window * 5;
|
||||
} else {
|
||||
width = width + window * 5;
|
||||
}
|
||||
int type = fixedImage.getColorModel().getTransparency();
|
||||
BufferedImage image = new BufferedImage(width, height, type);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
// 透明度
|
||||
double alpha = ThreadLocalRandom.current().nextDouble(0.5, 0.8);
|
||||
AlphaComposite alphaComposite = AlphaComposite.Src.derive((float) alpha);
|
||||
graphics.setComposite(alphaComposite);
|
||||
graphics.drawImage(fixedImage, 0, 0, width, height, null);
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public SliderImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
BufferedImage backgroundImage = captchaExchange.getBackgroundImage();
|
||||
BufferedImage sliderImage = captchaExchange.getTemplateImage();
|
||||
Resource resourceImage = captchaExchange.getResourceImage();
|
||||
ResourceMap templateResource = captchaExchange.getTemplateResource();
|
||||
CustomData customData = captchaExchange.getCustomData();
|
||||
Point data = (Point) captchaExchange.getTransferData();
|
||||
ImageTransformData transform = getImageTransform().transform(param, backgroundImage, sliderImage, resourceImage, templateResource, customData);
|
||||
|
||||
SliderImageCaptchaInfo imageCaptchaInfo = SliderImageCaptchaInfo.of(data.x, data.y,
|
||||
transform.getBackgroundImageUrl(),
|
||||
transform.getTemplateImageUrl(),
|
||||
resourceImage.getTag(),
|
||||
templateResource.getTag(),
|
||||
backgroundImage.getWidth(), backgroundImage.getHeight(),
|
||||
sliderImage.getWidth(), sliderImage.getHeight()
|
||||
);
|
||||
imageCaptchaInfo.setData(customData);
|
||||
return imageCaptchaInfo;
|
||||
}
|
||||
|
||||
protected int randomObfuscateX(int sliderX, int slWidth, int bgWidth) {
|
||||
if (bgWidth / 2 > (sliderX + (slWidth / 2))) {
|
||||
// 右边混淆
|
||||
return randomInt(sliderX + slWidth, bgWidth - slWidth);
|
||||
}
|
||||
// 左边混淆
|
||||
return randomInt(slWidth, sliderX - slWidth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package cloud.tianai.captcha.generator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.constant.CommonConstant;
|
||||
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
|
||||
import cloud.tianai.captcha.common.util.FontUtils;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.FontWrapper;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.*;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.FontCache;
|
||||
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 lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/27 11:46
|
||||
* @Description 点选验证码
|
||||
*/
|
||||
public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCaptchaGenerator {
|
||||
|
||||
/** 字体包. */
|
||||
// @Getter
|
||||
// @Setter
|
||||
// protected List<FontWrapper> fonts = new ArrayList<>();
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer clickImgWidth = 100;
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer clickImgHeight = 100;
|
||||
@Getter
|
||||
@Setter
|
||||
protected int tipImageInterferenceLineNum = 2;
|
||||
@Getter
|
||||
@Setter
|
||||
protected int tipImageInterferencePointNum = 5;
|
||||
/** 参与校验的数量. */
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer checkClickCount = 4;
|
||||
/** 干扰数量. */
|
||||
@Getter
|
||||
@Setter
|
||||
protected Integer interferenceCount = 2;
|
||||
|
||||
/**
|
||||
* 因为在画文字图形的时候 y 值不能准确通过 除法计算得出, 字体大小不一致中间的容错值算不准确
|
||||
* 方案: 通过 线性回归模型 计算出 intercept和coef 用于计算 容错值
|
||||
* 训练数据为 宋体 字体大小为 30~150 随机选择7组数据进行训练, 训练后r2结果为 0.9967106324620846
|
||||
*/
|
||||
// protected float intercept = 0.39583333f;
|
||||
// protected float coef = 0.14645833f;
|
||||
//
|
||||
// protected float currentFontTopCoef = 0.0f;
|
||||
public StandardWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
|
||||
this(imageCaptchaResourceManager, null, null);
|
||||
}
|
||||
|
||||
|
||||
public StandardWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
|
||||
super(imageCaptchaResourceManager);
|
||||
setImageTransform(imageTransform);
|
||||
setInterceptor(interceptor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<ResourceMap> randomGetClickImgTips(GenerateParam param) {
|
||||
Integer checkClickCount = param.getOrDefault(ParamKeyEnum.CLICK_CHECK_CLICK_COUNT, getCheckClickCount());
|
||||
Integer interferenceCount = param.getOrDefault(ParamKeyEnum.CLICK_INTERFERENCE_COUNT, getInterferenceCount());
|
||||
int tipSize = interferenceCount + checkClickCount;
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
List<ResourceMap> tipList = new ArrayList<>(tipSize);
|
||||
for (int i = 0; i < tipSize; i++) {
|
||||
String randomWord = FontUtils.getRandomChar(random);
|
||||
ResourceMap resourceMap = new ResourceMap(param.getTemplateImageTag());
|
||||
resourceMap.put(CommonConstant.IMAGE_TIP_ICON, new Resource(null, randomWord));
|
||||
resourceMap.put(CommonConstant.IMAGE_CLICK_ICON, new Resource(null, randomWord));
|
||||
tipList.add(resourceMap);
|
||||
}
|
||||
// 随机文字
|
||||
return tipList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// if (CollectionUtils.isEmpty(fonts)) {
|
||||
// throw new ImageCaptchaException("初始化文字点选验证码失败,请设置字体包后再调用init()");
|
||||
// }
|
||||
// ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore();
|
||||
// // 添加一些系统的资源文件
|
||||
// resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg"), DEFAULT_TAG));
|
||||
}
|
||||
|
||||
public FontWrapper randomFont(GenerateParam param) {
|
||||
String fontTag = param.getOrDefault(ParamKeyEnum.FONT_TAG, CommonConstant.DEFAULT_TAG);
|
||||
Resource resource = requiredRandomGetResource(FontCache.FONT_TYPE, fontTag);
|
||||
Object extra = resource.getExtra();
|
||||
if (extra instanceof FontWrapper) {
|
||||
return (FontWrapper) extra;
|
||||
}
|
||||
throw new ImageCaptchaException("随机获取字体失败, resource中没有读到字体包, resource=" + resource);
|
||||
}
|
||||
|
||||
public ClickImageCheckDefinition.ImgWrapper genTipImage(List<ClickImageCheckDefinition> imageCheckDefinitions, GenerateParam param) {
|
||||
FontWrapper fontWrapper = randomFont(param);
|
||||
Font font = fontWrapper.getFont();
|
||||
float currentFontTopCoef = fontWrapper.getCurrentFontTopCoef();
|
||||
String tips = imageCheckDefinitions.stream().map(c -> c.getTip().getData()).collect(Collectors.joining());
|
||||
// 生成随机颜色
|
||||
int fontWidth = tips.length() * font.getSize();
|
||||
int width = fontWidth + 6;
|
||||
int height = font.getSize() + 6;
|
||||
float left = (width - fontWidth) / 2f;
|
||||
float top = 6 / 2f + font.getSize() - currentFontTopCoef;
|
||||
BufferedImage bufferedImage = CaptchaImageUtils.genSimpleImgCaptcha(tips,
|
||||
font, width, height, left, top, tipImageInterferenceLineNum, tipImageInterferencePointNum);
|
||||
return new ClickImageCheckDefinition.ImgWrapper(bufferedImage, new Resource(null, tips), null);
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public ImgWrapper getClickImg(Resource tip) {
|
||||
// ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
// // 随机颜色
|
||||
// Color randomColor = CaptchaImageUtils.getRandomColor(random);
|
||||
// return getClickImg(tip, randomColor);
|
||||
// }
|
||||
|
||||
|
||||
@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
|
||||
protected List<ClickImageCheckDefinition> filterAndSortClickImageCheckDefinition(CaptchaExchange captchaExchange, List<ClickImageCheckDefinition> allCheckDefinitionList) {
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
Integer checkClickCount = param.getOrDefault(ParamKeyEnum.CLICK_CHECK_CLICK_COUNT, getCheckClickCount());
|
||||
// 打乱
|
||||
Collections.shuffle(allCheckDefinitionList);
|
||||
// 拿出参与校验的数据
|
||||
List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList = new ArrayList<>(checkClickCount);
|
||||
for (int i = 0; i < checkClickCount; i++) {
|
||||
ClickImageCheckDefinition clickImageCheckDefinition = allCheckDefinitionList.get(i);
|
||||
checkClickImageCheckDefinitionList.add(clickImageCheckDefinition);
|
||||
}
|
||||
return checkClickImageCheckDefinitionList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
|
||||
List<ClickImageCheckDefinition> checkClickImageCheckDefinitionList = (List<ClickImageCheckDefinition>) captchaExchange.getTransferData();
|
||||
BufferedImage bgImage = captchaExchange.getBackgroundImage();
|
||||
GenerateParam param = captchaExchange.getParam();
|
||||
Resource resourceImage = captchaExchange.getResourceImage();
|
||||
CustomData data = captchaExchange.getCustomData();
|
||||
// 提示图片
|
||||
BufferedImage tipImage = genTipImage(checkClickImageCheckDefinitionList, param).getImage();
|
||||
ImageTransformData transform = getImageTransform().transform(param, bgImage, tipImage, resourceImage, checkClickImageCheckDefinitionList, data);
|
||||
ImageCaptchaInfo clickImageCaptchaInfo = new ImageCaptchaInfo();
|
||||
clickImageCaptchaInfo.setBackgroundImage(transform.getBackgroundImageUrl());
|
||||
clickImageCaptchaInfo.setBackgroundImageTag(resourceImage.getTag());
|
||||
clickImageCaptchaInfo.setTemplateImage(transform.getTemplateImageUrl());
|
||||
clickImageCaptchaInfo.setBackgroundImageWidth(bgImage.getWidth());
|
||||
clickImageCaptchaInfo.setBackgroundImageHeight(bgImage.getHeight());
|
||||
clickImageCaptchaInfo.setTemplateImageWidth(tipImage.getWidth());
|
||||
clickImageCaptchaInfo.setTemplateImageHeight(tipImage.getHeight());
|
||||
clickImageCaptchaInfo.setRandomX(null);
|
||||
clickImageCaptchaInfo.setTolerant(null);
|
||||
clickImageCaptchaInfo.setType(CaptchaTypeConstant.WORD_IMAGE_CLICK);
|
||||
data.setExpand(checkClickImageCheckDefinitionList);
|
||||
return clickImageCaptchaInfo;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cloud.tianai.captcha.generator.impl.provider;
|
||||
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGeneratorProvider;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
|
||||
public class CommonImageCaptchaGeneratorProvider implements ImageCaptchaGeneratorProvider {
|
||||
|
||||
private String type;
|
||||
private ImageCaptchaGeneratorProvider provider;
|
||||
|
||||
public CommonImageCaptchaGeneratorProvider(String type, ImageCaptchaGeneratorProvider provider) {
|
||||
this.type = type;
|
||||
this.provider = provider;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
|
||||
return provider.get(resourceManager, imageTransform,interceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cloud.tianai.captcha.generator.impl.transform;
|
||||
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CustomData;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageTransformData;
|
||||
import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
|
||||
import cloud.tianai.captcha.generator.common.util.ImgWriter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/8/25 10:28
|
||||
* @Description base64 实现
|
||||
*/
|
||||
public class Base64ImageTransform implements ImageTransform {
|
||||
|
||||
@SneakyThrows(IOException.class)
|
||||
public String transform(BufferedImage bufferedImage, String transformType) {
|
||||
// 这里判断处理一下,加一些警告日志
|
||||
String result = beforeTransform(bufferedImage, transformType);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
if (CaptchaImageUtils.isPng(transformType) || CaptchaImageUtils.isJpeg(transformType)) {
|
||||
// 如果是 jpg 或者 png图片的话 用hutool的生成
|
||||
ImgWriter.write(bufferedImage, transformType, byteArrayOutputStream, -1);
|
||||
} else {
|
||||
ImageIO.write(bufferedImage, transformType, byteArrayOutputStream);
|
||||
}
|
||||
//转换成字节码
|
||||
byte[] data = byteArrayOutputStream.toByteArray();
|
||||
String base64 = Base64.getEncoder().encodeToString(data);
|
||||
return "data:image/" + transformType + ";base64,".concat(base64);
|
||||
}
|
||||
|
||||
public String beforeTransform(BufferedImage bufferedImage, String formatType) {
|
||||
// int type = bufferedImage.getType();
|
||||
// if (BufferedImage.TYPE_4BYTE_ABGR == type) {
|
||||
// // png , 如果转换的是jpg的话
|
||||
// if (CaptchaImageUtils.isJpeg(formatType)) {
|
||||
// // bufferedImage为 png, 但是转换的图片为 jpg
|
||||
// if (log.isWarnEnabled()) {
|
||||
// log.warn("图片验证码转换警告, 原图为 png格式时,指定转换的图片为jpg格式时可能会导致转换异常,如果转换的图片为出现错误,请设置指定转换的类型与原图的类型一致");
|
||||
// } else {
|
||||
// System.err.println("图片验证码转换警告, 原图为 png格式时,指定转换的图片为jpg格式时可能会导致转换异常,如果转换的图片为出现错误,请设置指定转换的类型与原图的类型一致");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// 其它的暂时不考虑
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageTransformData transform(GenerateParam param, BufferedImage backgroundImage, BufferedImage templateImage, Object backgroundResource, Object templateResource, CustomData data) {
|
||||
ImageTransformData imageTransformData = new ImageTransformData();
|
||||
if (backgroundImage != null) {
|
||||
imageTransformData.setBackgroundImageUrl(transform(backgroundImage, param.getBackgroundFormatName()));
|
||||
}
|
||||
if (templateImage != null) {
|
||||
imageTransformData.setTemplateImageUrl(transform(templateImage, param.getTemplateFormatName()));
|
||||
}
|
||||
return imageTransformData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cloud.tianai.captcha.interceptor;
|
||||
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
|
||||
// ============================ 拦截器执行顺序 ============================
|
||||
|
||||
// =================== 生成验证码 ===================
|
||||
// beforeGenerateCaptcha(...) ↓
|
||||
// beforeGenerateCaptchaImage(...) ↓
|
||||
// beforeWrapImageCaptchaInfo(...) ↓
|
||||
// afterGenerateCaptchaImage(...) ↓
|
||||
// beforeGenerateImageCaptchaValidData(...) ↓
|
||||
// afterGenerateImageCaptchaValidData(...) ↓
|
||||
// afterGenerateCaptcha(...) ↓
|
||||
// =================== 验证码校验 ===================
|
||||
// beforeValid(...) ↓
|
||||
// afterValid(...) ↓
|
||||
|
||||
// ============================ 拦截器执行顺序 ============================
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2024/7/11 18:05
|
||||
* @Description 验证码拦截器
|
||||
*/
|
||||
public interface CaptchaInterceptor {
|
||||
|
||||
default String getName() {
|
||||
return "interceptor";
|
||||
}
|
||||
|
||||
default Context createContext() {
|
||||
return new Context(getName(), null, -1, 1, EmptyCaptchaInterceptor.INSTANCE);
|
||||
}
|
||||
|
||||
default ApiResponse<ImageCaptchaVO> beforeGenerateCaptcha(Context context, String type, GenerateParam param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default ApiResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default void afterGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, AnyMap validData) {
|
||||
}
|
||||
|
||||
default void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
|
||||
}
|
||||
|
||||
default ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
|
||||
Object preReturn = context.getPreReturnData();
|
||||
if (preReturn != null) {
|
||||
return (ApiResponse<?>) preReturn;
|
||||
}
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
|
||||
default ApiResponse<?> afterValid(Context context, String type, MatchParam matchParam, AnyMap validData, ApiResponse<?> basicValid) {
|
||||
Object preReturn = context.getPreReturnData();
|
||||
if (preReturn != null) {
|
||||
return (ApiResponse<?>) preReturn;
|
||||
}
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
|
||||
default ImageCaptchaInfo beforeGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default void beforeWrapImageCaptchaInfo(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
|
||||
|
||||
}
|
||||
|
||||
default void afterGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo, AbstractImageCaptchaGenerator generator) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package cloud.tianai.captcha.interceptor;
|
||||
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CaptchaInterceptorGroup implements CaptchaInterceptor {
|
||||
|
||||
|
||||
private String name = "group_interceptor";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private List<CaptchaInterceptor> validators = new ArrayList<>();
|
||||
|
||||
public void addInterceptor(CaptchaInterceptor validator) {
|
||||
validators.add(validator);
|
||||
}
|
||||
|
||||
public void addInterceptor(List<CaptchaInterceptor> validators) {
|
||||
this.validators.addAll(validators);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public CaptchaInterceptorGroup() {
|
||||
}
|
||||
|
||||
public CaptchaInterceptorGroup(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context createContext() {
|
||||
return new Context(getName(), null, -1, validators.size(), this);
|
||||
}
|
||||
|
||||
protected Context createContextIfNecessary(Context context) {
|
||||
if (context == null) {
|
||||
return createContext();
|
||||
}
|
||||
if (!context.getGroup().equals(this)) {
|
||||
Context innerContext = createContext();
|
||||
innerContext.setParent(context);
|
||||
context = innerContext;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> beforeGenerateCaptcha(Context context, String type, GenerateParam param) {
|
||||
context = createContextIfNecessary(context);
|
||||
ApiResponse<ImageCaptchaVO> captchaResponse = null;
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
captchaResponse = interceptor.beforeGenerateCaptcha(context, type, param);
|
||||
context.setPreReturnData(captchaResponse);
|
||||
}
|
||||
return captchaResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
|
||||
context = createContextIfNecessary(context);
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
interceptor.afterGenerateCaptcha(context, type, imageCaptchaInfo, captchaResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
|
||||
context = createContextIfNecessary(context);
|
||||
ApiResponse<?> beforeValid = null;
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
beforeValid = interceptor.beforeValid(context, type, matchParam, validData);
|
||||
context.setPreReturnData(beforeValid);
|
||||
}
|
||||
return beforeValid == null ? ApiResponse.ofSuccess() : beforeValid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> afterValid(Context context, String type, MatchParam matchParam, AnyMap validData, ApiResponse<?> basicValid) {
|
||||
context = createContextIfNecessary(context);
|
||||
ApiResponse<?> valid = null;
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
valid = interceptor.afterValid(context, type, matchParam, validData, basicValid);
|
||||
context.setPreReturnData(valid);
|
||||
}
|
||||
return valid == null ? ApiResponse.ofSuccess() : valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<ImageCaptchaVO> beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) {
|
||||
context = createContextIfNecessary(context);
|
||||
ApiResponse<ImageCaptchaVO> captchaResponse = null;
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
captchaResponse = interceptor.beforeGenerateImageCaptchaValidData(context, type, imageCaptchaInfo);
|
||||
context.setPreReturnData(captchaResponse);
|
||||
}
|
||||
return captchaResponse;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, AnyMap validData) {
|
||||
context = createContextIfNecessary(context);
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
interceptor.afterGenerateImageCaptchaValidData(context, type, imageCaptchaInfo, validData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageCaptchaInfo beforeGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
|
||||
context = createContextIfNecessary(context);
|
||||
ImageCaptchaInfo response = null;
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
response = interceptor.beforeGenerateCaptchaImage(context, captchaExchange, generator);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeWrapImageCaptchaInfo(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
|
||||
context = createContextIfNecessary(context);
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
interceptor.beforeWrapImageCaptchaInfo(context, captchaExchange, generator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo, AbstractImageCaptchaGenerator generator) {
|
||||
context = createContextIfNecessary(context);
|
||||
while (context.next() < context.getCount()) {
|
||||
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
|
||||
interceptor.afterGenerateCaptchaImage(context, captchaExchange, imageCaptchaInfo, generator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String printTree() {
|
||||
return doPrintTree(1);
|
||||
}
|
||||
|
||||
private String doPrintTree(int index) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StringBuilder start = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < index; i++) {
|
||||
start.append("|-----");
|
||||
}
|
||||
for (int i = 0; i < validators.size(); i++) {
|
||||
CaptchaInterceptor validator = validators.get(i);
|
||||
sb.append(start).append("[").append(validator.getName()).append("]").append("\n");
|
||||
if (validator instanceof CaptchaInterceptorGroup) {
|
||||
sb.append(((CaptchaInterceptorGroup) validator).doPrintTree(index + 1));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package cloud.tianai.captcha.interceptor;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2024/7/11 16:22
|
||||
* @Description 拦截器的上下文参数
|
||||
*/
|
||||
@Getter
|
||||
public class Context {
|
||||
/** 名称. */
|
||||
private String name;
|
||||
/** 父容器. */
|
||||
@Setter
|
||||
private Context parent;
|
||||
/** 当前拦截器数量. */
|
||||
private Integer current;
|
||||
/** 拦截器总数. */
|
||||
private Integer count;
|
||||
/** 拦截器组. */
|
||||
private CaptchaInterceptor group;
|
||||
/** The previous interceptor returns data. */
|
||||
@Setter
|
||||
private Object preReturnData;
|
||||
/** 传输数据. */
|
||||
private AnyMap data = new AnyMap();
|
||||
|
||||
public Context(String name, Context parent, Integer current, Integer count, CaptchaInterceptor group) {
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
this.current = current;
|
||||
this.count = count;
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public Object getPreReturnData() {
|
||||
Object returnData = preReturnData;
|
||||
if (returnData == null && parent != null) {
|
||||
returnData = parent.getPreReturnData();
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
public void putCurrentData(String key, Object value) {
|
||||
data.put(key, value);
|
||||
}
|
||||
|
||||
public <T> T getCurrentData(String key, Class<T> type) {
|
||||
return convert(data.get(key), type);
|
||||
}
|
||||
|
||||
public void putData(String key, Object value) {
|
||||
putCurrentData(key, value);
|
||||
if (parent != null) {
|
||||
parent.putData(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T getData(String key, Class<T> type) {
|
||||
T result = getCurrentData(key, type);
|
||||
if (result == null && parent != null) {
|
||||
result = parent.getData(key, type);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private <T> T convert(Object data, Class<T> clazz) {
|
||||
if (data == null || clazz == null) {
|
||||
return null;
|
||||
}
|
||||
// 判断转换的类型是否是number类型
|
||||
return (T) data;
|
||||
}
|
||||
|
||||
public Integer next() {
|
||||
current++;
|
||||
return current;
|
||||
}
|
||||
|
||||
public Integer end() {
|
||||
current = count;
|
||||
return count;
|
||||
}
|
||||
|
||||
public Boolean isEnd() {
|
||||
return current >= count;
|
||||
}
|
||||
|
||||
public Boolean isStart() {
|
||||
return current < 0;
|
||||
}
|
||||
|
||||
public void allEnd() {
|
||||
Context context = parent;
|
||||
if (context != null) {
|
||||
context.allEnd();
|
||||
}
|
||||
// 结束自身
|
||||
end();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package cloud.tianai.captcha.interceptor;
|
||||
|
||||
public class EmptyCaptchaInterceptor implements CaptchaInterceptor{
|
||||
|
||||
public static EmptyCaptchaInterceptor INSTANCE = new EmptyCaptchaInterceptor();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package cloud.tianai.captcha.interceptor.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.common.response.CodeDefinition;
|
||||
import cloud.tianai.captcha.common.util.CaptchaTypeClassifier;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.interceptor.Context;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/1/4 10:00
|
||||
* @Description BasicCaptchaTrackValidator
|
||||
*/
|
||||
public class BasicTrackCaptchaInterceptor implements CaptchaInterceptor {
|
||||
public static final CodeDefinition DEFINITION = new CodeDefinition(50001, "basic track check fail");
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "basic_track_check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> afterValid(Context context, String type, MatchParam matchData, AnyMap validData, ApiResponse<?> basicValid) {
|
||||
if (!basicValid.isSuccess()) {
|
||||
return context.getGroup().afterValid(context, type, matchData, validData, basicValid);
|
||||
}
|
||||
if (!CaptchaTypeClassifier.isSliderCaptcha(type)) {
|
||||
// 不是滑动验证码的话暂时跳过,点选验证码行为轨迹还没做
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
ImageCaptchaTrack imageCaptchaTrack = matchData.getTrack();
|
||||
// 进行行为轨迹检测
|
||||
long startSlidingTime = imageCaptchaTrack.getStartTime();
|
||||
long endSlidingTime = imageCaptchaTrack.getStopTime();
|
||||
Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth();
|
||||
List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
|
||||
// 这里只进行基本检测, 用一些简单算法进行校验,如有需要可扩展
|
||||
// 检测1: 滑动时间如果小于300毫秒 返回false
|
||||
// 检测2: 轨迹数据要是少于背10,或者大于背景宽度的五倍 返回false
|
||||
// 检测3: x轴和y轴应该是从0开始的,要是一开始x轴和y轴乱跑,返回false
|
||||
// 检测4: 如果y轴是相同的,必然是机器操作,直接返回false
|
||||
// 检测5: x轴或者y轴直接的区间跳跃过大的话返回 false
|
||||
// 检测6: x轴应该是由快到慢的, 要是速率一致,返回false
|
||||
// 检测7: 如果x轴超过图片宽度的频率过高,返回false
|
||||
|
||||
// 检测1
|
||||
if (startSlidingTime + 300 > endSlidingTime) {
|
||||
context.end();
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
// 检测2
|
||||
if (trackList.size() < 10 || trackList.size() > bgImageWidth * 5) {
|
||||
context.end();
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
// 检测3
|
||||
ImageCaptchaTrack.Track firstTrack = trackList.get(0);
|
||||
if (firstTrack.getX() > 10 || firstTrack.getX() < -10 || firstTrack.getY() > 10 || firstTrack.getY() < -10) {
|
||||
context.end();
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
int check4 = 0;
|
||||
int check7 = 0;
|
||||
for (int i = 1; i < trackList.size(); i++) {
|
||||
ImageCaptchaTrack.Track track = trackList.get(i);
|
||||
float x = track.getX();
|
||||
float y = track.getY();
|
||||
// check4
|
||||
if (firstTrack.getY() == y) {
|
||||
check4++;
|
||||
}
|
||||
// check7
|
||||
if (x >= bgImageWidth) {
|
||||
check7++;
|
||||
}
|
||||
// check5
|
||||
ImageCaptchaTrack.Track preTrack = trackList.get(i - 1);
|
||||
if ((track.getX() - preTrack.getX()) > 50 || (track.getY() - preTrack.getY()) > 50) {
|
||||
context.end();
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
}
|
||||
if (check4 == trackList.size() || check7 > 200) {
|
||||
context.end();
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
|
||||
// check6
|
||||
int splitPos = (int) (trackList.size() * 0.7);
|
||||
ImageCaptchaTrack.Track splitPostTrack = trackList.get(splitPos - 1);
|
||||
ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1);
|
||||
// bugfix: wuhaochao
|
||||
ImageCaptchaTrack.Track stepOneFirstTrack = trackList.get(0);
|
||||
ImageCaptchaTrack.Track stepOneTwoTrack = trackList.get(splitPos);
|
||||
float posTime = splitPostTrack.getT() - stepOneFirstTrack.getT();
|
||||
double startAvgPosTime = posTime / (float) splitPos;
|
||||
double endAvgPosTime = (lastTrack.getT() - stepOneTwoTrack.getT()) / (float) (trackList.size() - splitPos);
|
||||
boolean check = endAvgPosTime > startAvgPosTime;
|
||||
if (check) {
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
context.end();
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cloud.tianai.captcha.interceptor.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||
import cloud.tianai.captcha.common.util.ObjectUtils;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.interceptor.Context;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/1/4 10:10
|
||||
* @Description 轨迹参数校验, 如果轨迹参数为空抛异常
|
||||
*/
|
||||
public class ParamCheckCaptchaInterceptor implements CaptchaInterceptor {
|
||||
@Override
|
||||
public ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
|
||||
checkParam(matchParam.getTrack());
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "param_check";
|
||||
}
|
||||
|
||||
public void checkParam(ImageCaptchaTrack imageCaptchaTrack) {
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageWidth())) {
|
||||
throw new IllegalArgumentException("bgImageWidth must not be null");
|
||||
}
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageHeight())) {
|
||||
throw new IllegalArgumentException("bgImageHeight must not be null");
|
||||
}
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getStartTime())) {
|
||||
throw new IllegalArgumentException("startTime must not be null");
|
||||
}
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getStopTime())) {
|
||||
throw new IllegalArgumentException("stopTime must not be null");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(imageCaptchaTrack.getTrackList())) {
|
||||
throw new IllegalArgumentException("trackList must not be null");
|
||||
}
|
||||
for (ImageCaptchaTrack.Track track : imageCaptchaTrack.getTrackList()) {
|
||||
Float x = track.getX();
|
||||
Float y = track.getY();
|
||||
Float t = track.getT();
|
||||
String type = track.getType();
|
||||
if (x == null || y == null || t == null || ObjectUtils.isEmpty(type)) {
|
||||
throw new IllegalArgumentException("track[x,y,t,type] must not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cloud.tianai.captcha.resource;
|
||||
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/12/16 16:52
|
||||
* @Description 抽象的ResourceProvider
|
||||
*/
|
||||
public abstract class AbstractResourceProvider implements ResourceProvider {
|
||||
@Override
|
||||
public InputStream getResourceInputStream(Resource data) {
|
||||
InputStream resourceInputStream = doGetResourceInputStream(data);
|
||||
if (resourceInputStream == null) {
|
||||
throw new IllegalArgumentException("无法读到指定的资源[" + getName() + "]" + data);
|
||||
}
|
||||
return resourceInputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Resource 获取 InputStream
|
||||
*
|
||||
* @param data data
|
||||
* @return InputStream
|
||||
*/
|
||||
public abstract InputStream doGetResourceInputStream(Resource data);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package cloud.tianai.captcha.resource;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static cloud.tianai.captcha.common.constant.CommonConstant.DEFAULT_TAG;
|
||||
import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.TEMPLATE_ACTIVE_IMAGE_NAME;
|
||||
import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.TEMPLATE_FIXED_IMAGE_NAME;
|
||||
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2024/7/15 9:10
|
||||
* @Description 默认资源配置
|
||||
* 注意: 不推荐使用该类,应该将资源模板自己设置,而不是使用默认的,这里编写的目的只是为了演示方便
|
||||
*/
|
||||
public class DefaultBuiltInResources {
|
||||
|
||||
public static final String PATH_PREFIX = "classpath:META-INF/cut-image/template";
|
||||
|
||||
private static Map<String, Consumer<CrudResourceStore>> defaultTemplateResource = new HashMap<>(8);
|
||||
|
||||
|
||||
public DefaultBuiltInResources(String defaultPathPrefix) {
|
||||
init(defaultPathPrefix);
|
||||
}
|
||||
|
||||
private void init(String defaultPathPrefix) {
|
||||
String[] split = defaultPathPrefix.split(":");
|
||||
String type;
|
||||
String pathPrefix;
|
||||
if (split.length < 1) {
|
||||
type = "file";
|
||||
pathPrefix = defaultPathPrefix;
|
||||
} else {
|
||||
type = split[0];
|
||||
pathPrefix = split[1];
|
||||
}
|
||||
if (pathPrefix.endsWith("/")) {
|
||||
pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
|
||||
}
|
||||
// 滑动验证
|
||||
String finalPathPrefix = pathPrefix;
|
||||
defaultTemplateResource.put(CaptchaTypeConstant.SLIDER, resourceStore -> {
|
||||
ResourceMap template1 = new ResourceMap(DEFAULT_TAG, 4);
|
||||
template1.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_1/active.png")));
|
||||
template1.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_1/fixed.png")));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template1);
|
||||
|
||||
ResourceMap template2 = new ResourceMap(DEFAULT_TAG, 4);
|
||||
template2.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_2/active.png")));
|
||||
template2.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_2/fixed.png")));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template2);
|
||||
});
|
||||
|
||||
// 旋转验证
|
||||
defaultTemplateResource.put(CaptchaTypeConstant.ROTATE, resourceStore -> {
|
||||
// 添加一些系统的 模板文件
|
||||
ResourceMap template1 = new ResourceMap(DEFAULT_TAG, 4);
|
||||
template1.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/rotate_1/active.png")));
|
||||
template1.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/rotate_1/fixed.png")));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.ROTATE, template1);
|
||||
});
|
||||
|
||||
// 字体包
|
||||
defaultTemplateResource.put(FontCache.FONT_TYPE, resourceStore -> {
|
||||
resourceStore.addResource(FontCache.FONT_TYPE,new Resource(type, finalPathPrefix.concat("/fonts/SIMSUN.TTC")));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void addDefaultTemplate(String type, ResourceStore resourceStore) {
|
||||
if (resourceStore instanceof CrudResourceStore) {
|
||||
Consumer<CrudResourceStore> resourceStoreConsumer = defaultTemplateResource.get(type);
|
||||
if (resourceStoreConsumer == null) {
|
||||
return;
|
||||
}
|
||||
resourceStoreConsumer.accept((CrudResourceStore) resourceStore);
|
||||
}
|
||||
}
|
||||
|
||||
public void addDefaultTemplate(ResourceStore resourceStore) {
|
||||
if (resourceStore instanceof CrudResourceStore) {
|
||||
defaultTemplateResource.forEach((type, consumer) -> {
|
||||
consumer.accept((CrudResourceStore) resourceStore);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package cloud.tianai.captcha.resource;
|
||||
|
||||
import cloud.tianai.captcha.generator.common.FontWrapper;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2024/11/19 11:25
|
||||
* @Description 一个用于统一缓存字体文件的对象
|
||||
*/
|
||||
@Slf4j
|
||||
public class FontCache implements ResourceStore {
|
||||
|
||||
|
||||
public static final String FONT_TYPE = "font";
|
||||
private final Map<String, FontWrapper> fontMap = new ConcurrentHashMap<>();
|
||||
|
||||
private ResourceStore resourceStore;
|
||||
private ImageCaptchaResourceManager resourceManager;
|
||||
@Setter
|
||||
@Getter
|
||||
private int fontSize = 70;
|
||||
|
||||
|
||||
|
||||
public FontCache(ResourceStore resourceStore) {
|
||||
this.resourceStore = resourceStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ImageCaptchaResourceManager resourceManager) {
|
||||
resourceStore.init(resourceManager);
|
||||
this.resourceManager = resourceManager;
|
||||
}
|
||||
|
||||
|
||||
public FontWrapper getFont(Resource resource) {
|
||||
try (InputStream stream = resourceManager.getResourceInputStream(resource)) {
|
||||
Font font = Font.createFont(0, stream);
|
||||
return new FontWrapper(font, fontSize);
|
||||
} catch (FontFormatException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String calcId(Resource resource) {
|
||||
// 缓存id, 避免重复加载。 多个验证码可能使用同一个字体, 这里不使用资源ID作为缓存ID, 而是使用type+data作为缓存ID。
|
||||
return resource.getType() + "_" + resource.getData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
|
||||
List<Resource> resources = resourceStore.randomGetResourceByTypeAndTag(type, tag, quantity);
|
||||
// 字体增强
|
||||
if (FONT_TYPE.equalsIgnoreCase(type)) {
|
||||
for (Resource resource : resources) {
|
||||
FontWrapper fontWrapper = fontMap.computeIfAbsent(calcId(resource), v -> getFont(resource));
|
||||
resource.setExtra(fontWrapper);
|
||||
}
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag, Integer quantity) {
|
||||
return resourceStore.randomGetTemplateByTypeAndTag(type, tag, quantity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceStore getTarget() {
|
||||
return resourceStore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 15:26
|
||||
* @Description 验证码图片资源管理器
|
||||
*/
|
||||
public interface ImageCaptchaResourceManager {
|
||||
|
||||
/**
|
||||
* 随机获取某个模板
|
||||
*
|
||||
* @param type 验证码类型
|
||||
* @param tag 二级过滤,可以为空
|
||||
* @return Map<String, Resource>
|
||||
*/
|
||||
ResourceMap randomGetTemplate(String type, String tag);
|
||||
|
||||
/**
|
||||
* 随机获取某个资源对象
|
||||
*
|
||||
* @param type 验证码类型
|
||||
* @param tag 二级过滤,可以为空
|
||||
* @return Resource
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 获取真正的资源流通过资源对象
|
||||
*
|
||||
* @param resource resource
|
||||
* @return InputStream
|
||||
*/
|
||||
InputStream getResourceInputStream(Resource resource);
|
||||
|
||||
/**
|
||||
* 获取所有资源提供者
|
||||
*
|
||||
* @return List<ResourceProvider>
|
||||
*/
|
||||
List<ResourceProvider> listResourceProviders();
|
||||
|
||||
/**
|
||||
* 注册资源提供者
|
||||
*
|
||||
* @param resourceProvider 资源提供者
|
||||
*/
|
||||
void registerResourceProvider(ResourceProvider resourceProvider);
|
||||
|
||||
/**
|
||||
* 删除资源提供者
|
||||
*
|
||||
* @param name 资源提供者名称
|
||||
* @return ResourceProvider
|
||||
*/
|
||||
boolean deleteResourceProviderByName(String name);
|
||||
|
||||
/**
|
||||
* 设置资源存储
|
||||
*
|
||||
* @param resourceStore resourceStore
|
||||
*/
|
||||
void setResourceStore(ResourceStore resourceStore);
|
||||
|
||||
/**
|
||||
* 获取资源存储
|
||||
*
|
||||
* @return ResourceStore
|
||||
*/
|
||||
ResourceStore getResourceStore();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cloud.tianai.captcha.resource;
|
||||
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 15:07
|
||||
* @Description 资源提供者
|
||||
*/
|
||||
public interface ResourceProvider {
|
||||
|
||||
/**
|
||||
* 获取资源
|
||||
*
|
||||
* @param data data
|
||||
* @return InputStream
|
||||
*/
|
||||
InputStream getResourceInputStream(Resource data);
|
||||
|
||||
/**
|
||||
* 是否支持
|
||||
*
|
||||
* @param resource resource
|
||||
* @return boolean
|
||||
*/
|
||||
boolean supported(Resource resource);
|
||||
|
||||
/**
|
||||
* 放弃资源提供者名称
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
String getName();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cloud.tianai.captcha.resource;
|
||||
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import cloud.tianai.captcha.resource.impl.provider.FileResourceProvider;
|
||||
import cloud.tianai.captcha.resource.impl.provider.URLResourceProvider;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ResourceProviders {
|
||||
|
||||
private final List<ResourceProvider> resourceProviderList = new ArrayList<>(8);
|
||||
|
||||
|
||||
public ResourceProviders() {
|
||||
registerResourceProvider(new URLResourceProvider());
|
||||
registerResourceProvider(new ClassPathResourceProvider());
|
||||
registerResourceProvider(new FileResourceProvider());
|
||||
}
|
||||
|
||||
public void registerResourceProvider(ResourceProvider resourceProvider) {
|
||||
deleteResourceProviderByName(resourceProvider.getName());
|
||||
resourceProviderList.add(resourceProvider);
|
||||
}
|
||||
|
||||
public boolean deleteResourceProviderByName(String name) {
|
||||
return resourceProviderList.removeIf(r -> r.getName().equals(name));
|
||||
}
|
||||
|
||||
public List<ResourceProvider> listResourceProviders() {
|
||||
return Collections.unmodifiableList(resourceProviderList);
|
||||
}
|
||||
|
||||
|
||||
public InputStream getResourceInputStream(Resource resource) {
|
||||
for (ResourceProvider resourceProvider : resourceProviderList) {
|
||||
if (resourceProvider.supported(resource)) {
|
||||
InputStream resourceInputStream = resourceProvider.getResourceInputStream(resource);
|
||||
if (resourceInputStream == null) {
|
||||
throw new IllegalArgumentException("滑块验证码 ResourceProvider 读到的图片资源为空,providerName=["
|
||||
+ resourceProvider.getName() + "], resource=[" + resource + "]");
|
||||
}
|
||||
return resourceInputStream;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("没有找到Resource [" + resource.getType() + "]对应的资源提供者");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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 2022/5/7 9:04
|
||||
* @Description 资源存储
|
||||
*/
|
||||
public interface ResourceStore {
|
||||
|
||||
void init(ImageCaptchaResourceManager resourceManager);
|
||||
|
||||
/**
|
||||
* 随机获取某个资源
|
||||
*
|
||||
* @param type type
|
||||
* @return Resource
|
||||
*/
|
||||
List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity);
|
||||
|
||||
/**
|
||||
* 随机获取某个模板通过type
|
||||
*
|
||||
* @param type type
|
||||
* @return Map<String, Resource>
|
||||
*/
|
||||
List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag,Integer quantity);
|
||||
|
||||
default ResourceStore getTarget() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cloud.tianai.captcha.resource.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.util.UUIDUtils;
|
||||
import cloud.tianai.captcha.resource.ResourceProvider;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 15:15
|
||||
* @Description 资源对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class Resource {
|
||||
/** 唯一ID. */
|
||||
private String id;
|
||||
/** 类型. */
|
||||
private String type;
|
||||
/** 数据,传输给 {@link ResourceProvider} 的参数 */
|
||||
public String data;
|
||||
/** 标签. */
|
||||
private String tag;
|
||||
/** 提示. */
|
||||
private String tip;
|
||||
/** 扩展. */
|
||||
private Object extra;
|
||||
|
||||
public Resource(String type, String data) {
|
||||
this(type, data, null);
|
||||
}
|
||||
|
||||
public Resource(String type, String data, String tag) {
|
||||
this(type, data, tag, null);
|
||||
}
|
||||
|
||||
public Resource(String type, String data, String tag, String tip) {
|
||||
this(UUIDUtils.getUUID(), type, data, tag, tip);
|
||||
}
|
||||
|
||||
public Resource(String id, String type, String data, String tag, String tip) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
this.tag = tag;
|
||||
this.tip = tip;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cloud.tianai.captcha.resource.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.common.util.UUIDUtils;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/12/30 9:23
|
||||
* @Description 存储一组Resource的Map, 增加tag标记
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class ResourceMap {
|
||||
/** 唯一ID. */
|
||||
private String id;
|
||||
private Map<String, Resource> resourceMap;
|
||||
private String tag;
|
||||
|
||||
public ResourceMap(String tag) {
|
||||
this(tag, 10);
|
||||
}
|
||||
|
||||
public ResourceMap(String tag, int initialCapacity) {
|
||||
this(UUIDUtils.getUUID(), tag, initialCapacity);
|
||||
}
|
||||
|
||||
public ResourceMap(String id, String tag, int initialCapacity) {
|
||||
this.tag = tag;
|
||||
this.resourceMap = new HashMap<>(initialCapacity);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public ResourceMap(int initialCapacity) {
|
||||
this(null, initialCapacity);
|
||||
}
|
||||
|
||||
public ResourceMap() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
private Map<String, Resource> getResourceMapOfCreate() {
|
||||
if (resourceMap == null) {
|
||||
resourceMap = new HashMap<>(2);
|
||||
}
|
||||
return resourceMap;
|
||||
}
|
||||
|
||||
// ================== Map ==================
|
||||
|
||||
public Resource put(String key, Resource value) {
|
||||
return getResourceMapOfCreate().put(key, value);
|
||||
}
|
||||
|
||||
public Resource get(Object key) {
|
||||
return getResourceMapOfCreate().get(key);
|
||||
}
|
||||
|
||||
public Resource remove(Object key) {
|
||||
return getResourceMapOfCreate().remove(key);
|
||||
}
|
||||
|
||||
public Collection<Resource> values() {
|
||||
return getResourceMapOfCreate().values();
|
||||
}
|
||||
|
||||
public void forEach(BiConsumer<String, Resource> action) {
|
||||
getResourceMapOfCreate().forEach(action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package cloud.tianai.captcha.resource.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||
import 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.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 15:35
|
||||
* @Description 默认的滑块验证码资源管理
|
||||
*/
|
||||
public class DefaultImageCaptchaResourceManager implements ImageCaptchaResourceManager {
|
||||
|
||||
/** 资源存储. */
|
||||
private ResourceStore resourceStore;
|
||||
/** 资源转换 转换为stream流. */
|
||||
@Getter
|
||||
private ResourceProviders resourceProviders;
|
||||
|
||||
public DefaultImageCaptchaResourceManager() {
|
||||
init();
|
||||
}
|
||||
|
||||
public DefaultImageCaptchaResourceManager(ResourceStore resourceStore, ResourceProviders resourceProviders) {
|
||||
this.resourceStore = resourceStore;
|
||||
this.resourceProviders = resourceProviders;
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
if (this.resourceStore == null) {
|
||||
this.resourceStore = new LocalMemoryResourceStore();
|
||||
}
|
||||
// 在这里临时加上字体缓存器
|
||||
resourceStore = new FontCache(resourceStore);
|
||||
resourceStore.init(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceMap randomGetTemplate(String type, String tag) {
|
||||
return randomGetTemplate(type, tag, 1).get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource randomGetResource(String type, String tag) {
|
||||
return randomGetResource(type, tag, 1).get(0);
|
||||
}
|
||||
|
||||
@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
|
||||
public InputStream getResourceInputStream(Resource resource) {
|
||||
return resourceProviders.getResourceInputStream(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceProvider> listResourceProviders() {
|
||||
return resourceProviders.listResourceProviders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerResourceProvider(ResourceProvider resourceProvider) {
|
||||
resourceProviders.registerResourceProvider(resourceProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteResourceProviderByName(String name) {
|
||||
return resourceProviders.deleteResourceProviderByName(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResourceStore(ResourceStore resourceStore) {
|
||||
this.resourceStore = resourceStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceStore getResourceStore() {
|
||||
return resourceStore;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package cloud.tianai.captcha.resource.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CommonConstant;
|
||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||
import cloud.tianai.captcha.common.util.ObjectUtils;
|
||||
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 java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 15:43
|
||||
* @Description 默认的资源存储
|
||||
*/
|
||||
public class LocalMemoryResourceStore implements CrudResourceStore {
|
||||
/** 用于检索 type和tag. */
|
||||
private final Map<String, Map<String, List<ResourceMap>>> templateResourceTagMap = 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
|
||||
public void addResource(String type, Resource resource) {
|
||||
if (ObjectUtils.isEmpty(resource.getTag())) {
|
||||
resource.setTag(CommonConstant.DEFAULT_TAG);
|
||||
}
|
||||
ensureTypeTagMapExists(resourceTagMap, type, resource.getTag());
|
||||
resourceTagMap.get(type).get(resource.getTag()).add(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTemplate(String type, ResourceMap template) {
|
||||
if (ObjectUtils.isEmpty(template.getTag())) {
|
||||
template.setTag(CommonConstant.DEFAULT_TAG);
|
||||
}
|
||||
ensureTypeTagMapExistsForTemplate(templateResourceTagMap, type, template.getTag());
|
||||
templateResourceTagMap.get(type).get(template.getTag()).add(template);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource deleteResource(String type, String id) {
|
||||
Map<String, List<Resource>> tagMap = resourceTagMap.get(type);
|
||||
if (tagMap == null) return null;
|
||||
|
||||
for (List<Resource> resources : tagMap.values()) {
|
||||
Iterator<Resource> iterator = resources.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Resource res = iterator.next();
|
||||
if (res.getId().equals(id)) {
|
||||
iterator.remove();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceMap deleteTemplate(String type, String id) {
|
||||
Map<String, List<ResourceMap>> tagMap = templateResourceTagMap.get(type);
|
||||
if (tagMap == null) return null;
|
||||
|
||||
for (List<ResourceMap> templates : tagMap.values()) {
|
||||
Iterator<ResourceMap> iterator = templates.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ResourceMap temp = iterator.next();
|
||||
if (temp.getId().equals(id)) {
|
||||
iterator.remove();
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Resource> listResourcesByTypeAndTag(String type, String tag) {
|
||||
if (!ObjectUtils.isEmpty(tag)) {
|
||||
Map<String, List<Resource>> tagMap = resourceTagMap.get(type);
|
||||
return tagMap == null ? Collections.emptyList() : tagMap.getOrDefault(tag, Collections.emptyList());
|
||||
}
|
||||
List<Resource> result = new ArrayList<>();
|
||||
Map<String, List<Resource>> tagMap = resourceTagMap.get(type);
|
||||
if (tagMap != null) {
|
||||
for (List<Resource> list : tagMap.values()) {
|
||||
result.addAll(list);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceMap> listTemplatesByTypeAndTag(String type, String tag) {
|
||||
if (!ObjectUtils.isEmpty(tag)) {
|
||||
Map<String, List<ResourceMap>> tagMap = templateResourceTagMap.get(type);
|
||||
return tagMap == null ? Collections.emptyList() : tagMap.getOrDefault(tag, Collections.emptyList());
|
||||
}
|
||||
List<ResourceMap> result = new ArrayList<>();
|
||||
Map<String, List<ResourceMap>> tagMap = templateResourceTagMap.get(type);
|
||||
if (tagMap != null) {
|
||||
for (List<ResourceMap> list : tagMap.values()) {
|
||||
result.addAll(list);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ImageCaptchaResourceManager resourceManager) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
|
||||
List<Resource> resources = listResourcesByTypeAndTag(type, tag);
|
||||
if (CollectionUtils.isEmpty(resources)) {
|
||||
throw new IllegalStateException("随机获取资源错误,store中资源为空, type:" + type + ",tag:" + tag);
|
||||
}
|
||||
int size = resources.size();
|
||||
if (quantity > size) {
|
||||
throw new IllegalArgumentException("请求的资源数量超过可用资源总数");
|
||||
}
|
||||
|
||||
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
|
||||
public List<ResourceMap> randomGetTemplateByTypeAndTag(String type, String tag, Integer quantity) {
|
||||
List<ResourceMap> templates = listTemplatesByTypeAndTag(type, tag);
|
||||
if (CollectionUtils.isEmpty(templates)) {
|
||||
throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type + ",tag:" + tag);
|
||||
}
|
||||
int size = templates.size();
|
||||
if (quantity > size) {
|
||||
throw new IllegalArgumentException("请求的模板数量超过可用模板总数");
|
||||
}
|
||||
|
||||
Set<Integer> indexes = new HashSet<>(quantity);
|
||||
while (indexes.size() < quantity) {
|
||||
indexes.add(ThreadLocalRandom.current().nextInt(size));
|
||||
}
|
||||
|
||||
List<ResourceMap> result = new ArrayList<>(quantity);
|
||||
for (int index : indexes) {
|
||||
result.add(templates.get(index));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAllResources() {
|
||||
resourceTagMap.clear();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void clearAllTemplates() {
|
||||
templateResourceTagMap.clear();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cloud.tianai.captcha.resource.impl.provider;
|
||||
|
||||
import cloud.tianai.captcha.resource.AbstractResourceProvider;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 16:07
|
||||
* @Description classPath
|
||||
*/
|
||||
public class ClassPathResourceProvider extends AbstractResourceProvider {
|
||||
|
||||
public static final String NAME = "classpath";
|
||||
|
||||
public static ClassLoader classLoader;
|
||||
|
||||
@Override
|
||||
public InputStream doGetResourceInputStream(Resource data) {
|
||||
if (classLoader == null) {
|
||||
return getClassLoader().getResourceAsStream(data.getData());
|
||||
}
|
||||
return classLoader.getResourceAsStream(data.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supported(Resource resource) {
|
||||
return NAME.equalsIgnoreCase(resource.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
private static ClassLoader getClassLoader() {
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
if (classLoader == null) {
|
||||
classLoader = ClassPathResourceProvider.getClassLoader();
|
||||
}
|
||||
if (classLoader == null) {
|
||||
classLoader = ClassLoader.getSystemClassLoader();
|
||||
}
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
public static void setClassLoader(ClassLoader classLoader) {
|
||||
ClassPathResourceProvider.classLoader = classLoader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cloud.tianai.captcha.resource.impl.provider;
|
||||
|
||||
import cloud.tianai.captcha.resource.AbstractResourceProvider;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/21 14:43
|
||||
* @Description file
|
||||
*/
|
||||
public class FileResourceProvider extends AbstractResourceProvider {
|
||||
|
||||
public static final String NAME = "file";
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public InputStream doGetResourceInputStream(Resource data) {
|
||||
FileInputStream fileInputStream = new FileInputStream(data.getData());
|
||||
return fileInputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supported(Resource resource) {
|
||||
return NAME.equalsIgnoreCase(resource.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cloud.tianai.captcha.resource.impl.provider;
|
||||
|
||||
import cloud.tianai.captcha.resource.AbstractResourceProvider;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2021/8/7 16:05
|
||||
* @Description url
|
||||
*/
|
||||
public class URLResourceProvider extends AbstractResourceProvider {
|
||||
|
||||
public static final String NAME = "URL";
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public InputStream doGetResourceInputStream(Resource data) {
|
||||
URL url = new URL(data.getData());
|
||||
return url.openStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supported(Resource resource) {
|
||||
return NAME.equalsIgnoreCase(resource.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cloud.tianai.captcha.validator;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/17 10:54
|
||||
* @Description 图片验证码校验器
|
||||
*/
|
||||
public interface ImageCaptchaValidator {
|
||||
|
||||
/**
|
||||
* 用于生成验证码校验时需要的回传参数
|
||||
*
|
||||
* @param imageCaptchaInfo 生成的验证码数据
|
||||
* @return AnyMap
|
||||
*/
|
||||
AnyMap generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo);
|
||||
|
||||
/**
|
||||
* 校验用户滑动滑块是否正确
|
||||
*
|
||||
* @param imageCaptchaTrack 包含了滑动轨迹,展示的图片宽高,滑动时间等参数
|
||||
* @param imageCaptchaValidData generateImageCaptchaValidData(生成的数据)
|
||||
* @return ApiResponse<?>
|
||||
*/
|
||||
ApiResponse<?> valid(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cloud.tianai.captcha.validator;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2023/1/19 10:40
|
||||
* @Description 滑动类验证码百分比校验
|
||||
*/
|
||||
public interface SliderCaptchaPercentageValidator {
|
||||
|
||||
/**
|
||||
* 计算滑块要背景图的百分比,基本校验
|
||||
* 用于计算滑动类验证码的缺口位置
|
||||
*
|
||||
* @param pos 移动的位置
|
||||
* @param maxPos 最大可移动的位置
|
||||
* @return float
|
||||
*/
|
||||
float calcPercentage(Number pos, Number maxPos);
|
||||
|
||||
/**
|
||||
* 校验滑块百分比
|
||||
* 用于校验滑动类验证码是否滑动到缺口
|
||||
*
|
||||
* @param newPercentage 用户滑动的百分比
|
||||
* @param oriPercentage 正确的滑块百分比
|
||||
* @return boolean
|
||||
*/
|
||||
boolean checkPercentage(Float newPercentage, Float oriPercentage);
|
||||
|
||||
/**
|
||||
* 校验滑块百分比
|
||||
* 用于校验滑动类验证码是否滑动到缺口
|
||||
*
|
||||
* @param newPercentage 用户滑动的百分比
|
||||
* @param oriPercentage 正确的滑块百分比
|
||||
* @param tolerant 容错值
|
||||
* @return boolean
|
||||
*/
|
||||
boolean checkPercentage(Float newPercentage, Float oriPercentage, float tolerant);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cloud.tianai.captcha.validator.common.constant;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/4/29 8:33
|
||||
* @Description 滑动轨迹类型
|
||||
*/
|
||||
public interface TrackTypeConstant {
|
||||
|
||||
/** 抬起.*/
|
||||
String UP = "UP";
|
||||
/** 按下.*/
|
||||
String DOWN = "DOWN";
|
||||
/** 移动.*/
|
||||
String MOVE = "MOVE";
|
||||
/** 点击.*/
|
||||
String CLICK = "CLICK";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cloud.tianai.captcha.validator.common.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Drives {
|
||||
private Integer hardwareConcurrency;
|
||||
private Boolean hasXhr = false;
|
||||
private String href;
|
||||
private String language;
|
||||
private Long start;
|
||||
private Long now;
|
||||
private String platform;
|
||||
private Integer scripts;
|
||||
private String userAgent;
|
||||
private Integer windowHeight;
|
||||
private Integer windowWidth;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cloud.tianai.captcha.validator.common.model.dto;
|
||||
|
||||
import cloud.tianai.captcha.validator.common.constant.TrackTypeConstant;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/17 9:23
|
||||
* @Description 图片验证码滑动轨迹
|
||||
*/
|
||||
@Data
|
||||
public class ImageCaptchaTrack {
|
||||
|
||||
/** 背景图片宽度. */
|
||||
private Integer bgImageWidth;
|
||||
/** 背景图片高度. */
|
||||
private Integer bgImageHeight;
|
||||
/** 模板图片宽度. */
|
||||
private Integer templateImageWidth;
|
||||
/** 模板图片高度. */
|
||||
private Integer templateImageHeight;
|
||||
/** 滑动开始时间. */
|
||||
private Long startTime;
|
||||
/** 滑动结束时间. */
|
||||
private Long stopTime;
|
||||
private Integer left;
|
||||
private Integer top;
|
||||
/** 滑动的轨迹. */
|
||||
private List<Track> trackList;
|
||||
/** 扩展数据,用户传输加密数据等.*/
|
||||
private Object data;
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Track {
|
||||
/** x. */
|
||||
private Float x;
|
||||
/** y. */
|
||||
private Float y;
|
||||
/** 时间. */
|
||||
private Float t;
|
||||
/** 类型. */
|
||||
private String type = TrackTypeConstant.MOVE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cloud.tianai.captcha.validator.common.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2024/8/19 15:12
|
||||
* @Description 验证码匹配的对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class MatchParam {
|
||||
/** 轨迹信息. */
|
||||
private ImageCaptchaTrack track;
|
||||
/** 检测到的设备信息. */
|
||||
private Drives drives;
|
||||
/** 留一个扩展属性. */
|
||||
private Object extendData;
|
||||
|
||||
|
||||
public MatchParam(ImageCaptchaTrack track) {
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
public MatchParam(ImageCaptchaTrack track, Drives drives) {
|
||||
this.track = track;
|
||||
this.drives = drives;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package cloud.tianai.captcha.validator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.common.response.CodeDefinition;
|
||||
import cloud.tianai.captcha.common.util.CaptchaTypeClassifier;
|
||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||
import cloud.tianai.captcha.common.util.ObjectUtils;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/17 11:01
|
||||
* @Description 基本的行为轨迹校验
|
||||
*/
|
||||
public class BasicCaptchaTrackValidator extends SimpleImageCaptchaValidator {
|
||||
public static final CodeDefinition DEFINITION = new CodeDefinition(50001, "basic check fail");
|
||||
|
||||
public BasicCaptchaTrackValidator() {
|
||||
}
|
||||
|
||||
public BasicCaptchaTrackValidator(float defaultTolerant) {
|
||||
super(defaultTolerant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> beforeValid(ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) {
|
||||
// 校验参数
|
||||
checkParam(imageCaptchaTrack);
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> afterValid(Boolean basicValid, ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) {
|
||||
if (!basicValid){
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
if (!CaptchaTypeClassifier.isSliderCaptcha(type)) {
|
||||
// 不是滑动验证码的话暂时跳过,点选验证码行为轨迹还没做
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
// 进行行为轨迹检测
|
||||
long startSlidingTime = imageCaptchaTrack.getStartTime();
|
||||
long endSlidingTime = imageCaptchaTrack.getStopTime();
|
||||
Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth();
|
||||
List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
|
||||
// 这里只进行基本检测, 用一些简单算法进行校验,如有需要可扩展
|
||||
// 检测1: 滑动时间如果小于300毫秒 返回false
|
||||
// 检测2: 轨迹数据要是少于背10,或者大于背景宽度的五倍 返回false
|
||||
// 检测3: x轴和y轴应该是从0开始的,要是一开始x轴和y轴乱跑,返回false
|
||||
// 检测4: 如果y轴是相同的,必然是机器操作,直接返回false
|
||||
// 检测5: x轴或者y轴直接的区间跳跃过大的话返回 false
|
||||
// 检测6: x轴应该是由快到慢的, 要是速率一致,返回false
|
||||
// 检测7: 如果x轴超过图片宽度的频率过高,返回false
|
||||
|
||||
// 检测1
|
||||
if (startSlidingTime + 300 > endSlidingTime) {
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
// 检测2
|
||||
if (trackList.size() < 10 || trackList.size() > bgImageWidth * 5) {
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
// 检测3
|
||||
ImageCaptchaTrack.Track firstTrack = trackList.get(0);
|
||||
if (firstTrack.getX() > 10 || firstTrack.getX() < -10 || firstTrack.getY() > 10 || firstTrack.getY() < -10) {
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
int check4 = 0;
|
||||
int check7 = 0;
|
||||
for (int i = 1; i < trackList.size(); i++) {
|
||||
ImageCaptchaTrack.Track track = trackList.get(i);
|
||||
float x = track.getX();
|
||||
float y = track.getY();
|
||||
// check4
|
||||
if (firstTrack.getY() == y) {
|
||||
check4++;
|
||||
}
|
||||
// check7
|
||||
if (x >= bgImageWidth) {
|
||||
check7++;
|
||||
}
|
||||
// check5
|
||||
ImageCaptchaTrack.Track preTrack = trackList.get(i - 1);
|
||||
if ((track.getX() - preTrack.getX()) > 50 || (track.getY() - preTrack.getY()) > 50) {
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
}
|
||||
if (check4 == trackList.size() || check7 > 200) {
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
|
||||
// check6
|
||||
int splitPos = (int) (trackList.size() * 0.7);
|
||||
ImageCaptchaTrack.Track splitPostTrack = trackList.get(splitPos - 1);
|
||||
float posTime = splitPostTrack.getT();
|
||||
float startAvgPosTime = posTime / (float) splitPos;
|
||||
|
||||
ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1);
|
||||
double endAvgPosTime = lastTrack.getT() / (float) (trackList.size() - splitPos);
|
||||
|
||||
boolean check = endAvgPosTime > startAvgPosTime;
|
||||
if (check) {
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
return ApiResponse.ofMessage(DEFINITION);
|
||||
}
|
||||
|
||||
public void checkParam(ImageCaptchaTrack imageCaptchaTrack) {
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageWidth())) {
|
||||
throw new IllegalArgumentException("bgImageWidth must not be null");
|
||||
}
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageHeight())) {
|
||||
throw new IllegalArgumentException("bgImageHeight must not be null");
|
||||
}
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getStartTime())) {
|
||||
throw new IllegalArgumentException("startSlidingTime must not be null");
|
||||
}
|
||||
if (ObjectUtils.isEmpty(imageCaptchaTrack.getStopTime())) {
|
||||
throw new IllegalArgumentException("endSlidingTime must not be null");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(imageCaptchaTrack.getTrackList())) {
|
||||
throw new IllegalArgumentException("trackList must not be null");
|
||||
}
|
||||
for (ImageCaptchaTrack.Track track : imageCaptchaTrack.getTrackList()) {
|
||||
Float x = track.getX();
|
||||
Float y = track.getY();
|
||||
Float t = track.getT();
|
||||
String type = track.getType();
|
||||
if (x == null || y == null || t == null || ObjectUtils.isEmpty(type)) {
|
||||
throw new IllegalArgumentException("track[x,y,t,type] must not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package cloud.tianai.captcha.validator.impl;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.common.response.ApiResponseStatusConstant;
|
||||
import cloud.tianai.captcha.common.util.CaptchaTypeClassifier;
|
||||
import cloud.tianai.captcha.common.util.CollectionUtils;
|
||||
import cloud.tianai.captcha.common.util.ObjectUtils;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ClickImageCheckDefinition;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
|
||||
import cloud.tianai.captcha.validator.SliderCaptchaPercentageValidator;
|
||||
import cloud.tianai.captcha.validator.common.constant.TrackTypeConstant;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Author: 天爱有情
|
||||
* @date 2022/2/17 11:01
|
||||
* @Description 基本的滑块验证校验 , 值进行基本校验, 目前只校验用户是否滑动到缺口处,不校验行为轨迹
|
||||
*/
|
||||
@Slf4j
|
||||
public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, SliderCaptchaPercentageValidator {
|
||||
|
||||
/** 默认的容错值. */
|
||||
public static float DEFAULT_TOLERANT = 0.02f;
|
||||
/** 验证数据 key. */
|
||||
public static final String PERCENTAGE_KEY = "percentage";
|
||||
/** 容错值key. */
|
||||
public static final String TOLERANT_KEY = "tolerant";
|
||||
/** 类型 key, 标识是哪张类型的验证码. */
|
||||
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 = "user_current_percentage";
|
||||
/** 容错值. */
|
||||
@Getter
|
||||
@Setter
|
||||
public float defaultTolerant = DEFAULT_TOLERANT;
|
||||
|
||||
public SimpleImageCaptchaValidator() {
|
||||
CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.CONCAT);
|
||||
CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.ROTATE);
|
||||
CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.SLIDER);
|
||||
CaptchaTypeClassifier.addClickCaptchaType(CaptchaTypeConstant.WORD_IMAGE_CLICK);
|
||||
}
|
||||
|
||||
public SimpleImageCaptchaValidator(float defaultTolerant) {
|
||||
this();
|
||||
this.defaultTolerant = defaultTolerant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float calcPercentage(Number pos, Number maxPos) {
|
||||
return pos.floatValue() / maxPos.floatValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkPercentage(Float newPercentage, Float oriPercentage) {
|
||||
return checkPercentage(newPercentage, oriPercentage, defaultTolerant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkPercentage(Float newPercentage, Float oriPercentage, float tolerant) {
|
||||
if (newPercentage == null || Float.isNaN(newPercentage) || Float.isInfinite(newPercentage)
|
||||
|| oriPercentage == null || Float.isNaN(oriPercentage) || Float.isInfinite(oriPercentage)) {
|
||||
return false;
|
||||
}
|
||||
// 容错值
|
||||
float maxTolerant = oriPercentage + tolerant;
|
||||
float minTolerant = oriPercentage - tolerant;
|
||||
return newPercentage >= minTolerant && newPercentage <= maxTolerant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnyMap generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) {
|
||||
AnyMap map = AnyMap.of(new HashMap<>(8));
|
||||
if (beforeGenerateImageCaptchaValidData(imageCaptchaInfo, map)) {
|
||||
doGenerateImageCaptchaValidData(map, imageCaptchaInfo);
|
||||
}
|
||||
afterGenerateImageCaptchaValidData(imageCaptchaInfo, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
public boolean beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo, AnyMap map) {
|
||||
// 容错值
|
||||
Float tolerant = imageCaptchaInfo.getTolerant();
|
||||
if (tolerant != null && tolerant > 0) {
|
||||
map.put(TOLERANT_KEY, tolerant);
|
||||
}
|
||||
// 类型
|
||||
String type = imageCaptchaInfo.getType();
|
||||
if (ObjectUtils.isEmpty(type)) {
|
||||
type = CaptchaTypeConstant.SLIDER;
|
||||
}
|
||||
map.put(TYPE_KEY, type);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void afterGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo, AnyMap map) {
|
||||
|
||||
}
|
||||
|
||||
public void doGenerateImageCaptchaValidData(AnyMap map,
|
||||
ImageCaptchaInfo imageCaptchaInfo) {
|
||||
// type
|
||||
String type = (String) map.getOrDefault(TYPE_KEY, CaptchaTypeConstant.SLIDER);
|
||||
Object expand = imageCaptchaInfo.getData() == null ? null : imageCaptchaInfo.getData().getExpand();
|
||||
if (CaptchaTypeClassifier.isSliderCaptcha(type)) {
|
||||
// 滑动验证码
|
||||
addPercentage(imageCaptchaInfo, map);
|
||||
} else if (CaptchaTypeClassifier.isClickCaptcha(type)) {
|
||||
// 图片点选验证码
|
||||
if (expand == null) {
|
||||
throw new IllegalArgumentException("点选验证码扩展数据转换为 List<ClickImageCheckDefinition> 失败, info=" + imageCaptchaInfo);
|
||||
}
|
||||
List<ClickImageCheckDefinition> clickImageCheckDefinitionList;
|
||||
try {
|
||||
clickImageCheckDefinitionList = (List<ClickImageCheckDefinition>) expand;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("点选验证码扩展数据转换为 List<ClickImageCheckDefinition> 失败, info=" + imageCaptchaInfo);
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < clickImageCheckDefinitionList.size(); i++) {
|
||||
ClickImageCheckDefinition definition = clickImageCheckDefinitionList.get(i);
|
||||
Integer x = definition.getX();
|
||||
Integer y = definition.getY();
|
||||
Integer width = imageCaptchaInfo.getBackgroundImageWidth();
|
||||
Integer height = imageCaptchaInfo.getBackgroundImageHeight();
|
||||
float vx = calcPercentage(x, width);
|
||||
float vy = calcPercentage(y, height);
|
||||
sb.append(vx).append(",").append(vy).append(";");
|
||||
if (i == 0 && !map.containsKey(TOLERANT_KEY)) {
|
||||
// 重新计算容错值
|
||||
float minLeft = calcPercentage(x - definition.getWidth() / 2f, width);
|
||||
float tolerant = vx - minLeft;
|
||||
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());
|
||||
} else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) {
|
||||
// 拼图验证码
|
||||
map.put(PERCENTAGE_KEY, expand);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse<?> valid(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData) {
|
||||
// 读容错值
|
||||
Float tolerant = recalculateTolerant(imageCaptchaValidData.getFloat(TOLERANT_KEY, defaultTolerant), imageCaptchaTrack, imageCaptchaValidData);
|
||||
// 读验证码类型
|
||||
String type = imageCaptchaValidData.getString(TYPE_KEY, CaptchaTypeConstant.SLIDER);
|
||||
// 验证前
|
||||
// 在验证前必须读取 容错值 和验证码类型
|
||||
ApiResponse<?> beforeValid = beforeValid(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
|
||||
if (!beforeValid.isSuccess()) {
|
||||
return beforeValid;
|
||||
}
|
||||
Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth();
|
||||
if (bgImageWidth == null || bgImageWidth < 1) {
|
||||
// 没有背景图片宽度
|
||||
return ApiResponse.ofCheckError("验证码背景图片宽度参数错误");
|
||||
}
|
||||
List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
|
||||
if (CollectionUtils.isEmpty(trackList)) {
|
||||
// 没有滑动轨迹
|
||||
return ApiResponse.ofCheckError("没有解析到滑动轨迹");
|
||||
}
|
||||
// 验证
|
||||
boolean valid = doValid(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 imageCaptchaTrack
|
||||
* @param captchaValidData captchaValidData
|
||||
* @param tolerant tolerant
|
||||
* @param type type
|
||||
* @return boolean
|
||||
*/
|
||||
public ApiResponse<?> beforeValid(ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) {
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证后
|
||||
*
|
||||
* @param imageCaptchaTrack imageCaptchaTrack
|
||||
* @param captchaValidData captchaValidData
|
||||
* @param tolerant tolerant
|
||||
* @param type type
|
||||
* @return boolean
|
||||
*/
|
||||
public ApiResponse<?> afterValid(Boolean basicValid, ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) {
|
||||
if (!basicValid) {
|
||||
return ApiResponse.ofMessage(ApiResponseStatusConstant.BASIC_CHECK_FAIL);
|
||||
}
|
||||
return ApiResponse.ofSuccess();
|
||||
}
|
||||
|
||||
public boolean doValid(ImageCaptchaTrack imageCaptchaTrack,
|
||||
AnyMap imageCaptchaValidData,
|
||||
Float tolerant,
|
||||
String type) {
|
||||
if (CaptchaTypeClassifier.isSliderCaptcha(type)) {
|
||||
// 滑动类型验证码
|
||||
return doValidSliderCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
|
||||
} else if (CaptchaTypeClassifier.isClickCaptcha(type)) {
|
||||
// 点选类型验证码
|
||||
return doValidClickCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
|
||||
} else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) {
|
||||
// 拼图类型验证码
|
||||
return doValidJigsawCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
|
||||
}
|
||||
// 不支持的类型
|
||||
log.warn("校验验证码警告, 不支持的验证码类型:{}, 请手动扩展 cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator.doValid 进行校验扩展", type);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean doValidJigsawCaptcha(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData, Float tolerant, String type) {
|
||||
if (imageCaptchaTrack.getData() == null || !(imageCaptchaTrack.getData() instanceof String)) {
|
||||
throw new IllegalArgumentException("拼图验证码必须传data数据,且必须是字符串类型逗号分隔数据");
|
||||
}
|
||||
String posArr = (String) imageCaptchaTrack.getData();
|
||||
String successPosStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null);
|
||||
return successPosStr.equals(posArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验点选验证码
|
||||
*
|
||||
* @param imageCaptchaTrack imageCaptchaTrack
|
||||
* @param imageCaptchaValidData imageCaptchaValidData
|
||||
* @param tolerant tolerant
|
||||
* @param type type
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean doValidClickCaptcha(ImageCaptchaTrack imageCaptchaTrack,
|
||||
AnyMap imageCaptchaValidData,
|
||||
Float tolerant,
|
||||
String type) {
|
||||
String validStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null);
|
||||
Object checkOrder = imageCaptchaValidData.getOrDefault(CLICK_IMAGE_CHECK_ORDER_KEY, true);
|
||||
if (ObjectUtils.isEmpty(validStr)) {
|
||||
return false;
|
||||
}
|
||||
String[] splitArr = validStr.split(";");
|
||||
List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
|
||||
if (trackList.size() < splitArr.length) {
|
||||
return false;
|
||||
}
|
||||
// 取出点击事件的轨迹数据
|
||||
List<ImageCaptchaTrack.Track> clickTrackList = trackList
|
||||
.stream()
|
||||
.filter(t -> TrackTypeConstant.CLICK.equalsIgnoreCase(t.getType()))
|
||||
.collect(Collectors.toList());
|
||||
if (clickTrackList.size() != splitArr.length) {
|
||||
return false;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<Double> percentages = new ArrayList<>();
|
||||
for (int i = 0; i < splitArr.length; i++) {
|
||||
String posStr = splitArr[i];
|
||||
String[] posArr = posStr.split(",");
|
||||
float xPercentage = Float.parseFloat(posArr[0]);
|
||||
float yPercentage = Float.parseFloat(posArr[1]);
|
||||
float calcXPercentage = 0f;
|
||||
float calcYPercentage = 0f;
|
||||
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)
|
||||
|| !checkPercentage(calcYPercentage, yPercentage, tolerant)) {
|
||||
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) {
|
||||
sb.append("|");
|
||||
}
|
||||
sb.append(calcXPercentage).append(",").append(calcYPercentage);
|
||||
percentages.add((double) ((calcXPercentage - xPercentage) + (calcYPercentage - yPercentage)));
|
||||
}
|
||||
// 存储一下当前计算出来的值
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验滑动验证码
|
||||
*
|
||||
* @param imageCaptchaTrack imageCaptchaTrack
|
||||
* @param imageCaptchaValidData imageCaptchaValidData
|
||||
* @param tolerant tolerant
|
||||
* @param type type
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean doValidSliderCaptcha(ImageCaptchaTrack imageCaptchaTrack,
|
||||
AnyMap imageCaptchaValidData,
|
||||
Float tolerant,
|
||||
String type) {
|
||||
Float oriPercentage = imageCaptchaValidData.getFloat(PERCENTAGE_KEY);
|
||||
if (oriPercentage == null) {
|
||||
// 没读取到百分比
|
||||
return false;
|
||||
}
|
||||
List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
|
||||
ImageCaptchaTrack.Track firstTrack = trackList.get(0);
|
||||
// 取最后一个滑动轨迹
|
||||
ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1);
|
||||
// 计算百分比
|
||||
float calcPercentage = calcPercentage(lastTrack.getX() - firstTrack.getX(), imageCaptchaTrack.getBgImageWidth());
|
||||
// 校验百分比
|
||||
boolean percentage = checkPercentage(calcPercentage, oriPercentage, tolerant);
|
||||
if (percentage) {
|
||||
// 校验成功
|
||||
// 存储一下当前计算出来的值
|
||||
imageCaptchaValidData.put(USER_CURRENT_PERCENTAGE, String.valueOf(calcPercentage));
|
||||
imageCaptchaValidData.put(USER_CURRENT_PERCENTAGE_STD, String.valueOf(calcPercentage - oriPercentage));
|
||||
}
|
||||
return percentage;
|
||||
}
|
||||
|
||||
protected void addPercentage(ImageCaptchaInfo imageCaptchaInfo, AnyMap imageCaptchaValidData) {
|
||||
float percentage = calcPercentage(imageCaptchaInfo.getRandomX(), imageCaptchaInfo.getBackgroundImageWidth());
|
||||
imageCaptchaValidData.put(PERCENTAGE_KEY, percentage);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,71 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.application.DefaultImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.application.ImageCaptchaProperties;
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.cache.CacheStore;
|
||||
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptorGroup;
|
||||
import cloud.tianai.captcha.interceptor.impl.BasicTrackCaptchaInterceptor;
|
||||
import cloud.tianai.captcha.interceptor.impl.ParamCheckCaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ApplicationTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ImageCaptchaApplication application = createImageCaptchaApplication();
|
||||
// 生成验证码数据, 可以将该数据直接返回给前端 , 可配合 tianai-captcha-web-sdk 使用
|
||||
ApiResponse<ImageCaptchaVO> res = application.generateCaptcha("SLIDER");
|
||||
System.out.println(res);
|
||||
|
||||
// 校验验证码, ImageCaptchaTrack 和 id 均为前端传开的参数, 可将 valid数据直接返回给 前端
|
||||
// 注意: 该项目只负责生成和校验验证码数据, 至于二次验证等需要自行扩展
|
||||
String id =res.getData().getId();
|
||||
ImageCaptchaTrack imageCaptchaTrack = null;
|
||||
ApiResponse<?> valid = application.matching(id, imageCaptchaTrack);
|
||||
System.out.println(valid.isSuccess());
|
||||
|
||||
|
||||
// 扩展: 一个简单的二次验证
|
||||
CacheStore cacheStore = new LocalCacheStore();
|
||||
if (valid.isSuccess()) {
|
||||
// 如果验证成功,生成一个token并存储, 将该token返回给客户端,客户端下次请求数据时携带该token, 后台判断是否有效
|
||||
String token = UUID.randomUUID().toString();
|
||||
cacheStore.setCache(token, new AnyMap(), 5L, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static ImageCaptchaApplication createImageCaptchaApplication() {
|
||||
// 验证码资源管理器 该类负责管理验证码背景图和模板图等数据
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
// 验证码生成器; 注意: 生成器必须调用init(...)初始化方法 true为加载默认资源,false为不加载,
|
||||
ImageCaptchaGenerator generator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager).init();
|
||||
// 验证码校验器
|
||||
ImageCaptchaValidator imageCaptchaValidator = new SimpleImageCaptchaValidator();
|
||||
// 缓存, 用于存放校验数据
|
||||
CacheStore cacheStore = new LocalCacheStore();
|
||||
// 验证码拦截器, 可以是单个,也可以是一组拦截器,可以嵌套, 这里演示加载参数校验拦截,和 滑动轨迹拦截
|
||||
CaptchaInterceptorGroup group = new CaptchaInterceptorGroup();
|
||||
group.addInterceptor(new ParamCheckCaptchaInterceptor());
|
||||
group.addInterceptor(new BasicTrackCaptchaInterceptor());
|
||||
|
||||
ImageCaptchaProperties prop = new ImageCaptchaProperties();
|
||||
// application 验证码封装, prop为所需的一些扩展参数
|
||||
ImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, imageCaptchaValidator, cacheStore, prop, group);
|
||||
return application;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.ImageTransform;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 基础 SimpleDemo
|
||||
*/
|
||||
public class SimpleDemo {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
ImageTransform imageTransform = new Base64ImageTransform();
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init();
|
||||
BasicCaptchaTrackValidator imageCaptchaValidator = new BasicCaptchaTrackValidator();
|
||||
// 注意: 上面这个四个对象都是单例的, 整个项目创建一次即可
|
||||
|
||||
// 这里生成一个滑块验证码数据, 里面包括背景图、滑块图等等,按需传给前端进行展示
|
||||
ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
|
||||
|
||||
// 这个数据是根据当前生成的这条验证码数据生成对应的验证数据, 该数据要存到缓存中
|
||||
AnyMap map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo);
|
||||
|
||||
|
||||
|
||||
// 这是用户移动滑块后的校验接口
|
||||
// imageCaptchaTrack 对象为前端传来的滑动轨迹数据, 这里进行验证滑块, 返回 true 说明校验通过
|
||||
ImageCaptchaTrack imageCaptchaTrack = null;
|
||||
boolean check = imageCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.application.ImageCaptchaProperties;
|
||||
import cloud.tianai.captcha.application.TACBuilder;
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
|
||||
public class TACBuilderTest {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Font font= null;
|
||||
// ResourceMap template1 = new ResourceMap("default", 4);
|
||||
// template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/active.png"));
|
||||
// template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/fixed.png"));
|
||||
|
||||
ImageCaptchaApplication application = TACBuilder.builder(new LocalMemoryResourceStore())
|
||||
// 加载系统自带的默认资源
|
||||
.addDefaultTemplate()
|
||||
// 设置验证码过期时间
|
||||
.expire("default", 10000L)
|
||||
.expire("WORD_IMAGE_CLICK", 60000L)
|
||||
// 设置拦截器
|
||||
.setInterceptor(EmptyCaptchaInterceptor.INSTANCE)
|
||||
// 添加验证码背景图片
|
||||
.addResource("SLIDER", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
|
||||
.addResource("WORD_IMAGE_CLICK", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
|
||||
.addResource("ROTATE", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
|
||||
// 添加验证码模板图片
|
||||
// .addTemplate("SLIDER",template1)
|
||||
// 设置缓冲器,可提前生成验证码,用于增加并发性
|
||||
.cached(10, 1000, 5000, 10000L)
|
||||
// 添加字体包,用于给文字点选验证码提供字体
|
||||
.addFont(new Resource("file", "C:\\Users\\Thinkpad\\Desktop\\captcha\\手写字体\\ttf\\千图小兔体.ttf"))
|
||||
// 设置缓存存储器,如果要支持分布式,需要把这里改成分布式缓存,比如通过redis实现的 CacheStore 缓存
|
||||
.setCacheStore(new LocalCacheStore())
|
||||
// 设置资源存储器,如果想在分布式环境或者想统一管理以及扩展 实现 ResourceStore 接口,自定义
|
||||
// .setResourceStore(new LocalMemoryResourceStore())
|
||||
// 图片转换器,默认是将图片转换成base64格式, 背景图为jpg, 模板图为png, 如果想要扩展,可替换成自己实现的
|
||||
.setTransform(new Base64ImageTransform())
|
||||
.build();
|
||||
|
||||
while (true){
|
||||
long start = System.currentTimeMillis();
|
||||
ApiResponse<ImageCaptchaVO> response = application.generateCaptcha("SLIDER");
|
||||
System.out.println("耗时:" + (System.currentTimeMillis() - start));
|
||||
// System.out.println(response);
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
// System.out.println(response);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.application.TACBuilder;
|
||||
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
|
||||
import cloud.tianai.captcha.interceptor.Context;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TACBuilderTest2 {
|
||||
|
||||
public static void main(String[] args) throws IOException, FontFormatException {
|
||||
ImageCaptchaApplication application = TACBuilder.builder()
|
||||
.addDefaultTemplate()
|
||||
.expire("default", 10000L)
|
||||
.expire("WORD_IMAGE_CLICK", 60000L)
|
||||
.addResource("SLIDER", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
|
||||
.addResource("WORD_IMAGE_CLICK", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
|
||||
.addResource("ROTATE", new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
|
||||
.build();
|
||||
ApiResponse<ImageCaptchaVO> response = application.generateCaptcha("SLIDER");
|
||||
System.out.println(response);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
|
||||
import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class Test {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
Base64ImageTransform imageTransform = new Base64ImageTransform();
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init();
|
||||
/*
|
||||
生成滑块验证码图片, 可选项
|
||||
SLIDER (滑块验证码)
|
||||
ROTATE (旋转验证码)
|
||||
CONCAT (滑动还原验证码)
|
||||
WORD_IMAGE_CLICK (文字点选验证码)
|
||||
|
||||
更多验证码支持 详见 cloud.tianai.captcha.common.constant.CaptchaTypeConstant
|
||||
*/
|
||||
ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
|
||||
System.out.println(imageCaptchaInfo);
|
||||
|
||||
// 负责计算一些数据存到缓存中,用于校验使用
|
||||
// ImageCaptchaValidator负责校验用户滑动滑块是否正确和生成滑块的一些校验数据; 比如滑块到凹槽的百分比值
|
||||
ImageCaptchaValidator imageCaptchaValidator = new BasicCaptchaTrackValidator();
|
||||
// 这个map数据应该存到缓存中,校验的时候需要用到该数据
|
||||
Map<String, Object> map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.AnyMap;
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class Test2 {
|
||||
public static void main(String[] args) {
|
||||
BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator();
|
||||
|
||||
ImageCaptchaTrack imageCaptchaTrack = null;
|
||||
AnyMap map = null;
|
||||
Float percentage = null;
|
||||
// 用户传来的行为轨迹和进行校验
|
||||
// - imageCaptchaTrack为前端传来的滑动轨迹数据
|
||||
// - map 为生成验证码时缓存的map数据
|
||||
boolean check = sliderCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess();
|
||||
// // 如果只想校验用户是否滑到指定凹槽即可,也可以使用
|
||||
// // - 参数1 用户传来的百分比数据
|
||||
// // - 参数2 生成滑块是真实的百分比数据
|
||||
check = sliderCaptchaValidator.checkPercentage(0.2f, percentage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
|
||||
public class Test3 {
|
||||
public static void main(String[] args) {
|
||||
// 资源管理器
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
Base64ImageTransform imageTransform = new Base64ImageTransform();
|
||||
// 标准验证码生成器
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init();
|
||||
// 生成 具有混淆的 滑块验证码 (目前只有滑块验证码支持混淆滑块, 旋转验证,滑动还原,点选验证 均不支持混淆功能)
|
||||
ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(GenerateParam.builder()
|
||||
// 设置验证码类型
|
||||
.type(CaptchaTypeConstant.SLIDER)
|
||||
.templateFormatName("jpeg")
|
||||
// 设置背景图片格式
|
||||
.backgroundFormatName("png")
|
||||
// 是否添加混淆滑块
|
||||
.obfuscate(true)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
|
||||
public class Test4 {
|
||||
public static void main(String[] args) {
|
||||
// 资源管理器
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
// 标准验证码生成器
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,new Base64ImageTransform()).init();
|
||||
// 生成旋转验证码 图片类型为 webp
|
||||
// 注意 tianai-captcha 后面默认删除了生成webp格式图片需要用户自定义添加webp转换的工具,需要用户自定义添加和扩展
|
||||
// 参考 https://bitbucket.org/luciad/webp-imageio
|
||||
ImageCaptchaInfo slideImageInfo = imageCaptchaGenerator.generateCaptchaImage(GenerateParam.builder()
|
||||
.type(CaptchaTypeConstant.ROTATE)
|
||||
.templateFormatName("webp")
|
||||
.backgroundFormatName("webp")
|
||||
.build());
|
||||
System.out.println(slideImageInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.resource.CrudResourceStore;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
|
||||
public class Test6 {
|
||||
public static void main(String[] args) {
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
// 通过资源管理器或者资源存储器
|
||||
CrudResourceStore resourceStore = (CrudResourceStore) imageCaptchaResourceManager.getResourceStore();
|
||||
// 添加滑块验证码模板.模板图片由三张图片组成
|
||||
ResourceMap template1 = new ResourceMap("default", 4);
|
||||
template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/active.png"));
|
||||
template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/fixed.png"));
|
||||
resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template1);
|
||||
// 模板与三张图片组成 滑块、凹槽、背景图
|
||||
// 同样默认支持 classpath 和 url 两种获取图片资源, 如果想扩展可实现 ResourceProvider 接口,进行自定义扩展
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceProvider;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class Test7 {
|
||||
public static void main(String[] args) {
|
||||
// 自定义 ResourceProvider
|
||||
ResourceProvider resourceProvider = new ResourceProvider() {
|
||||
@Override
|
||||
public InputStream getResourceInputStream(Resource data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supported(Resource type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,new Base64ImageTransform()).init();
|
||||
// 注册
|
||||
imageCaptchaResourceManager.registerResourceProvider(resourceProvider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
|
||||
import cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
|
||||
import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
|
||||
public class Test8 {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
// 使用 CacheSliderCaptchaGenerator 对滑块验证码进行缓存,使其提前生成滑块图片
|
||||
// 参数一: 真正实现 滑块的 SliderCaptchaGenerator
|
||||
// 参数二: 默认提前缓存多少个
|
||||
// 参数三: 出错后 等待xx时间再进行生成
|
||||
// 参数四: 检查时间间隔
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
ImageCaptchaGenerator imageCaptchaGenerator = new CacheImageCaptchaGenerator(new MultiImageCaptchaGenerator(imageCaptchaResourceManager,new Base64ImageTransform()), 10, 1000, 100);
|
||||
imageCaptchaGenerator.init();
|
||||
// 生成滑块图片
|
||||
ImageCaptchaInfo slideImageInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
|
||||
// 获取背景图片的base64
|
||||
String backgroundImage = slideImageInfo.getBackgroundImage();
|
||||
// 获取滑块图片
|
||||
String sliderImage = slideImageInfo.getTemplateImage();
|
||||
System.out.println(slideImageInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package example.readme;
|
||||
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.resource.CrudResourceStore;
|
||||
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.ResourceStore;
|
||||
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||
import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager;
|
||||
import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
|
||||
import cloud.tianai.captcha.resource.impl.provider.URLResourceProvider;
|
||||
|
||||
/**
|
||||
* 图片验证码测试
|
||||
*/
|
||||
public class TestImageCaptcha {
|
||||
public static void main(String[] args) {
|
||||
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
|
||||
// 通过资源管理器或者资源存储器
|
||||
CrudResourceStore resourceStore = (CrudResourceStore) imageCaptchaResourceManager.getResourceStore();
|
||||
// 添加classpath目录下的 aa.jpg 图片
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource(ClassPathResourceProvider.NAME, "/aa.jpg"));
|
||||
// 添加远程url图片资源
|
||||
resourceStore.addResource(CaptchaTypeConstant.SLIDER,new Resource(URLResourceProvider.NAME, "http://www.xx.com/aa.jpg"));
|
||||
// 内置了通过url 和 classpath读取图片资源,如果想扩展可实现 ResourceProvider 接口,进行自定义扩展
|
||||
}
|
||||
}
|
||||