diff --git a/src/main/java/cloud/tianai/captcha/application/CaptchaImageType.java b/src/main/java/cloud/tianai/captcha/application/CaptchaImageType.java new file mode 100644 index 0000000..dc133f9 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/application/CaptchaImageType.java @@ -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; + } +} diff --git a/src/main/java/cloud/tianai/captcha/application/DefaultImageCaptchaApplication.java b/src/main/java/cloud/tianai/captcha/application/DefaultImageCaptchaApplication.java new file mode 100644 index 0000000..bc9af2f --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/application/DefaultImageCaptchaApplication.java @@ -0,0 +1,297 @@ +package cloud.tianai.captcha.application; + +import cloud.tianai.captcha.application.vo.CaptchaResponse; +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.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.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; + setImageCaptchaGenerator(captchaGenerator); + 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); + } + + @Override + public CaptchaResponse generateCaptcha() { + // 生成滑块验证码 + return generateCaptcha(CaptchaTypeConstant.SLIDER); + } + + @Override + public CaptchaResponse generateCaptcha(String type) { + GenerateParam generateParam = new GenerateParam(); + generateParam.setType(type); + return generateCaptcha(generateParam); + } + + @Override + public CaptchaResponse generateCaptcha(GenerateParam param) { + CaptchaResponse captchaResponse = beforeGenerateCaptcha(param); + if (captchaResponse != null) { + return captchaResponse; + } + ImageCaptchaInfo imageCaptchaInfo = getImageCaptchaGenerator().generateCaptchaImage(param); + captchaResponse = convertToCaptchaResponse(imageCaptchaInfo); + afterGenerateCaptcha(imageCaptchaInfo, captchaResponse); + return captchaResponse; + } + + @Override + public CaptchaResponse generateCaptcha(CaptchaImageType captchaImageType) { + return generateCaptcha(CaptchaTypeConstant.SLIDER, captchaImageType); + } + + @Override + public CaptchaResponse 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 CaptchaResponse convertToCaptchaResponse(ImageCaptchaInfo imageCaptchaInfo) { + if (imageCaptchaInfo == null) { + // 要是生成失败 + throw new ImageCaptchaException("生成验证码失败,验证码生成为空"); + } + // 生成ID + String id = generatorId(imageCaptchaInfo); + CaptchaResponse 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()); + return CaptchaResponse.of(id, verificationVO); + } + + + @Override + public ApiResponse matching(String id, ImageCaptchaTrack imageCaptchaTrack) { + AnyMap validData = getVerification(id); + if (validData == null) { + return ApiResponse.ofMessage(ApiResponseStatusConstant.EXPIRED); + } + ApiResponse response = beforeValid(id, imageCaptchaTrack, validData); + if (!response.isSuccess()) { + return response; + } + ApiResponse basicValid = getImageCaptchaValidator().valid(imageCaptchaTrack, validData); + response = afterValid(id, imageCaptchaTrack, validData, basicValid); + if (!response.isSuccess()) { + return response; + } + return basicValid; + } + + + @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.captchaGenerator = captchaGenerator; + } + + @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, CaptchaResponse captchaResponse) { + captchaInterceptor.afterGenerateCaptcha(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, captchaResponse); + } + + private CaptchaResponse beforeGenerateCaptcha(GenerateParam param) { + return captchaInterceptor.beforeGenerateCaptcha(captchaInterceptor.createContext(), param.getType(), param); + } + + private CaptchaResponse 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, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) { + return captchaInterceptor.beforeValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), imageCaptchaTrack, validData); + } + + private ApiResponse afterValid(String id, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData, ApiResponse basicValid) { + return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), imageCaptchaTrack, validData, basicValid); + } + +} diff --git a/src/main/java/cloud/tianai/captcha/application/FilterImageCaptchaApplication.java b/src/main/java/cloud/tianai/captcha/application/FilterImageCaptchaApplication.java new file mode 100644 index 0000000..224f891 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/application/FilterImageCaptchaApplication.java @@ -0,0 +1,112 @@ +package cloud.tianai.captcha.application; + +import cloud.tianai.captcha.application.vo.CaptchaResponse; +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; + +/** + * @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 CaptchaResponse generateCaptcha() { + return target.generateCaptcha(); + } + + @Override + public CaptchaResponse generateCaptcha(String type) { + return target.generateCaptcha(type); + } + + @Override + public CaptchaResponse generateCaptcha(CaptchaImageType captchaImageType) { + return target.generateCaptcha(captchaImageType); + } + + @Override + public CaptchaResponse generateCaptcha(String type, CaptchaImageType captchaImageType) { + return target.generateCaptcha(type, captchaImageType); + } + + @Override + public CaptchaResponse generateCaptcha(GenerateParam param) { + return target.generateCaptcha(param); + } + + @Override + public ApiResponse matching(String id, ImageCaptchaTrack ImageCaptchaTrack) { + return target.matching(id, ImageCaptchaTrack); + } + + @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(); + } +} diff --git a/src/main/java/cloud/tianai/captcha/application/ImageCaptchaApplication.java b/src/main/java/cloud/tianai/captcha/application/ImageCaptchaApplication.java new file mode 100644 index 0000000..81259bd --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/application/ImageCaptchaApplication.java @@ -0,0 +1,152 @@ +package cloud.tianai.captcha.application; + + +import cloud.tianai.captcha.application.vo.CaptchaResponse; +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; + +/** + * @Author: 天爱有情 + * @Date 2020/5/29 8:33 + * @Description 滑块验证码应用程序 + */ +public interface ImageCaptchaApplication { + + /** + * 生成滑块验证码 + * + * @return + */ + CaptchaResponse generateCaptcha(); + + /** + * 生成滑块验证码 + * + * @param type type类型 + * @return CaptchaResponse + */ + CaptchaResponse generateCaptcha(String type); + + /** + * 生成滑块验证码 + * + * @param captchaImageType 要生成webp还是jpg类型的图片 + * @return CaptchaResponse + */ + CaptchaResponse generateCaptcha(CaptchaImageType captchaImageType); + + /** + * 生成验证码 + * + * @param type type + * @param captchaImageType CaptchaImageType + * @return CaptchaResponse + */ + CaptchaResponse generateCaptcha(String type, CaptchaImageType captchaImageType); + + + /** + * 生成滑块验证码 + * + * @param param param + * @return CaptchaResponse + */ + CaptchaResponse generateCaptcha(GenerateParam param); + + /** + * 匹配 + * + * @param id 验证码的ID + * @param imageCaptchaTrack 滑动轨迹 + * @return 匹配成功返回true, 否则返回false + */ + ApiResponse matching(String id, ImageCaptchaTrack imageCaptchaTrack); + + /** + * 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, ImageCaptchaTrack)} + * + * @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(); + +} diff --git a/src/main/java/cloud/tianai/captcha/application/ImageCaptchaProperties.java b/src/main/java/cloud/tianai/captcha/application/ImageCaptchaProperties.java new file mode 100644 index 0000000..654d098 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/application/ImageCaptchaProperties.java @@ -0,0 +1,19 @@ +package cloud.tianai.captcha.application; + +import lombok.Data; + +import java.util.Collections; +import java.util.Map; + +/** + * @Author: 天爱有情 + * @date 2020/10/19 18:41 + * @Description 滑块验证码属性 + */ +@Data +public class ImageCaptchaProperties { + /** 过期key prefix. */ + private String prefix = "captcha"; + /** 过期时间. */ + private Map expire = Collections.emptyMap(); +} diff --git a/src/main/java/cloud/tianai/captcha/application/vo/CaptchaResponse.java b/src/main/java/cloud/tianai/captcha/application/vo/CaptchaResponse.java new file mode 100644 index 0000000..a309fc8 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/application/vo/CaptchaResponse.java @@ -0,0 +1,24 @@ +package cloud.tianai.captcha.application.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @Author: 天爱有情 + * @Date 2020/5/29 8:31 + * @Description 验证码返回对象 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CaptchaResponse implements Serializable { + private String id; + private T captcha; + + public static CaptchaResponse of(String id, T data) { + return new CaptchaResponse(id, data); + } +} diff --git a/src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java b/src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java new file mode 100644 index 0000000..0754012 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java @@ -0,0 +1,33 @@ +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 { + /** 验证码类型.*/ + 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; +} diff --git a/src/main/java/cloud/tianai/captcha/cache/CacheStore.java b/src/main/java/cloud/tianai/captcha/cache/CacheStore.java new file mode 100644 index 0000000..a27aee4 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/cache/CacheStore.java @@ -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); +} diff --git a/src/main/java/cloud/tianai/captcha/cache/impl/ConCurrentExpiringMap.java b/src/main/java/cloud/tianai/captcha/cache/impl/ConCurrentExpiringMap.java new file mode 100644 index 0000000..0ee9359 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/cache/impl/ConCurrentExpiringMap.java @@ -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 implements ExpiringMap { + + private ConcurrentHashMap> storage; + private SortedMap> 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 put(K k, V v, Long expire, TimeUnit timeUnit) { + if (expire == null || expire < 1) { + expire = DEFAULT_EXPIRE; + } + TimeMapEntity 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 old = storage.put(k, entity); + return old; + } + + @Override + public Optional> 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> entityOptional = getData(k); + if (!entityOptional.isPresent()) { + return false; + } + synchronized (k) { + // 双重校验 + entityOptional = getData(k); + if (!entityOptional.isPresent()) { + return false; + } + TimeMapEntity entity = entityOptional.get(); + + TimeMapEntity 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> values = storage.values(); + Optional> any = values.stream().filter(v -> v.getValue().equals(value)).findAny(); + return any.isPresent(); + } + + @Override + public V get(Object key) { + TimeMapEntity timeMapEntity = storage.get(key); + if (isTimeout(timeMapEntity)) { + removeData(key); + return null; + } + return timeMapEntity.getValue(); + } + + protected boolean isTimeout(K key) { + Optional> data = getData(key); + return isTimeout(data.orElse(null)); + } + + protected boolean isTimeout(TimeMapEntity 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> removeData(Object key) { + synchronized (key) { + TimeMapEntity oldValue = storage.get(key); + if (oldValue != null) { + TimeMapEntity entity = storage.remove(key); + Long expire = oldValue.getExpire(); + if (expire != null && expire > 0) { + LinkedList 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 m) { + m.forEach(this::put); + } + + @Override + public void clear() { + Map> copyStorage = new HashMap<>(storage); + storage.clear(); + sortedMap.clear(); + } + + /** + * 这个可能会消耗点cpu + * + * @return + */ + @Override + public Set keySet() { + return storage.keySet() + .stream() + .parallel() + .filter(k -> !isTimeout(k)) + .collect(Collectors.toSet()); + } + + @Override + public Collection values() { + return storage.values().stream().map(TimeMapEntity::getValue).collect(Collectors.toSet()); + } + + @Override + public Set> entrySet() { + throw new IllegalArgumentException("timemap not impl entrySet."); + } + + /** + * 定时执行任务 + * + * @since 0.0.3 + */ + private class ExpireThread implements Runnable { + @Override + public void run() { + SortedMap> 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 removeKeys = null; + // 删除的逻辑处理 + long currentTime = System.nanoTime(); + if (currentTime < expireMap.firstKey()) { + return; + } + for (Entry> entry : expireMap.entrySet()) { + final Long expireAt = entry.getKey(); + LinkedList 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 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); + } + } + } + } +} diff --git a/src/main/java/cloud/tianai/captcha/cache/impl/ExpiringMap.java b/src/main/java/cloud/tianai/captcha/cache/impl/ExpiringMap.java new file mode 100644 index 0000000..2fbb77c --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/cache/impl/ExpiringMap.java @@ -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 extends Map { + /** + * 默认-1 无超时时间. + */ + Long DEFAULT_EXPIRE = -1L; + + /** + * 添加值 + * @param k key + * @param v value + * @param timeout 超时时间, + * @param timeUnit 超时时间单位 + * @return 返回旧的数据,如果没有,返回null + */ + TimeMapEntity put(K k, V v, Long timeout, TimeUnit timeUnit); + + /** + * 获取value值 + * @param k key + * @return + */ + Optional> 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 { + 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 entity) { + this.key = entity.getKey(); + this.value = entity.getValue(); + this.expire = entity.getExpire(); + this.createTime = entity.getCreateTime(); + } + } +} diff --git a/src/main/java/cloud/tianai/captcha/cache/impl/LocalCacheStore.java b/src/main/java/cloud/tianai/captcha/cache/impl/LocalCacheStore.java new file mode 100644 index 0000000..cf3c546 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/cache/impl/LocalCacheStore.java @@ -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 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 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 stringObjectMap = cache.get(key); + if (stringObjectMap != null) { + return (Long) stringObjectMap.get("___incr___"); + } + return null; + } +} diff --git a/src/main/java/cloud/tianai/captcha/common/AnyMap.java b/src/main/java/cloud/tianai/captcha/common/AnyMap.java new file mode 100644 index 0000000..ca7bbfd --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/common/AnyMap.java @@ -0,0 +1,193 @@ +package cloud.tianai.captcha.common; + +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; + +public class AnyMap implements Map { + + private Map target; + + public AnyMap() { + target = new LinkedHashMap<>(); + } + + public AnyMap(Map 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 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 m) { + target.putAll(m); + } + + @Override + public void clear() { + target.clear(); + } + + @Override + public Set keySet() { + return target.keySet(); + } + + @Override + public Collection values() { + return target.values(); + } + + @Override + public Set> entrySet() { + return target.entrySet(); + } + + @Override + public Object getOrDefault(Object key, Object defaultValue) { + return target.getOrDefault(key, defaultValue); + } + + @Override + public void forEach(BiConsumer action) { + target.forEach(action); + } + + @Override + public void replaceAll(BiFunction 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 mappingFunction) { + return target.computeIfAbsent(key, mappingFunction); + } + + @Override + public Object computeIfPresent(String key, BiFunction remappingFunction) { + return target.computeIfPresent(key, remappingFunction); + } + + @Override + public Object compute(String key, BiFunction remappingFunction) { + return target.compute(key, remappingFunction); + } + + @Override + public Object merge(String key, Object value, BiFunction remappingFunction) { + return target.merge(key, value, remappingFunction); + } +} diff --git a/src/main/java/cloud/tianai/captcha/generator/AbstractImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/AbstractImageCaptchaGenerator.java index dd3c567..87e0867 100644 --- a/src/main/java/cloud/tianai/captcha/generator/AbstractImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/AbstractImageCaptchaGenerator.java @@ -8,6 +8,7 @@ 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; @@ -23,8 +24,6 @@ import java.util.Collection; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; -import static cloud.tianai.captcha.generator.impl.StaticCaptchaPostProcessorManager.*; - /** * @Author: 天爱有情 * @date 2022/4/22 16:30 @@ -50,6 +49,8 @@ public abstract class AbstractImageCaptchaGenerator implements ImageCaptchaGener /** 图片转换器. */ protected ImageTransform imageTransform; + protected CaptchaInterceptor interceptor; + @Getter private boolean init = false; @@ -102,17 +103,36 @@ public abstract class AbstractImageCaptchaGenerator implements ImageCaptchaGener assertInit(); CustomData data = new CustomData(); CaptchaExchange captchaExchange = CaptchaExchange.create(data, param); - ImageCaptchaInfo imageCaptchaInfo = applyPostProcessorBeforeGenerate(captchaExchange, this); + ImageCaptchaInfo imageCaptchaInfo = beforeGenerate(captchaExchange); if (imageCaptchaInfo != null) { return imageCaptchaInfo; } doGenerateCaptchaImage(captchaExchange); - applyPostProcessorBeforeWrapImageCaptchaInfo(captchaExchange, this); + beforeWrapImageCaptchaInfo(captchaExchange); imageCaptchaInfo = wrapImageCaptchaInfo(captchaExchange); - applyPostProcessorAfterGenerateCaptchaImage(captchaExchange, imageCaptchaInfo, this); + 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()); @@ -250,4 +270,14 @@ public abstract class AbstractImageCaptchaGenerator implements ImageCaptchaGener public void setImageTransform(ImageTransform imageTransform) { this.imageTransform = imageTransform; } + + @Override + public CaptchaInterceptor getInterceptor() { + return interceptor; + } + + @Override + public void setInterceptor(CaptchaInterceptor interceptor) { + this.interceptor = interceptor; + } } diff --git a/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGenerator.java index 8b88dbb..25bf2eb 100644 --- a/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGenerator.java @@ -3,6 +3,7 @@ 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; /** @@ -78,4 +79,9 @@ public interface ImageCaptchaGenerator { */ void setImageTransform(ImageTransform imageTransform); + + CaptchaInterceptor getInterceptor(); + + void setInterceptor(CaptchaInterceptor interceptor); + } diff --git a/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGeneratorProvider.java b/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGeneratorProvider.java index d6a6e83..dfa3236 100644 --- a/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGeneratorProvider.java +++ b/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGeneratorProvider.java @@ -1,6 +1,7 @@ package cloud.tianai.captcha.generator; +import cloud.tianai.captcha.interceptor.CaptchaInterceptor; import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; /** @@ -17,7 +18,7 @@ public interface ImageCaptchaGeneratorProvider { * @param imageTransform imageTransform * @return ImageCaptchaGenerator */ - ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform); + ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor); /** * 验证码类型 diff --git a/src/main/java/cloud/tianai/captcha/generator/common/model/dto/CustomData.java b/src/main/java/cloud/tianai/captcha/generator/common/model/dto/CustomData.java index a31c7eb..f29619c 100644 --- a/src/main/java/cloud/tianai/captcha/generator/common/model/dto/CustomData.java +++ b/src/main/java/cloud/tianai/captcha/generator/common/model/dto/CustomData.java @@ -1,10 +1,8 @@ package cloud.tianai.captcha.generator.common.model.dto; +import cloud.tianai.captcha.common.AnyMap; import lombok.Data; -import java.util.HashMap; -import java.util.Map; - /** * @Author: 天爱有情 * @date 2023/4/24 10:27 @@ -14,9 +12,9 @@ import java.util.Map; public class CustomData { /** 透传字段,用于传给前端. */ - private Map viewData; + private AnyMap viewData; /** 内部使用的字段数据. */ - private Map data; + private AnyMap data; /** * 扩展字段 */ @@ -24,14 +22,14 @@ public class CustomData { public void putViewData(String key, Object data) { if (this.viewData == null) { - this.viewData = new HashMap<>(); + this.viewData = new AnyMap(); } this.viewData.put(key, data); } public void putData(String key, Object data) { if (this.data == null) { - this.data = new HashMap<>(); + this.data = new AnyMap(); } this.data.put(key, data); } diff --git a/src/main/java/cloud/tianai/captcha/generator/common/model/dto/GenerateParam.java b/src/main/java/cloud/tianai/captcha/generator/common/model/dto/GenerateParam.java index d96d3ea..6b02c6c 100644 --- a/src/main/java/cloud/tianai/captcha/generator/common/model/dto/GenerateParam.java +++ b/src/main/java/cloud/tianai/captcha/generator/common/model/dto/GenerateParam.java @@ -1,11 +1,9 @@ package cloud.tianai.captcha.generator.common.model.dto; +import cloud.tianai.captcha.common.AnyMap; import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; import lombok.*; -import java.util.HashMap; -import java.util.Map; - /** * @Author: 天爱有情 * @date 2022/2/11 9:44 @@ -32,7 +30,7 @@ public class GenerateParam { /** 滑动图片标签,用户二级过滤模板图片,或指定某模板图片.. */ private String templateImageTag; /** 扩展参数. */ - private Map param = new HashMap<>(4); + private AnyMap param = new AnyMap(); public void addParam(String key, Object value) { doGetOrCreateParam().put(key, value); @@ -42,9 +40,9 @@ public class GenerateParam { return param == null ? null : param.get(key); } - private Map doGetOrCreateParam() { + private AnyMap doGetOrCreateParam() { if (param == null) { - param = new HashMap<>(4); + param = new AnyMap(); } return param; } diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/CacheImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/impl/CacheImageCaptchaGenerator.java index 3d5ee13..90f26a4 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/CacheImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/CacheImageCaptchaGenerator.java @@ -5,6 +5,7 @@ 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; @@ -198,4 +199,14 @@ public class CacheImageCaptchaGenerator implements ImageCaptchaGenerator { public void setImageTransform(ImageTransform imageTransform) { target.setImageTransform(imageTransform); } + + @Override + public CaptchaInterceptor getInterceptor() { + return target.getInterceptor(); + } + + @Override + public void setInterceptor(CaptchaInterceptor interceptor) { + target.setInterceptor(interceptor); + } } diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java index 6c7fbc1..6af1c8a 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java @@ -110,7 +110,7 @@ public class MultiImageCaptchaGenerator extends AbstractImageCaptchaGenerator { if (provider == null) { throw new IllegalArgumentException("生成验证码失败,错误的type类型:" + t); } - return provider.get(getImageResourceManager(), getImageTransform()).init(initDefaultResource); + return provider.get(getImageResourceManager(), getImageTransform(), getInterceptor()).init(initDefaultResource); }); return imageCaptchaGenerator; } diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/SliderImageCaptchaV2Generator.java b/src/main/java/cloud/tianai/captcha/generator/impl/SliderImageCaptchaV2Generator.java new file mode 100644 index 0000000..237fd30 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/generator/impl/SliderImageCaptchaV2Generator.java @@ -0,0 +1,167 @@ +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.CaptchaExchange; +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.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.provider.ClassPathResourceProvider; +import lombok.SneakyThrows; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.FileOutputStream; +import java.util.UUID; + +import static cloud.tianai.captcha.common.constant.CommonConstant.*; + +public class SliderImageCaptchaV2Generator 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 SliderImageCaptchaV2Generator(ImageCaptchaResourceManager imageCaptchaResourceManager) { + super(imageCaptchaResourceManager); + } + + public SliderImageCaptchaV2Generator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) { + super(imageCaptchaResourceManager); + setImageTransform(imageTransform); + } + + @Override + protected void doInit(boolean initDefaultResource) { + if (initDefaultResource) { + initDefaultResource(); + } + } + + @Override + @SneakyThrows + protected void doGenerateCaptchaImage(CaptchaExchange captchaExchange) { + GenerateParam param = captchaExchange.getParam(); + 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); + + int randomX = randomInt(fixedTemplate.getWidth() + 5, background.getWidth() - background.getHeight() - 10); + int randomY = randomInt(background.getHeight() - fixedTemplate.getHeight()); + + + // 随机角度 +// double randomDegree = randomDouble(10, 80); +// double randomDegree2 = randomDouble(10, 80); + + int randomObfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), background.getWidth() - background.getHeight() - 10); + int randomObfuscateY = randomInt(background.getHeight() - fixedTemplate.getHeight()); + + double rotatePosRight = background.getHeight(); + + + BufferedImage cutImage = CaptchaImageUtils.cutImage(background, fixedTemplate, randomX, randomY); + + + // 正确的图 + Graphics2D backgroundGraphics = background.createGraphics(); +// backgroundGraphics.rotate(Math.toRadians(randomDegree), randomX + fixedTemplate.getWidth(), rotatePosRight); + backgroundGraphics.drawImage(fixedTemplate, randomX, randomY, null); +// backgroundGraphics.rotate(Math.toRadians(-randomDegree + randomDegree2), randomX + fixedTemplate.getWidth(), rotatePosRight); + // 干扰图 + backgroundGraphics.drawImage(fixedTemplate, randomObfuscateX, randomObfuscateY, null); + + backgroundGraphics.dispose(); + + CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0); + BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(activeTemplate.getWidth(), background.getHeight()); + CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY); + + + FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Thinkpad\\Desktop\\captcha\\temp\\1\\test-bg" + UUID.randomUUID().toString() + + ".jpg"); + ImageIO.write(background, "jpg", fileOutputStream); + fileOutputStream.close(); + +// FileOutputStream fileOutputStream2 = new FileOutputStream("C:\\Users\\Thinkpad\\Desktop\\captcha\\temp\\test-slider.png"); +// ImageIO.write(matrixTemplate, "png", fileOutputStream2); +// fileOutputStream2.close(); + + + System.out.println("randomX=" + randomX); + System.out.println("randomY=" + randomY); +// System.out.println("randomDegree=" + randomDegree); + System.out.println("randomObfuscateX=" + randomObfuscateX); + System.out.println("randomObfuscateY=" + randomY); + + +// CaptchaImageUtils.overlayImage(background, fixedTemplate, randomX, randomY); +// +// BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(activeTemplate.getWidth(), background.getHeight()); + + + } + + + protected double randomDegree(double hypotenuse, double adjacent) { + if (hypotenuse <= adjacent) { + return 90.0; + } + // 使用勾股定理计算对边的长度 + double opposite = Math.sqrt(hypotenuse * hypotenuse - adjacent * adjacent); + + // 使用Math.atan2计算角度(以弧度为单位) + double angleInRadians = Math.atan2(opposite, adjacent); + + // 将弧度转换为度 + double angleInDegrees = Math.toDegrees(angleInRadians); + + return angleInDegrees; + } + + protected int randomObfuscateX(int sliderX, int slWidth, int bgWidth) { + if (bgWidth / 2 > (sliderX + (slWidth / 2))) { + // 右边混淆 + return randomInt(sliderX + 10, bgWidth - slWidth); + } + // 左边混淆 + return randomInt(10, sliderX - slWidth); + } + + @Override + protected ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) { + return null; + } + + + /** + * 初始化默认资源 + */ + public void initDefaultResource() { + ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore(); + // 添加一些系统的资源文件 + resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_RESOURCE_PATH.concat("/1.jpg"), DEFAULT_TAG)); + + // 添加一些系统的 模板文件 + ResourceMap template1 = new ResourceMap(DEFAULT_TAG, 4); + template1.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png"))); + template1.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png"))); + resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template1); + + ResourceMap template2 = new ResourceMap(DEFAULT_TAG, 4); + template2.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png"))); + template2.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png"))); + resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template2); + } +} diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/StandardConcatImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/impl/StandardConcatImageCaptchaGenerator.java index 1267567..99ade97 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/StandardConcatImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/StandardConcatImageCaptchaGenerator.java @@ -4,6 +4,7 @@ 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.ResourceStore; import cloud.tianai.captcha.resource.common.model.dto.Resource; @@ -33,6 +34,12 @@ public class StandardConcatImageCaptchaGenerator extends AbstractImageCaptchaGen setImageTransform(imageTransform); } + public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) { + super(imageCaptchaResourceManager); + setImageTransform(imageTransform); + setInterceptor(interceptor); + } + @Override protected void doInit(boolean initDefaultResource) { if (initDefaultResource) { diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java index c7ea3b1..4a606fc 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java @@ -6,6 +6,7 @@ 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.ResourceStore; import cloud.tianai.captcha.resource.common.model.dto.Resource; @@ -41,6 +42,11 @@ public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGen setImageTransform(imageTransform); } + public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) { + super(imageCaptchaResourceManager); + setImageTransform(imageTransform); + setInterceptor(interceptor); + } @Override protected void doInit(boolean initDefaultResource) { if (initDefaultResource) { diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/StandardSliderImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/impl/StandardSliderImageCaptchaGenerator.java index 74db479..f3003d9 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/StandardSliderImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/StandardSliderImageCaptchaGenerator.java @@ -5,6 +5,7 @@ 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.ResourceStore; import cloud.tianai.captcha.resource.common.model.dto.Resource; @@ -48,6 +49,13 @@ public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGen setImageTransform(imageTransform); } + public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) { + super(imageCaptchaResourceManager); + setImageTransform(imageTransform); + setInterceptor(interceptor); + } + + @Override protected void doInit(boolean initDefaultResource) { if (initDefaultResource) { diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/StandardWordClickImageCaptchaGenerator.java b/src/main/java/cloud/tianai/captcha/generator/impl/StandardWordClickImageCaptchaGenerator.java index d1b679a..6f08522 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/StandardWordClickImageCaptchaGenerator.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/StandardWordClickImageCaptchaGenerator.java @@ -7,6 +7,7 @@ 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.ImageCaptchaResourceManager; import cloud.tianai.captcha.resource.ResourceStore; import cloud.tianai.captcha.resource.common.model.dto.Resource; @@ -78,12 +79,21 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa setImageTransform(imageTransform); } - public StandardWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, List fonts) { + + public StandardWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) { super(imageCaptchaResourceManager); setImageTransform(imageTransform); + setInterceptor(interceptor); + } + + public StandardWordClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor, List fonts) { + super(imageCaptchaResourceManager); + setImageTransform(imageTransform); + setInterceptor(interceptor); this.fonts = fonts; } + @Override protected List randomGetClickImgTips(GenerateParam param) { int tipSize = interferenceCount + checkClickCount; diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/StaticCaptchaPostProcessorManager.java b/src/main/java/cloud/tianai/captcha/generator/impl/StaticCaptchaPostProcessorManager.java index 8438d21..52bdac2 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/StaticCaptchaPostProcessorManager.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/StaticCaptchaPostProcessorManager.java @@ -1,83 +1,74 @@ package cloud.tianai.captcha.generator.impl; -import cloud.tianai.captcha.common.exception.ImageCaptchaException; -import cloud.tianai.captcha.generator.ImageCaptchaGenerator; -import cloud.tianai.captcha.generator.ImageCaptchaPostProcessor; -import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange; -import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo; -import lombok.Getter; - -import java.util.LinkedList; -import java.util.List; - -/** - * @Author: 天爱有情 - * @date 2023/4/24 15:23 - * @Description 验证码后处理器管理 - */ -public class StaticCaptchaPostProcessorManager { - - @Getter - private static LinkedList processors = new LinkedList<>(); - - public static void add(ImageCaptchaPostProcessor processor) { - processors.add(processor); - } - - public static void add(Integer index, ImageCaptchaPostProcessor processor) { - processors.add(index, processor); - } - - public static void addFirst(ImageCaptchaPostProcessor processor) { - processors.addFirst(processor); - } - - public static void addLast(ImageCaptchaPostProcessor processor) { - processors.addLast(processor); - } - - public static void clear() { - processors.clear(); - } - - public static void add(List addPostProcessors) { - processors.addAll(addPostProcessors); - } - - - public static ImageCaptchaInfo applyPostProcessorBeforeGenerate(CaptchaExchange captchaExchange, ImageCaptchaGenerator context) { - for (ImageCaptchaPostProcessor processor : processors) { - try { - ImageCaptchaInfo imageCaptchaInfo = processor.beforeGenerateCaptchaImage(captchaExchange, context); - if (imageCaptchaInfo != null) { - return imageCaptchaInfo; - } - } catch (Exception e) { - throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.beforeGenerateCaptchaImage error, [" + processor.getClass() + "]", e); - } - } - return null; - } - - public static void applyPostProcessorBeforeWrapImageCaptchaInfo(CaptchaExchange captchaExchange, ImageCaptchaGenerator context) { - for (ImageCaptchaPostProcessor processor : processors) { - try { - processor.beforeWrapImageCaptchaInfo(captchaExchange, context); - } catch (Exception e) { - throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.beforeWrapImageCaptchaInfo error, [" + processor.getClass() + "]", e); - } - } - } - - - public static void applyPostProcessorAfterGenerateCaptchaImage(CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo, ImageCaptchaGenerator context) { - for (ImageCaptchaPostProcessor processor : processors) { - try { - processor.afterGenerateCaptchaImage(captchaExchange, imageCaptchaInfo, context); - } catch (Exception e) { - throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.afterGenerateCaptchaImage error, [" + processor.getClass() + "]", e); - } - } - } - -} +// +///** +// * @Author: 天爱有情 +// * @date 2023/4/24 15:23 +// * @Description 验证码后处理器管理 +// */ +//public class StaticCaptchaPostProcessorManager { +// +// @Getter +// private static LinkedList processors = new LinkedList<>(); +// +// public static void add(ImageCaptchaPostProcessor processor) { +// processors.add(processor); +// } +// +// public static void add(Integer index, ImageCaptchaPostProcessor processor) { +// processors.add(index, processor); +// } +// +// public static void addFirst(ImageCaptchaPostProcessor processor) { +// processors.addFirst(processor); +// } +// +// public static void addLast(ImageCaptchaPostProcessor processor) { +// processors.addLast(processor); +// } +// +// public static void clear() { +// processors.clear(); +// } +// +// public static void add(List addPostProcessors) { +// processors.addAll(addPostProcessors); +// } +// +// +// public static ImageCaptchaInfo applyPostProcessorBeforeGenerate(CaptchaExchange captchaExchange, ImageCaptchaGenerator context) { +// for (ImageCaptchaPostProcessor processor : processors) { +// try { +// ImageCaptchaInfo imageCaptchaInfo = processor.beforeGenerateCaptchaImage(captchaExchange, context); +// if (imageCaptchaInfo != null) { +// return imageCaptchaInfo; +// } +// } catch (Exception e) { +// throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.beforeGenerateCaptchaImage error, [" + processor.getClass() + "]", e); +// } +// } +// return null; +// } +// +// public static void applyPostProcessorBeforeWrapImageCaptchaInfo(CaptchaExchange captchaExchange, ImageCaptchaGenerator context) { +// for (ImageCaptchaPostProcessor processor : processors) { +// try { +// processor.beforeWrapImageCaptchaInfo(captchaExchange, context); +// } catch (Exception e) { +// throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.beforeWrapImageCaptchaInfo error, [" + processor.getClass() + "]", e); +// } +// } +// } +// +// +// public static void applyPostProcessorAfterGenerateCaptchaImage(CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo, ImageCaptchaGenerator context) { +// for (ImageCaptchaPostProcessor processor : processors) { +// try { +// processor.afterGenerateCaptchaImage(captchaExchange, imageCaptchaInfo, context); +// } catch (Exception e) { +// throw new ImageCaptchaException("apply ImageCaptchaPostProcessor.afterGenerateCaptchaImage error, [" + processor.getClass() + "]", e); +// } +// } +// } +// +//} diff --git a/src/main/java/cloud/tianai/captcha/generator/impl/provider/CommonImageCaptchaGeneratorProvider.java b/src/main/java/cloud/tianai/captcha/generator/impl/provider/CommonImageCaptchaGeneratorProvider.java index f5414e6..9c74e02 100644 --- a/src/main/java/cloud/tianai/captcha/generator/impl/provider/CommonImageCaptchaGeneratorProvider.java +++ b/src/main/java/cloud/tianai/captcha/generator/impl/provider/CommonImageCaptchaGeneratorProvider.java @@ -3,6 +3,7 @@ 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 { @@ -17,8 +18,8 @@ public class CommonImageCaptchaGeneratorProvider implements ImageCaptchaGenerato } @Override - public ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform) { - return provider.get(resourceManager, imageTransform); + public ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) { + return provider.get(resourceManager, imageTransform,interceptor); } @Override diff --git a/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptor.java b/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptor.java new file mode 100644 index 0000000..38dba8b --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptor.java @@ -0,0 +1,85 @@ +package cloud.tianai.captcha.interceptor; + +import cloud.tianai.captcha.application.vo.CaptchaResponse; +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.ImageCaptchaTrack; + +// ============================ 拦截器执行顺序 ============================ + +// =================== 生成验证码 =================== +// 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 CaptchaResponse beforeGenerateCaptcha(Context context, String type, GenerateParam param) { + return null; + } + + default CaptchaResponse 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, CaptchaResponse captchaResponse) { + } + + default ApiResponse beforeValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) { + Object preReturn = context.getPreReturnData(); + if (preReturn != null) { + return (ApiResponse) preReturn; + } + return ApiResponse.ofSuccess(); + } + + default ApiResponse afterValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, 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) { + + } +} diff --git a/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptorGroup.java b/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptorGroup.java new file mode 100644 index 0000000..13117cb --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptorGroup.java @@ -0,0 +1,186 @@ +package cloud.tianai.captcha.interceptor; + +import cloud.tianai.captcha.application.vo.CaptchaResponse; +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.ImageCaptchaTrack; +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 validators = new ArrayList<>(); + + public void addInterceptor(CaptchaInterceptor validator) { + validators.add(validator); + } + + public void addInterceptor(List 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 CaptchaResponse beforeGenerateCaptcha(Context context, String type, GenerateParam param) { + context = createContextIfNecessary(context); + CaptchaResponse 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, CaptchaResponse 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, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) { + context = createContextIfNecessary(context); + ApiResponse beforeValid = null; + while (context.next() < context.getCount()) { + CaptchaInterceptor interceptor = validators.get(context.getCurrent()); + beforeValid = interceptor.beforeValid(context, type, imageCaptchaTrack, validData); + context.setPreReturnData(beforeValid); + } + return beforeValid == null ? ApiResponse.ofSuccess() : beforeValid; + } + + @Override + public ApiResponse afterValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, 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, imageCaptchaTrack, validData, basicValid); + context.setPreReturnData(valid); + } + return valid == null ? ApiResponse.ofSuccess() : valid; + } + + @Override + public CaptchaResponse beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) { + context = createContextIfNecessary(context); + CaptchaResponse 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(); + } +} diff --git a/src/main/java/cloud/tianai/captcha/interceptor/Context.java b/src/main/java/cloud/tianai/captcha/interceptor/Context.java new file mode 100644 index 0000000..cf708f7 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/interceptor/Context.java @@ -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 getCurrentData(String key, Class 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 getData(String key, Class type) { + T result = getCurrentData(key, type); + if (result == null && parent != null) { + result = parent.getData(key, type); + } + return result; + } + + + private T convert(Object data, Class 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(); + } +} diff --git a/src/main/java/cloud/tianai/captcha/interceptor/EmptyCaptchaInterceptor.java b/src/main/java/cloud/tianai/captcha/interceptor/EmptyCaptchaInterceptor.java new file mode 100644 index 0000000..ccc2f22 --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/interceptor/EmptyCaptchaInterceptor.java @@ -0,0 +1,7 @@ +package cloud.tianai.captcha.interceptor; + +public class EmptyCaptchaInterceptor implements CaptchaInterceptor{ + + public static EmptyCaptchaInterceptor INSTANCE = new EmptyCaptchaInterceptor(); + +} diff --git a/src/main/java/cloud/tianai/captcha/interceptor/impl/BasicTrackCaptchaInterceptor.java b/src/main/java/cloud/tianai/captcha/interceptor/impl/BasicTrackCaptchaInterceptor.java new file mode 100644 index 0000000..7d6b20d --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/interceptor/impl/BasicTrackCaptchaInterceptor.java @@ -0,0 +1,109 @@ +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 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 check fail"); + + @Override + public String getName() { + return "basic_track_check"; + } + + @Override + public ApiResponse afterValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData, ApiResponse basicValid) { + if (!basicValid.isSuccess()) { + return context.getGroup().afterValid(context, type, imageCaptchaTrack, validData, basicValid); + } + if (!CaptchaTypeClassifier.isSliderCaptcha(type)) { + // 不是滑动验证码的话暂时跳过,点选验证码行为轨迹还没做 + return ApiResponse.ofSuccess(); + } + // 进行行为轨迹检测 + long startSlidingTime = imageCaptchaTrack.getStartTime().getTime(); + long endSlidingTime = imageCaptchaTrack.getStopTime().getTime(); + Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth(); + List 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); + } + +} diff --git a/src/main/java/cloud/tianai/captcha/interceptor/impl/ParamCheckCaptchaInterceptor.java b/src/main/java/cloud/tianai/captcha/interceptor/impl/ParamCheckCaptchaInterceptor.java new file mode 100644 index 0000000..e461c1e --- /dev/null +++ b/src/main/java/cloud/tianai/captcha/interceptor/impl/ParamCheckCaptchaInterceptor.java @@ -0,0 +1,54 @@ +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; + +/** + * @Author: 天爱有情 + * @date 2023/1/4 10:10 + * @Description 轨迹参数校验, 如果轨迹参数为空抛异常 + */ +public class ParamCheckCaptchaInterceptor implements CaptchaInterceptor { + @Override + public ApiResponse beforeValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) { + checkParam(imageCaptchaTrack); + 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"); + } + } + } +} diff --git a/src/main/java/cloud/tianai/captcha/resource/ResourceStore.java b/src/main/java/cloud/tianai/captcha/resource/ResourceStore.java index e21cde8..308eb57 100644 --- a/src/main/java/cloud/tianai/captcha/resource/ResourceStore.java +++ b/src/main/java/cloud/tianai/captcha/resource/ResourceStore.java @@ -43,4 +43,14 @@ public interface ResourceStore { */ ResourceMap randomGetTemplateByTypeAndTag(String type, String tag); + /** + * 清除所有内置模板 + */ + void clearAllTemplates(); + + /** + * 清除所有内置资源 + */ + void clearAllResources(); + } diff --git a/src/main/java/cloud/tianai/captcha/resource/impl/LocalMemoryResourceStore.java b/src/main/java/cloud/tianai/captcha/resource/impl/LocalMemoryResourceStore.java index da25b4f..a67834e 100644 --- a/src/main/java/cloud/tianai/captcha/resource/impl/LocalMemoryResourceStore.java +++ b/src/main/java/cloud/tianai/captcha/resource/impl/LocalMemoryResourceStore.java @@ -76,6 +76,7 @@ public class LocalMemoryResourceStore implements ResourceStore { resourceTagMap.remove(mergeTypeAndTag(type, tag)); } + @Override public void clearAllResources() { resourceTagMap.clear(); } @@ -101,6 +102,7 @@ public class LocalMemoryResourceStore implements ResourceStore { } + @Override public void clearAllTemplates() { templateResourceTagMap.clear(); } diff --git a/src/main/java/cloud/tianai/captcha/validator/ImageCaptchaValidator.java b/src/main/java/cloud/tianai/captcha/validator/ImageCaptchaValidator.java index b54722d..5a549b6 100644 --- a/src/main/java/cloud/tianai/captcha/validator/ImageCaptchaValidator.java +++ b/src/main/java/cloud/tianai/captcha/validator/ImageCaptchaValidator.java @@ -1,11 +1,10 @@ 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; -import java.util.Map; - /** * @Author: 天爱有情 * @date 2022/2/17 10:54 @@ -17,9 +16,9 @@ public interface ImageCaptchaValidator { * 用于生成验证码校验时需要的回传参数 * * @param imageCaptchaInfo 生成的验证码数据 - * @return Map + * @return AnyMap */ - Map generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo); + AnyMap generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo); /** * 校验用户滑动滑块是否正确 @@ -28,5 +27,5 @@ public interface ImageCaptchaValidator { * @param imageCaptchaValidData generateImageCaptchaValidData(生成的数据) * @return ApiResponse */ - ApiResponse valid(ImageCaptchaTrack imageCaptchaTrack, Map imageCaptchaValidData); + ApiResponse valid(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData); } diff --git a/src/main/java/cloud/tianai/captcha/validator/common/model/dto/ImageCaptchaTrack.java b/src/main/java/cloud/tianai/captcha/validator/common/model/dto/ImageCaptchaTrack.java index 093fe49..e0893e7 100644 --- a/src/main/java/cloud/tianai/captcha/validator/common/model/dto/ImageCaptchaTrack.java +++ b/src/main/java/cloud/tianai/captcha/validator/common/model/dto/ImageCaptchaTrack.java @@ -25,9 +25,11 @@ public class ImageCaptchaTrack { /** 模板图片高度. */ private Integer templateImageHeight; /** 滑动开始时间. */ - private Date startSlidingTime; + private Date startTime; /** 滑动结束时间. */ - private Date endSlidingTime; + private Date stopTime; + private Integer left; + private Integer top; /** 滑动的轨迹. */ private List trackList; /** 扩展数据,用户传输加密数据等.*/ diff --git a/src/main/java/cloud/tianai/captcha/validator/impl/BasicCaptchaTrackValidator.java b/src/main/java/cloud/tianai/captcha/validator/impl/BasicCaptchaTrackValidator.java index 2d6792c..8362ce1 100644 --- a/src/main/java/cloud/tianai/captcha/validator/impl/BasicCaptchaTrackValidator.java +++ b/src/main/java/cloud/tianai/captcha/validator/impl/BasicCaptchaTrackValidator.java @@ -1,5 +1,6 @@ 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; @@ -8,7 +9,6 @@ import cloud.tianai.captcha.common.util.ObjectUtils; import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; import java.util.List; -import java.util.Map; /** * @Author: 天爱有情 @@ -26,21 +26,24 @@ public class BasicCaptchaTrackValidator extends SimpleImageCaptchaValidator { } @Override - public ApiResponse beforeValid(ImageCaptchaTrack imageCaptchaTrack, Map captchaValidData, Float tolerant, String type) { + public ApiResponse beforeValid(ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) { // 校验参数 checkParam(imageCaptchaTrack); return ApiResponse.ofSuccess(); } @Override - public ApiResponse afterValid(ImageCaptchaTrack imageCaptchaTrack, Map captchaValidData, Float tolerant, String type) { + 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.getStartSlidingTime().getTime(); - long endSlidingTime = imageCaptchaTrack.getEndSlidingTime().getTime(); + long startSlidingTime = imageCaptchaTrack.getStartTime().getTime(); + long endSlidingTime = imageCaptchaTrack.getStopTime().getTime(); Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth(); List trackList = imageCaptchaTrack.getTrackList(); // 这里只进行基本检测, 用一些简单算法进行校验,如有需要可扩展 @@ -112,10 +115,10 @@ public class BasicCaptchaTrackValidator extends SimpleImageCaptchaValidator { if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageHeight())) { throw new IllegalArgumentException("bgImageHeight must not be null"); } - if (ObjectUtils.isEmpty(imageCaptchaTrack.getStartSlidingTime())) { + if (ObjectUtils.isEmpty(imageCaptchaTrack.getStartTime())) { throw new IllegalArgumentException("startSlidingTime must not be null"); } - if (ObjectUtils.isEmpty(imageCaptchaTrack.getEndSlidingTime())) { + if (ObjectUtils.isEmpty(imageCaptchaTrack.getStopTime())) { throw new IllegalArgumentException("endSlidingTime must not be null"); } if (CollectionUtils.isEmpty(imageCaptchaTrack.getTrackList())) { diff --git a/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java b/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java index 48c46e9..424743e 100644 --- a/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java +++ b/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java @@ -1,5 +1,6 @@ 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; @@ -19,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; /** @@ -81,8 +81,8 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide } @Override - public Map generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) { - Map map = new HashMap<>(8); + public AnyMap generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo) { + AnyMap map = AnyMap.of(new HashMap<>(8)); if (beforeGenerateImageCaptchaValidData(imageCaptchaInfo, map)) { doGenerateImageCaptchaValidData(map, imageCaptchaInfo); } @@ -90,7 +90,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide return map; } - public boolean beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo, Map map) { + public boolean beforeGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo, AnyMap map) { // 容错值 Float tolerant = imageCaptchaInfo.getTolerant(); if (tolerant != null && tolerant > 0) { @@ -105,11 +105,11 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide return true; } - public void afterGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo, Map map) { + public void afterGenerateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo, AnyMap map) { } - public void doGenerateImageCaptchaValidData(Map map, + public void doGenerateImageCaptchaValidData(AnyMap map, ImageCaptchaInfo imageCaptchaInfo) { // type String type = (String) map.getOrDefault(TYPE_KEY, CaptchaTypeConstant.SLIDER); @@ -154,11 +154,11 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide } @Override - public ApiResponse valid(ImageCaptchaTrack imageCaptchaTrack, Map imageCaptchaValidData) { + public ApiResponse valid(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData) { // 读容错值 - Float tolerant = getFloatParam(TOLERANT_KEY, imageCaptchaValidData, defaultTolerant); + Float tolerant = imageCaptchaValidData.getFloat(TOLERANT_KEY, defaultTolerant); // 读验证码类型 - String type = getStringParam(TYPE_KEY, imageCaptchaValidData, CaptchaTypeConstant.SLIDER); + String type = imageCaptchaValidData.getString(TYPE_KEY, CaptchaTypeConstant.SLIDER); // 验证前 // 在验证前必须读取 容错值 和验证码类型 ApiResponse beforeValid = beforeValid(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); @@ -178,14 +178,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide // 验证 ApiResponse response; boolean valid = doValid(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); - if (valid) { - // 验证后 - response = afterValid(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); - } else { - // 缺口位置校验失败 - response = ApiResponse.ofMessage(ApiResponseStatusConstant.BASIC_CHECK_FAIL); - } - return response; + return afterValid(valid, imageCaptchaTrack, imageCaptchaValidData, tolerant, type); } /** @@ -197,7 +190,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide * @param type type * @return boolean */ - public ApiResponse beforeValid(ImageCaptchaTrack imageCaptchaTrack, Map captchaValidData, Float tolerant, String type) { + public ApiResponse beforeValid(ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) { return ApiResponse.ofSuccess(); } @@ -210,12 +203,15 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide * @param type type * @return boolean */ - public ApiResponse afterValid(ImageCaptchaTrack imageCaptchaTrack, Map captchaValidData, Float tolerant, String type) { + 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, - Map imageCaptchaValidData, + AnyMap imageCaptchaValidData, Float tolerant, String type) { if (CaptchaTypeClassifier.isSliderCaptcha(type)) { @@ -233,12 +229,12 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide return false; } - public boolean doValidJigsawCaptcha(ImageCaptchaTrack imageCaptchaTrack, Map imageCaptchaValidData, Float tolerant, String type) { + 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 = getStringParam(PERCENTAGE_KEY, imageCaptchaValidData, null); + String successPosStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null); return successPosStr.equals(posArr); } @@ -252,10 +248,10 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide * @return boolean */ public boolean doValidClickCaptcha(ImageCaptchaTrack imageCaptchaTrack, - Map imageCaptchaValidData, + AnyMap imageCaptchaValidData, Float tolerant, String type) { - String validStr = getStringParam(PERCENTAGE_KEY, imageCaptchaValidData, null); + String validStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null); if (ObjectUtils.isEmpty(validStr)) { return false; } @@ -307,19 +303,20 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide * @return boolean */ public boolean doValidSliderCaptcha(ImageCaptchaTrack imageCaptchaTrack, - Map imageCaptchaValidData, + AnyMap imageCaptchaValidData, Float tolerant, String type) { - Float oriPercentage = getFloatParam(PERCENTAGE_KEY, imageCaptchaValidData); + Float oriPercentage = imageCaptchaValidData.getFloat(PERCENTAGE_KEY); if (oriPercentage == null) { // 没读取到百分比 return false; } List trackList = imageCaptchaTrack.getTrackList(); + ImageCaptchaTrack.Track firstTrack = trackList.get(0); // 取最后一个滑动轨迹 ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); // 计算百分比 - float calcPercentage = calcPercentage(lastTrack.getX(), imageCaptchaTrack.getBgImageWidth()); + float calcPercentage = calcPercentage(lastTrack.getX() - firstTrack.getX(), imageCaptchaTrack.getBgImageWidth()); // 校验百分比 boolean percentage = checkPercentage(calcPercentage, oriPercentage, tolerant); if (percentage) { @@ -331,48 +328,7 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide return percentage; } - public Float getFloatParam(String key, Map imageCaptchaValidData) { - return getFloatParam(key, imageCaptchaValidData, null); - } - - public Float getFloatParam(String key, Map imageCaptchaValidData, Float defaultData) { - Object data = imageCaptchaValidData.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) { - log.error("从 imageCaptchaValidData 读取到的 " + key + "无法转换成float类型, [{}]", data); - throw e; - } - } - return defaultData; - } - - public String getStringParam(String key, Map imageCaptchaValidData, String defaultData) { - if (CollectionUtils.isEmpty(imageCaptchaValidData)) { - return defaultData; - } - Object data = imageCaptchaValidData.get(key); - if (data != null) { - if (data instanceof String) { - return (String) data; - } - try { - return String.valueOf(data); - } catch (NumberFormatException e) { - log.error("从 imageCaptchaValidData 读取到的 " + key + "无法转换成String类型, [{}]", data); - throw e; - } - } - return defaultData; - } - - protected void addPercentage(ImageCaptchaInfo imageCaptchaInfo, Map imageCaptchaValidData) { + protected void addPercentage(ImageCaptchaInfo imageCaptchaInfo, AnyMap imageCaptchaValidData) { float percentage = calcPercentage(imageCaptchaInfo.getRandomX(), imageCaptchaInfo.getBackgroundImageWidth()); imageCaptchaValidData.put(PERCENTAGE_KEY, percentage); } diff --git a/src/main/test/java/example/readme/ApplicationTest.java b/src/main/test/java/example/readme/ApplicationTest.java new file mode 100644 index 0000000..2d033fa --- /dev/null +++ b/src/main/test/java/example/readme/ApplicationTest.java @@ -0,0 +1,40 @@ +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.CaptchaResponse; +import cloud.tianai.captcha.application.vo.ImageCaptchaVO; +import cloud.tianai.captcha.cache.CacheStore; +import cloud.tianai.captcha.cache.impl.LocalCacheStore; +import cloud.tianai.captcha.generator.ImageCaptchaGenerator; +import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; +import cloud.tianai.captcha.interceptor.CaptchaInterceptor; +import cloud.tianai.captcha.interceptor.CaptchaInterceptorGroup; +import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor; +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.impl.DefaultImageCaptchaResourceManager; +import cloud.tianai.captcha.validator.ImageCaptchaValidator; +import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator; + +public class ApplicationTest { + + public static void main(String[] args) { + ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); + ImageCaptchaGenerator generator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager); + generator.init(true); + ImageCaptchaValidator imageCaptchaValidator = new SimpleImageCaptchaValidator(); + CacheStore cacheStore = new LocalCacheStore(); + ImageCaptchaProperties prop = new ImageCaptchaProperties(); + CaptchaInterceptorGroup group = new CaptchaInterceptorGroup(); + group.addInterceptor(new ParamCheckCaptchaInterceptor()); + group.addInterceptor(new BasicTrackCaptchaInterceptor()); + + ImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, imageCaptchaValidator, cacheStore, prop, group); + + CaptchaResponse res = application.generateCaptcha("SLIDER"); + System.out.println(res); + } +} diff --git a/src/main/test/java/example/readme/SimpleDemo.java b/src/main/test/java/example/readme/SimpleDemo.java index fa60c48..67f924c 100644 --- a/src/main/test/java/example/readme/SimpleDemo.java +++ b/src/main/test/java/example/readme/SimpleDemo.java @@ -1,5 +1,6 @@ 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; @@ -26,7 +27,7 @@ public class SimpleDemo { ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER); // 这个数据是根据当前生成的这条验证码数据生成对应的验证数据, 该数据要存到缓存中 - Map map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo); + AnyMap map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo); diff --git a/src/main/test/java/example/readme/Test2.java b/src/main/test/java/example/readme/Test2.java index 2ecdce7..b3906a4 100644 --- a/src/main/test/java/example/readme/Test2.java +++ b/src/main/test/java/example/readme/Test2.java @@ -1,5 +1,6 @@ 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; @@ -10,7 +11,7 @@ public class Test2 { BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator(); ImageCaptchaTrack imageCaptchaTrack = null; - Map map = null; + AnyMap map = null; Float percentage = null; // 用户传来的行为轨迹和进行校验 // - imageCaptchaTrack为前端传来的滑动轨迹数据 diff --git a/src/main/test/java/example/readme/Test6.java b/src/main/test/java/example/readme/Test6.java index 7b4cfc0..f34b9b3 100644 --- a/src/main/test/java/example/readme/Test6.java +++ b/src/main/test/java/example/readme/Test6.java @@ -1,7 +1,7 @@ package example.readme; import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; -import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant; +import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator; import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; import cloud.tianai.captcha.resource.ResourceStore; import cloud.tianai.captcha.resource.common.model.dto.Resource; @@ -16,8 +16,8 @@ public class Test6 { ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore(); // 添加滑块验证码模板.模板图片由三张图片组成 ResourceMap template1 = new ResourceMap("default", 4); - template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/active.png")); - template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/fixed.png")); + 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 接口,进行自定义扩展