) SUCCESS;
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/common/response/ApiResponseStatusConstant.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.common.response;
2 |
3 |
4 | /**
5 | * @Author: 天爱有情
6 | * @Date 2020/5/26 17:58
7 | * @Description 统一返回错误码, 详见 阿里巴巴开发规范 错误码列表
8 | *
9 | * 该枚举定义了一些公共的code码,自定义code码数据需在自己业务中编写
10 | */
11 | public interface ApiResponseStatusConstant {
12 |
13 | /**
14 | * 成功.
15 | */
16 | CodeDefinition SUCCESS = new CodeDefinition(200, "OK");
17 |
18 | CodeDefinition NOT_VALID_PARAM = new CodeDefinition(403, "无效参数");
19 |
20 | CodeDefinition INTERNAL_SERVER_ERROR = new CodeDefinition(500, "未知的内部错误");
21 |
22 | CodeDefinition EXPIRED = new CodeDefinition(4000, "已失效");
23 |
24 | CodeDefinition BASIC_CHECK_FAIL = new CodeDefinition(4001, "基础校验失败");
25 |
26 |
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/common/response/CodeDefinition.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.common.response;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | /**
7 | * @Author: 天爱有情
8 | * @date 2022/4/13 12:37
9 | * @Description code 定义
10 | */
11 | @Data
12 | @AllArgsConstructor
13 | public class CodeDefinition {
14 |
15 | private Integer code;
16 | private String message;
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/common/util/CaptchaTypeClassifier.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.common.util;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | /**
7 | * @Author: 天爱有情
8 | * @date 2023/11/2 9:22
9 | * @Description 验证码类型分类
10 | */
11 | public class CaptchaTypeClassifier {
12 |
13 | private static final Set SLIDER_CAPTCHA_TYPES = new HashSet<>();
14 | private static final Set CLICK_CAPTCHA_TYPES = new HashSet<>();
15 | private static final Set JIGSAW_CAPTCHA_TYPES = new HashSet<>();
16 |
17 | public static void addSliderCaptchaType(String type) {
18 | SLIDER_CAPTCHA_TYPES.add(type.toUpperCase());
19 | }
20 |
21 | public static void addClickCaptchaType(String type) {
22 | CLICK_CAPTCHA_TYPES.add(type.toUpperCase());
23 | }
24 |
25 | public static boolean isSliderCaptcha(String type) {
26 | return SLIDER_CAPTCHA_TYPES.contains(type.toUpperCase());
27 | }
28 |
29 | public static boolean isClickCaptcha(String type) {
30 | return CLICK_CAPTCHA_TYPES.contains(type.toUpperCase());
31 | }
32 |
33 | public static Set getSliderCaptchaTypes() {
34 | return SLIDER_CAPTCHA_TYPES;
35 | }
36 |
37 | public static Set getClickCaptchaTypes() {
38 | return CLICK_CAPTCHA_TYPES;
39 | }
40 |
41 | public static void removeSliderCaptchaType(String type) {
42 | SLIDER_CAPTCHA_TYPES.remove(type.toUpperCase());
43 | }
44 |
45 | public static void removeClickCaptchaType(String type) {
46 | CLICK_CAPTCHA_TYPES.remove(type.toUpperCase());
47 | }
48 |
49 | public static boolean isJigsawCaptcha(String type) {
50 | return JIGSAW_CAPTCHA_TYPES.contains(type.toUpperCase());
51 | }
52 |
53 | public static void addJigsawCaptchaType(String type) {
54 | JIGSAW_CAPTCHA_TYPES.add(type.toUpperCase());
55 | }
56 |
57 | public static void removeJigsawCaptchaType(String type) {
58 | JIGSAW_CAPTCHA_TYPES.remove(type.toUpperCase());
59 | }
60 |
61 | public static Set getJigsawCaptchaTypes() {
62 | return JIGSAW_CAPTCHA_TYPES;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/common/util/FontUtils.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.common.util;
2 |
3 | import lombok.SneakyThrows;
4 |
5 | import java.util.Random;
6 |
7 | /**
8 | * @Author: 天爱有情
9 | * @date 2022/4/27 11:34
10 | * @Description 字体工具包
11 | */
12 | public class FontUtils {
13 |
14 | /**
15 | * 获取随机文字
16 | *
17 | * @param random 随机数生成器
18 | * @return String
19 | */
20 | @SneakyThrows
21 | public static String getRandomChar(Random random) {
22 | Integer heightPos, lowPos; // 定义高低位
23 | heightPos = (176 + Math.abs(random.nextInt(39)));
24 | lowPos = (161 + Math.abs(random.nextInt(93)));
25 | byte[] bytes = new byte[2];
26 | bytes[0] = heightPos.byteValue();
27 | bytes[1] = lowPos.byteValue();
28 | return new String(bytes, "GBK");
29 | }
30 |
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/common/util/NamedThreadFactory.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.common.util;
2 |
3 | import java.util.concurrent.ThreadFactory;
4 | import java.util.concurrent.atomic.AtomicInteger;
5 |
6 | /**
7 | * A ThreadFactory that allows for custom thread names.
8 | */
9 | public class NamedThreadFactory implements ThreadFactory {
10 |
11 | private static final AtomicInteger THREAD_INDEX = new AtomicInteger(0);
12 |
13 | private final String basename;
14 | private final boolean daemon;
15 |
16 | /**
17 | * Creates a new instance of the factory.
18 | *
19 | * @param basename Basename of a new tread created by this factory.
20 | */
21 | public NamedThreadFactory(final String basename) {
22 | this(basename, true);
23 | }
24 |
25 | /**
26 | * Creates a new instance of the factory.
27 | *
28 | * @param basename Basename of a new tread created by this factory.
29 | * @param daemon If true, marks new thread as a daemon thread
30 | */
31 | public NamedThreadFactory(final String basename, final boolean daemon) {
32 |
33 | this.basename = basename;
34 | this.daemon = daemon;
35 | }
36 |
37 | @Override
38 | public Thread newThread(final Runnable runnable) {
39 |
40 | final Thread thread = new Thread(runnable, basename + "-" + THREAD_INDEX.getAndIncrement());
41 | thread.setDaemon(daemon);
42 | return thread;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/common/util/UUIDUtils.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.common.util;
2 |
3 | public class UUIDUtils {
4 |
5 | public static String getUUID() {
6 | return java.util.UUID.randomUUID().toString().replace("-", "");
7 | }
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGenerator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator;
2 |
3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
4 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
5 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
6 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
7 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
8 |
9 | /**
10 | * @Author: 天爱有情
11 | * @date 2020/10/19 18:37
12 | * @Description 图片验证码生成器
13 | */
14 | public interface ImageCaptchaGenerator {
15 |
16 |
17 | /**
18 | * 初始化
19 | *
20 | * @return ImageCaptchaGenerator
21 | */
22 | ImageCaptchaGenerator init();
23 |
24 | /**
25 | * 生成验证码图片
26 | *
27 | * @param type 类型 {@link CaptchaTypeConstant}
28 | * @return SliderCaptchaInfo
29 | */
30 | ImageCaptchaInfo generateCaptchaImage(String type);
31 |
32 |
33 | /**
34 | * 生成滑块验证码
35 | *
36 | * @param type type {@link CaptchaTypeConstant}
37 | * @param targetFormatName jpeg或者webp格式
38 | * @param matrixFormatName png或者webp格式
39 | * @return SliderCaptchaInfo
40 | */
41 | ImageCaptchaInfo generateCaptchaImage(String type, String targetFormatName, String matrixFormatName);
42 |
43 | /**
44 | * 生成验证码
45 | *
46 | * @param param 生成参数
47 | * @return SliderCaptchaInfo
48 | */
49 | ImageCaptchaInfo generateCaptchaImage(GenerateParam param);
50 |
51 |
52 | /**
53 | * 获取滑块验证码资源管理器
54 | *
55 | * @return SliderCaptchaResourceManager
56 | */
57 | ImageCaptchaResourceManager getImageResourceManager();
58 |
59 | /**
60 | * 设置滑块验证码资源管理器
61 | *
62 | * @param imageCaptchaResourceManager
63 | */
64 | void setImageResourceManager(ImageCaptchaResourceManager imageCaptchaResourceManager);
65 |
66 | /**
67 | * 获取图片转换器
68 | *
69 | * @return ImageTransform
70 | */
71 | ImageTransform getImageTransform();
72 |
73 | /**
74 | * 设置图片转换器
75 | *
76 | * @param imageTransform imageTransform
77 | * @return ImageTransform
78 | */
79 | void setImageTransform(ImageTransform imageTransform);
80 |
81 |
82 | CaptchaInterceptor getInterceptor();
83 |
84 | void setInterceptor(CaptchaInterceptor interceptor);
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/ImageCaptchaGeneratorProvider.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator;
2 |
3 |
4 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
5 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
6 |
7 | /**
8 | * @Author: 天爱有情
9 | * @date 2022/5/19 14:45
10 | * @Description ImageCaptchaGenerator 提供者
11 | */
12 | public interface ImageCaptchaGeneratorProvider {
13 |
14 | /**
15 | * 生成/获取 ImageCaptchaGenerator
16 | *
17 | * @param resourceManager resourceManager
18 | * @param imageTransform imageTransform
19 | * @return ImageCaptchaGenerator
20 | */
21 | ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor);
22 |
23 | /**
24 | * 验证码类型
25 | *
26 | * @return String
27 | */
28 | default String getType() {
29 | return "unknown";
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/ImageTransform.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator;
2 |
3 | import cloud.tianai.captcha.generator.common.model.dto.CustomData;
4 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
5 | import cloud.tianai.captcha.generator.common.model.dto.ImageTransformData;
6 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
7 |
8 | import java.awt.image.BufferedImage;
9 |
10 | /**
11 | * @Author: 天爱有情
12 | * @date 2022/8/25 10:21
13 | * @Description 图片转换为字符串, 扩展接口, 可以转换为文件地址等
14 | */
15 | public interface ImageTransform {
16 |
17 | /**
18 | * 转换
19 | *
20 | * @param backgroundImage 背景图片
21 | * @param param 参数
22 | * @param backgroundResource 背景资源对象
23 | * @param data 自定义透传数据
24 | * @return ImageTransformData
25 | */
26 | default ImageTransformData transform(GenerateParam param, BufferedImage backgroundImage, Resource backgroundResource, CustomData data) {
27 | return transform(param, backgroundImage, null, backgroundResource, null, data);
28 | }
29 |
30 | /**
31 | * 转换
32 | *
33 | * @param backgroundImage 背景图片
34 | * @param templateImage 模板图片(可能为空)
35 | * @param param 参数
36 | * @param backgroundResource 背景资源对象
37 | * @param templateResource 模板资源对象(可能为空)
38 | * @param data 自定义透传数据
39 | * @return String
40 | */
41 | ImageTransformData transform(GenerateParam param,
42 | BufferedImage backgroundImage,
43 | BufferedImage templateImage,
44 | Object backgroundResource,
45 | Object templateResource,
46 | CustomData data);
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/FontWrapper.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 | import lombok.NoArgsConstructor;
6 |
7 | import java.awt.*;
8 |
9 | @Data
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | public class FontWrapper {
13 | private Font font;
14 | private Float currentFontTopCoef;
15 |
16 | public FontWrapper(Font font) {
17 | this(font, 70);
18 | }
19 |
20 | public FontWrapper(Font font, int fontSize) {
21 | this.font = font;
22 | this.font = font.deriveFont(Font.BOLD, fontSize);
23 | }
24 |
25 | public float getCurrentFontTopCoef() {
26 | if (currentFontTopCoef != null) {
27 | return currentFontTopCoef;
28 | }
29 | currentFontTopCoef = 0.14645833f * font.getSize() + 0.39583333f;
30 | return currentFontTopCoef;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/CaptchaExchange.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
5 | import lombok.Data;
6 |
7 | import java.awt.image.BufferedImage;
8 |
9 | /**
10 | * @Author: 天爱有情
11 | * @date 2023/4/24 15:02
12 | * @Description 传输用
13 | */
14 | @Data
15 | public class CaptchaExchange {
16 | /** 模板对象. */
17 | private ResourceMap templateResource;
18 | /** 资源对象. */
19 | private Resource resourceImage;
20 | /** 生成好的背景图片. */
21 | private BufferedImage backgroundImage;
22 | /** 生成好的模板图片. */
23 | private BufferedImage templateImage;
24 | /** 最终要回调给验证器的自定义对象. */
25 | private CustomData customData;
26 | /** 用户传来的生成参数. */
27 | private GenerateParam param;
28 | /** 传输对象,扩展自定义. */
29 | private Object transferData;
30 |
31 | public static CaptchaExchange create(CustomData customData, GenerateParam param) {
32 | CaptchaExchange captchaExchange = new CaptchaExchange();
33 | captchaExchange.setCustomData(customData);
34 | captchaExchange.setParam(param);
35 | return captchaExchange;
36 | }
37 |
38 | public static CaptchaExchange create(GenerateParam param) {
39 | return create(new CustomData(), param);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/ClickImageCheckDefinition.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | import java.awt.*;
9 |
10 | /**
11 | * @Author: 天爱有情
12 | * @date 2022/4/28 16:51
13 | * @Description 点击图片校验描述
14 | */
15 | @Data
16 | @NoArgsConstructor
17 | @AllArgsConstructor
18 | public class ClickImageCheckDefinition {
19 | /** 提示.*/
20 | private Resource tip;
21 | /** x.*/
22 | private Integer x;
23 | /** y.*/
24 | private Integer y;
25 | /** 宽.*/
26 | private Integer width;
27 | /** 高.*/
28 | private Integer height;
29 | /** 颜色.*/
30 | private Color imageColor;
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/CustomData.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import cloud.tianai.captcha.common.AnyMap;
4 | import lombok.Data;
5 |
6 | /**
7 | * @Author: 天爱有情
8 | * @date 2023/4/24 10:27
9 | * @Description 自定义扩展数据
10 | */
11 | @Data
12 | public class CustomData {
13 |
14 | /** 透传字段,用于传给前端. */
15 | private AnyMap viewData;
16 | /** 内部使用的字段数据. */
17 | private AnyMap data;
18 | /**
19 | * 扩展字段
20 | */
21 | public Object expand;
22 |
23 | public void putViewData(String key, Object data) {
24 | if (this.viewData == null) {
25 | this.viewData = new AnyMap();
26 | }
27 | this.viewData.put(key, data);
28 | }
29 |
30 | public void putData(String key, Object data) {
31 | if (this.data == null) {
32 | this.data = new AnyMap();
33 | }
34 | this.data.put(key, data);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/GenerateParam.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import cloud.tianai.captcha.common.AnyMap;
4 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
5 | import lombok.*;
6 |
7 | /**
8 | * @Author: 天爱有情
9 | * @date 2022/2/11 9:44
10 | * @Description 生成参数
11 | */
12 | @Data
13 | // param作为扩展字段暂时将param从equals和toString中移除掉 以适应 CacheImageCaptchaGenerator
14 | @EqualsAndHashCode(exclude = "param")
15 | public class GenerateParam {
16 |
17 | /**
18 | * 背景格式化类型.
19 | */
20 | private String backgroundFormatName = "jpeg";
21 | /**
22 | * 模板图片格式化类型.
23 | */
24 | private String templateFormatName = "png";
25 | /**
26 | * 是否混淆.
27 | */
28 | private Boolean obfuscate = false;
29 | /**
30 | * 类型.
31 | */
32 | private String type = CaptchaTypeConstant.SLIDER;
33 | /**
34 | * 背景图片标签, 用户二级过滤背景图片,或指定某背景图片.
35 | */
36 | private String backgroundImageTag;
37 | /**
38 | * 滑动图片标签,用户二级过滤模板图片,或指定某模板图片..
39 | */
40 | private String templateImageTag;
41 | /**
42 | * 扩展参数.
43 | */
44 | private AnyMap param = new AnyMap();
45 |
46 | public void addParam(String key, Object value) {
47 | doGetOrCreateParam().put(key, value);
48 | }
49 |
50 | public Object getParam(String key) {
51 | return param == null ? null : param.get(key);
52 | }
53 |
54 | private AnyMap doGetOrCreateParam() {
55 | if (param == null) {
56 | param = new AnyMap();
57 | }
58 | return param;
59 | }
60 |
61 | public Object removeParam(String key) {
62 | if (param == null) {
63 | return null;
64 | }
65 | return param.remove(key);
66 | }
67 |
68 | public Object getOrDefault(String key, Object defaultValue) {
69 | if (param == null) {
70 | return defaultValue;
71 | }
72 | return param.getOrDefault(key, defaultValue);
73 | }
74 |
75 |
76 | public Object putIfAbsent(String key, Object value) {
77 | return doGetOrCreateParam().putIfAbsent(key, value);
78 | }
79 |
80 |
81 | public void addParam(ParamKey paramKey, T value) {
82 | addParam(paramKey.getKey(), value);
83 | }
84 |
85 | public T getParam(ParamKey paramKey) {
86 | return (T) getParam(paramKey.getKey());
87 | }
88 |
89 | public T getOrDefault(ParamKey paramKey, T defaultValue) {
90 | return (T) getOrDefault(paramKey.getKey(), defaultValue);
91 | }
92 |
93 | public static Builder builder() {
94 | return new Builder();
95 | }
96 |
97 | public static class Builder {
98 | private String backgroundFormatName = "jpeg";
99 | private String templateFormatName = "png";
100 | private Boolean obfuscate = false;
101 | private String type = CaptchaTypeConstant.SLIDER;
102 | private String backgroundImageTag;
103 | private String templateImageTag;
104 | private AnyMap param = new AnyMap();
105 |
106 | private Builder() {
107 | }
108 |
109 | public Builder backgroundFormatName(String backgroundFormatName) {
110 | this.backgroundFormatName = backgroundFormatName;
111 | return this;
112 | }
113 |
114 | public Builder templateFormatName(String templateFormatName) {
115 | this.templateFormatName = templateFormatName;
116 | return this;
117 | }
118 |
119 | public Builder obfuscate(Boolean obfuscate) {
120 | this.obfuscate = obfuscate;
121 | return this;
122 | }
123 |
124 | public Builder type(String type) {
125 | this.type = type;
126 | return this;
127 | }
128 |
129 | public Builder backgroundImageTag(String backgroundImageTag) {
130 | this.backgroundImageTag = backgroundImageTag;
131 | return this;
132 | }
133 |
134 | public Builder templateImageTag(String templateImageTag) {
135 | this.templateImageTag = templateImageTag;
136 | return this;
137 | }
138 |
139 | public Builder param(AnyMap param) {
140 | this.param = param;
141 | return this;
142 | }
143 |
144 | public GenerateParam build() {
145 | GenerateParam generateParam = new GenerateParam();
146 | generateParam.backgroundFormatName = backgroundFormatName;
147 | generateParam.templateFormatName = templateFormatName;
148 | generateParam.obfuscate = obfuscate;
149 | generateParam.type = type;
150 | generateParam.backgroundImageTag = backgroundImageTag;
151 | generateParam.templateImageTag = templateImageTag;
152 | generateParam.param = param;
153 | return generateParam;
154 | }
155 | }
156 |
157 |
158 | }
159 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/ImageCaptchaInfo.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 | import lombok.NoArgsConstructor;
6 |
7 | /**
8 | * @Author: 天爱有情
9 | * @Date 2020/5/29 8:04
10 | * @Description 滑块验证码
11 | */
12 | @Data
13 | @AllArgsConstructor
14 | @NoArgsConstructor
15 | public class ImageCaptchaInfo {
16 |
17 | /** 背景图. */
18 | private String backgroundImage;
19 | /** 模板图. */
20 | private String templateImage;
21 | /** 背景图片所属标签. */
22 | private String backgroundImageTag;
23 | /** 模板图片所属标签. */
24 | private String templateImageTag;
25 | /** 背景图片宽度. */
26 | private Integer backgroundImageWidth;
27 | /** 背景图片高度. */
28 | private Integer backgroundImageHeight;
29 | /** 滑块图片宽度. */
30 | private Integer templateImageWidth;
31 | /** 滑块图片高度. */
32 | private Integer templateImageHeight;
33 | /** 随机值. */
34 | private Integer randomX;
35 | /** 容错值, 可以为空 默认 0.02容错,校验的时候用. */
36 | private Float tolerant;
37 | /** 验证码类型. */
38 | private String type;
39 | private CustomData data;
40 |
41 | public ImageCaptchaInfo(String backgroundImage,
42 | String templateImage,
43 | String backgroundImageTag,
44 | String templateImageTag,
45 | Integer backgroundImageWidth,
46 | Integer backgroundImageHeight,
47 | Integer templateImageWidth,
48 | Integer templateImageHeight,
49 | Integer randomX,
50 | String type) {
51 | this.backgroundImage = backgroundImage;
52 | this.templateImage = templateImage;
53 | this.backgroundImageTag = backgroundImageTag;
54 | this.templateImageTag = templateImageTag;
55 | this.backgroundImageWidth = backgroundImageWidth;
56 | this.backgroundImageHeight = backgroundImageHeight;
57 | this.templateImageWidth = templateImageWidth;
58 | this.templateImageHeight = templateImageHeight;
59 | this.randomX = randomX;
60 | this.type = type;
61 | }
62 |
63 | public static ImageCaptchaInfo of(String backgroundImage,
64 | String templateImage,
65 | String backgroundImageTag,
66 | String templateImageTag,
67 | Integer backgroundImageWidth,
68 | Integer backgroundImageHeight,
69 | Integer templateImageWidth,
70 | Integer templateImageHeight,
71 | Integer randomX,
72 | String type) {
73 | return new ImageCaptchaInfo(backgroundImage,
74 | templateImage,
75 | backgroundImageTag,
76 | templateImageTag,
77 | backgroundImageWidth,
78 | backgroundImageHeight,
79 | templateImageWidth,
80 | templateImageHeight,
81 | randomX, type);
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/ImageTransformData.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import lombok.Data;
4 | import lombok.NoArgsConstructor;
5 |
6 | /**
7 | * @Author: 天爱有情
8 | * @date 2023/1/5 11:39
9 | * @Description 图片转换成url后的对象
10 | */
11 | @Data
12 | @NoArgsConstructor
13 | public class ImageTransformData {
14 | /** 背景图. */
15 | private String backgroundImageUrl;
16 | /** 模板图. */
17 | private String templateImageUrl;
18 | /** 留一个扩展数据. */
19 | private Object data;
20 |
21 | public ImageTransformData(String backgroundImageUrl, String templateImageUrl) {
22 | this.backgroundImageUrl = backgroundImageUrl;
23 | this.templateImageUrl = templateImageUrl;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/ParamKey.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | /**
4 | * @Author: 天爱有情
5 | * @date 2024/11/20 11:34
6 | * @Description 此接口的作用是在给 {@link GenerateParam} 添加/获取参数时做一个类型限制和转换
7 | */
8 | public interface ParamKey {
9 |
10 | String getKey();
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/ParamKeyEnum.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 |
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 |
7 | @Getter
8 | @AllArgsConstructor
9 | public class ParamKeyEnum implements ParamKey {
10 |
11 |
12 | /** 点选验证码参与校验的数量. 值为Integer */
13 | public static final ParamKey CLICK_CHECK_CLICK_COUNT = new ParamKeyEnum<>("checkClickCount");
14 | /** 点选验证码干扰数量. 值为Integer */
15 | public static final ParamKey CLICK_INTERFERENCE_COUNT = new ParamKeyEnum<>("interferenceCount");
16 |
17 | private String key;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/RotateImageCaptchaInfo.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
4 | import lombok.Data;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.ToString;
7 |
8 | /**
9 | * @Author: 天爱有情
10 | * @date 2022/4/22 15:49
11 | * @Description 旋转图片
12 | */
13 | @Data
14 | @EqualsAndHashCode(callSuper = true)
15 | @ToString(callSuper = true)
16 | public class RotateImageCaptchaInfo extends ImageCaptchaInfo {
17 | /**
18 | * 旋转多少度
19 | */
20 | private Double degree;
21 | /** 旋转图片的容错值大一点. */
22 | public static final Float DEFAULT_TOLERANT = 0.03F;
23 |
24 | public static RotateImageCaptchaInfo of(Double degree,
25 | Integer randomX,
26 | String backgroundImage,
27 | String templateImage,
28 | String backgroundImageTag,
29 | String templateImageTag,
30 | Integer bgImageWidth,
31 | Integer bgImageHeight,
32 | Integer templateImageWidth,
33 | Integer templateImageHeight) {
34 | RotateImageCaptchaInfo rotateImageCaptchaInfo = new RotateImageCaptchaInfo();
35 | rotateImageCaptchaInfo.setDegree(degree);
36 | rotateImageCaptchaInfo.setRandomX(randomX);
37 | rotateImageCaptchaInfo.setBackgroundImage(backgroundImage);
38 | rotateImageCaptchaInfo.setBackgroundImageTag(backgroundImageTag);
39 | rotateImageCaptchaInfo.setTemplateImageTag(templateImageTag);
40 | rotateImageCaptchaInfo.setTolerant(DEFAULT_TOLERANT);
41 | rotateImageCaptchaInfo.setTemplateImage(templateImage);
42 | rotateImageCaptchaInfo.setBackgroundImageWidth(bgImageWidth);
43 | rotateImageCaptchaInfo.setBackgroundImageHeight(bgImageHeight);
44 | rotateImageCaptchaInfo.setTemplateImageWidth(templateImageWidth);
45 | rotateImageCaptchaInfo.setTemplateImageHeight(templateImageHeight);
46 | // 类型为旋转图片验证码
47 | rotateImageCaptchaInfo.setType(CaptchaTypeConstant.ROTATE);
48 | return rotateImageCaptchaInfo;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/model/dto/SliderImageCaptchaInfo.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.model.dto;
2 |
3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
4 | import lombok.Data;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.ToString;
7 |
8 | @Data
9 | @EqualsAndHashCode(callSuper = true)
10 | @ToString(callSuper = true)
11 | public class SliderImageCaptchaInfo extends ImageCaptchaInfo {
12 | /**
13 | * x轴
14 | */
15 | private Integer x;
16 | /**
17 | * y轴
18 | */
19 | private Integer y;
20 |
21 |
22 | public static SliderImageCaptchaInfo of(Integer x,
23 | Integer y,
24 | String backgroundImage,
25 | String templateImage,
26 | String backgroundImageTag,
27 | String templateImageTag,
28 | Integer bgImageWidth,
29 | Integer bgImageHeight,
30 | Integer sliderImageWidth,
31 | Integer sliderImageHeight) {
32 | SliderImageCaptchaInfo sliderImageCaptchaInfo = new SliderImageCaptchaInfo();
33 | sliderImageCaptchaInfo.setX(x);
34 | sliderImageCaptchaInfo.setY(y);
35 | sliderImageCaptchaInfo.setRandomX(x);
36 | sliderImageCaptchaInfo.setBackgroundImage(backgroundImage);
37 | sliderImageCaptchaInfo.setTemplateImage(templateImage);
38 | sliderImageCaptchaInfo.setBackgroundImageTag(backgroundImageTag);
39 | sliderImageCaptchaInfo.setTemplateImageTag(templateImageTag);
40 | sliderImageCaptchaInfo.setBackgroundImageWidth(bgImageWidth);
41 | sliderImageCaptchaInfo.setBackgroundImageHeight(bgImageHeight);
42 | sliderImageCaptchaInfo.setTemplateImageWidth(sliderImageWidth);
43 | sliderImageCaptchaInfo.setTemplateImageHeight(sliderImageHeight);
44 | sliderImageCaptchaInfo.setType(CaptchaTypeConstant.SLIDER);
45 | return sliderImageCaptchaInfo;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/common/util/ImgWriter.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.common.util;
2 |
3 | import cloud.tianai.captcha.common.util.ObjectUtils;
4 | import lombok.SneakyThrows;
5 |
6 | import javax.imageio.*;
7 | import javax.imageio.stream.ImageOutputStream;
8 | import java.awt.*;
9 | import java.awt.image.BufferedImage;
10 | import java.awt.image.ColorModel;
11 | import java.awt.image.RenderedImage;
12 | import java.io.IOException;
13 | import java.io.OutputStream;
14 | import java.util.Iterator;
15 |
16 | /**
17 | * @Author: 天爱有情
18 | * @date 2022/5/9 11:47
19 | * @Description 拷贝from hutool(https://gitee.com/dromara/hutool/blob/v5-master/hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java)
20 | * 为了不依赖更多无用包, 单独拷贝出来
21 | */
22 | public class ImgWriter {
23 |
24 | /**
25 | * 输出
26 | *
27 | * @param image image
28 | * @param imageType imageType
29 | * @param destImageStream destImageStream
30 | * @param quality quality 0~1
31 | * @return
32 | */
33 | public static boolean write(Image image, String imageType, OutputStream destImageStream, float quality) {
34 | if (ObjectUtils.isEmpty(imageType)) {
35 | imageType = CaptchaImageUtils.TYPE_JPG;
36 | }
37 | ImageOutputStream imageOutputStream = transformImageOutputStream(destImageStream);
38 | final BufferedImage bufferedImage = CaptchaImageUtils.toBufferedImage(image, imageType);
39 | final ImageWriter writer = getWriter(bufferedImage, imageType);
40 | return write(bufferedImage, writer, imageOutputStream, quality);
41 | }
42 |
43 | /**
44 | * 输出
45 | *
46 | * @param image image
47 | * @param writer writer
48 | * @param output output
49 | * @param quality quality
50 | * @return boolean
51 | */
52 | public static boolean write(Image image, ImageWriter writer, ImageOutputStream output, float quality) {
53 | if (writer == null) {
54 | return false;
55 | }
56 | writer.setOutput(output);
57 | final RenderedImage renderedImage = toRenderedImage(image);
58 | // 设置质量
59 | ImageWriteParam imgWriteParams = null;
60 | if (quality > 0 && quality < 1) {
61 | imgWriteParams = writer.getDefaultWriteParam();
62 | if (imgWriteParams.canWriteCompressed()) {
63 | imgWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
64 | imgWriteParams.setCompressionQuality(quality);
65 | final ColorModel colorModel = renderedImage.getColorModel();// ColorModel.getRGBdefault();
66 | imgWriteParams.setDestinationType(new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(16, 16)));
67 | }
68 | }
69 |
70 | try {
71 | if (null != imgWriteParams) {
72 | writer.write(null, new IIOImage(renderedImage, null, null), imgWriteParams);
73 | } else {
74 | writer.write(renderedImage);
75 | }
76 | output.flush();
77 | } catch (IOException e) {
78 | throw new RuntimeException(e);
79 | } finally {
80 | writer.dispose();
81 | }
82 | return true;
83 | }
84 |
85 | public static RenderedImage toRenderedImage(Image img) {
86 | if (img instanceof RenderedImage) {
87 | return (RenderedImage) img;
88 | }
89 | return CaptchaImageUtils.copyImage(img, BufferedImage.TYPE_INT_RGB);
90 | }
91 |
92 | /**
93 | * 获取 ImageWriter
94 | *
95 | * @param img img
96 | * @param formatName formatName
97 | * @return ImageWriter
98 | */
99 | public static ImageWriter getWriter(Image img, String formatName) {
100 | final ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(CaptchaImageUtils.toBufferedImage(img, formatName));
101 | final Iterator iter = ImageIO.getImageWriters(type, formatName);
102 | return iter.hasNext() ? iter.next() : null;
103 | }
104 |
105 | /**
106 | * 将 OutputStream 转换为 ImageOutputStream
107 | *
108 | * @param out out
109 | * @return ImageOutputStream
110 | * @throws RuntimeException
111 | */
112 | @SneakyThrows(IOException.class)
113 | public static ImageOutputStream transformImageOutputStream(OutputStream out) throws RuntimeException {
114 | ImageOutputStream result = ImageIO.createImageOutputStream(out);
115 | if (null == result) {
116 | throw new IllegalArgumentException("Image type is not supported!");
117 | }
118 | return result;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/AbstractClickImageCaptchaGenerator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl;
2 |
3 | import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
4 | import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
5 | import cloud.tianai.captcha.generator.common.model.dto.ClickImageCheckDefinition;
6 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
7 | import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
8 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
9 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
10 | import lombok.AllArgsConstructor;
11 | import lombok.Data;
12 | import lombok.NoArgsConstructor;
13 | import lombok.SneakyThrows;
14 |
15 | import java.awt.*;
16 | import java.awt.image.BufferedImage;
17 | import java.util.ArrayList;
18 | import java.util.List;
19 |
20 | /**
21 | * @Author: 天爱有情
22 | * @date 2022/4/27 11:46
23 | * @Description 点选验证码 点选验证码分为点选文字和点选图标等
24 | */
25 | public abstract class AbstractClickImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
26 |
27 | public static final String CLICK_IMAGE_DISTORT_KEY = "clickImageDistort";
28 |
29 | public AbstractClickImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
30 | super(imageCaptchaResourceManager);
31 | }
32 |
33 | public AbstractClickImageCaptchaGenerator() {
34 | }
35 |
36 | @SneakyThrows
37 | @Override
38 | public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
39 | GenerateParam param = captchaExchange.getParam();
40 | // 文字点选验证码不需要模板 只需要背景图
41 | Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
42 |
43 | BufferedImage bgImage = getResourceImage(resourceImage);
44 |
45 | List imgTips = randomGetClickImgTips(param);
46 | int allImages = imgTips.size();
47 | List clickImageCheckDefinitionList = new ArrayList<>(allImages);
48 | int avg = bgImage.getWidth() / allImages;
49 | if (allImages < imgTips.size()) {
50 | throw new IllegalStateException("随机生成点击图片小于请求数量, 请求生成数量=" + allImages + ",实际生成数量=" + imgTips.size());
51 | }
52 | for (int i = 0; i < allImages; i++) {
53 | // 随机获取点击图片
54 | ImgWrapper imgWrapper = getClickImg(imgTips.get(i),null);
55 | BufferedImage image = imgWrapper.getImage();
56 | int clickImgWidth = image.getWidth();
57 | int clickImgHeight = image.getHeight();
58 | // 随机x
59 | int randomX;
60 | if (i == 0) {
61 | randomX = 1;
62 | } else {
63 | randomX = avg * i;
64 | }
65 | // 随机y
66 | int randomY = randomInt(10, bgImage.getHeight() - clickImgHeight);
67 | // 通过随机x和y 进行覆盖图片
68 | CaptchaImageUtils.overlayImage(bgImage, image, randomX, randomY);
69 | ClickImageCheckDefinition clickImageCheckDefinition = new ClickImageCheckDefinition();
70 | clickImageCheckDefinition.setTip(imgWrapper.getTip());
71 | clickImageCheckDefinition.setX(randomX + clickImgWidth / 2);
72 | clickImageCheckDefinition.setY(randomY + clickImgHeight / 2);
73 | clickImageCheckDefinition.setWidth(clickImgWidth);
74 | clickImageCheckDefinition.setHeight(clickImgHeight);
75 | clickImageCheckDefinition.setImageColor(imgWrapper.getImageColor());
76 | clickImageCheckDefinitionList.add(clickImageCheckDefinition);
77 | }
78 | List checkClickImageCheckDefinitionList = filterAndSortClickImageCheckDefinition(captchaExchange,clickImageCheckDefinitionList);
79 | captchaExchange.setBackgroundImage(bgImage);
80 | captchaExchange.setTransferData(checkClickImageCheckDefinitionList);
81 | captchaExchange.setResourceImage(resourceImage);
82 |
83 |
84 | // // wrap
85 | // ImageCaptchaInfo imageCaptchaInfo = wrapClickImageCaptchaInfo(param, bgImage, checkClickImageCheckDefinitionList, resourceImage, data);
86 | // imageCaptchaInfo.setData(data);
87 | // return imageCaptchaInfo;
88 |
89 | }
90 |
91 | /**
92 | * 过滤并排序校验的图片点选顺序
93 | *
94 | * @param allCheckDefinitionList 总的点选图片
95 | * @return List
96 | */
97 | protected abstract List filterAndSortClickImageCheckDefinition(CaptchaExchange captchaExchange,List allCheckDefinitionList);
98 |
99 | /**
100 | * 随机获取一组数据用于生成随机图
101 | *
102 | * @return List
103 | */
104 | protected abstract List randomGetClickImgTips(GenerateParam param);
105 |
106 | /**
107 | * 随机获取点击的图片
108 | *
109 | * @param tip 提示数据,根据改数据生成图片
110 | * @return ImgWrapper
111 | */
112 | public abstract ImgWrapper getClickImg(Resource tip, Color randomColor);
113 |
114 | /**
115 | * @Author: 天爱有情
116 | * @date 2022/4/28 14:26
117 | * @Description 点击图片包装
118 | */
119 | @Data
120 | @NoArgsConstructor
121 | @AllArgsConstructor
122 | public static class ImgWrapper {
123 | /** 图片. */
124 | private BufferedImage image;
125 | /** 提示. */
126 | private Resource tip;
127 | /** 图片颜色. */
128 | private Color imageColor;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/CacheImageCaptchaGenerator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl;
2 |
3 | import cloud.tianai.captcha.common.util.NamedThreadFactory;
4 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
5 | import cloud.tianai.captcha.generator.ImageTransform;
6 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
7 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
8 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
9 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
10 | import lombok.Getter;
11 | import lombok.Setter;
12 | import lombok.SneakyThrows;
13 | import lombok.extern.slf4j.Slf4j;
14 |
15 | import java.util.Map;
16 | import java.util.concurrent.*;
17 | import java.util.concurrent.atomic.AtomicInteger;
18 |
19 | /**
20 | * @Author: 天爱有情
21 | * @date 2020/10/20 9:23
22 | * @Description 滑块验证码缓冲器
23 | */
24 | @Slf4j
25 | public class CacheImageCaptchaGenerator implements ImageCaptchaGenerator {
26 |
27 | protected final ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("slider-captcha-queue"));
28 | protected Map> queueMap = new ConcurrentHashMap<>(8);
29 | protected Map posMap = new ConcurrentHashMap<>(8);
30 | protected Map lastUpdateMap = new ConcurrentHashMap<>(8);
31 | protected ImageCaptchaGenerator target;
32 | protected int size;
33 | /** 等待时间,一般报错或者拉取为空时会休眠一段时间再试. */
34 | protected int waitTime = 1000;
35 | /** 调度器检查缓存的间隔时间. */
36 | protected int period = 5000;
37 | /** 10天内没有任何操作就删除已缓存的数据. */
38 | protected long expireTime = TimeUnit.DAYS.toMillis(10);
39 | @Getter
40 | @Setter
41 | protected boolean requiredGetCaptcha = true;
42 |
43 | private boolean init = false;
44 |
45 | public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size) {
46 | this.target = target;
47 | this.size = size;
48 | }
49 |
50 | public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size, int waitTime, int period) {
51 | this.target = target;
52 | this.size = size;
53 | this.waitTime = waitTime;
54 | this.period = period;
55 | }
56 |
57 | public CacheImageCaptchaGenerator(ImageCaptchaGenerator target, int size, int waitTime, int period, Long expireTime) {
58 | this.target = target;
59 | this.size = size;
60 | this.waitTime = waitTime;
61 | this.period = period;
62 | if (expireTime != null){
63 | this.expireTime = expireTime;
64 | }
65 | }
66 |
67 | /**
68 | * 记的初始化调度器
69 | */
70 | public void initSchedule() {
71 | init(size);
72 | }
73 |
74 | private void init(int z) {
75 | if (init) {
76 | return;
77 | }
78 | this.size = z;
79 | // 初始化一个队列扫描
80 | scheduledExecutor.scheduleAtFixedRate(() -> queueMap.forEach((k, queue) -> {
81 | try {
82 | AtomicInteger pos = posMap.computeIfAbsent(k, k1 -> new AtomicInteger(0));
83 | int addCount = 0;
84 | while (pos.get() < this.size) {
85 | if (pos.get() >= size) {
86 | return;
87 | }
88 | ImageCaptchaInfo slideImageInfo = target.generateCaptchaImage(k);
89 | if (slideImageInfo != null) {
90 | boolean addStatus = queue.offer(slideImageInfo);
91 | addCount++;
92 | if (addStatus) {
93 | // 添加记录
94 | pos.incrementAndGet();
95 | }
96 | } else {
97 | sleep();
98 | }
99 | }
100 | if (addCount == 0) {
101 | // 没有添加,检测最新更新时间 如果时间过长,直接清除数据
102 | Long lastUpdate = lastUpdateMap.get(k);
103 | if (lastUpdate != null && System.currentTimeMillis() - lastUpdate > expireTime) {
104 | queueMap.remove(k);
105 | posMap.remove(k);
106 | lastUpdateMap.remove(k);
107 | }
108 | }
109 | } catch (Exception e) {
110 | // cache所有
111 | log.error("缓存队列扫描时出错, ex", e);
112 | // 删掉它
113 | queueMap.remove(k);
114 | posMap.remove(k);
115 | lastUpdateMap.remove(k);
116 | // 休眠
117 | sleep();
118 | }
119 | }), 0, period, TimeUnit.MILLISECONDS);
120 | init = true;
121 | }
122 |
123 | private void sleep() {
124 | try {
125 | TimeUnit.MILLISECONDS.sleep(waitTime);
126 | } catch (InterruptedException ignored) {
127 | }
128 | }
129 |
130 | @Override
131 | public ImageCaptchaGenerator init() {
132 | ImageCaptchaGenerator captchaGenerator = target.init();
133 | // 初始化缓存
134 | init(size);;
135 | return captchaGenerator;
136 | }
137 |
138 | @SneakyThrows
139 | @Override
140 | public ImageCaptchaInfo generateCaptchaImage(String type) {
141 | GenerateParam generateParam = new GenerateParam();
142 | generateParam.setType(type);
143 | return generateCaptchaImage(generateParam, this.requiredGetCaptcha);
144 | }
145 |
146 | @SneakyThrows
147 | public ImageCaptchaInfo generateCaptchaImage(GenerateParam generateParam, boolean requiredGetCaptcha) {
148 | ConcurrentLinkedQueue queue = queueMap.get(generateParam);
149 | ImageCaptchaInfo captchaInfo = null;
150 | if (queue != null) {
151 | captchaInfo = queue.poll();
152 | if (captchaInfo == null) {
153 | log.warn("滑块验证码缓存不足, genParam:{}", generateParam);
154 | } else {
155 | AtomicInteger pos = posMap.get(generateParam);
156 | if (pos != null) {
157 | pos.decrementAndGet();
158 | }
159 | }
160 | } else {
161 | queueMap.putIfAbsent(generateParam, new ConcurrentLinkedQueue<>());
162 | posMap.putIfAbsent(generateParam, new AtomicInteger(0));
163 | }
164 | if (captchaInfo == null && requiredGetCaptcha) {
165 | // 直接生成 不走缓存
166 | captchaInfo = target.generateCaptchaImage(generateParam);
167 | }
168 | if (captchaInfo != null) {
169 | // 记录最新时间
170 | lastUpdateMap.put(generateParam, System.currentTimeMillis());
171 | }
172 | return captchaInfo;
173 | }
174 |
175 | @Override
176 | public ImageCaptchaInfo generateCaptchaImage(String type, String targetFormatName, String matrixFormatName) {
177 | return generateCaptchaImage(GenerateParam.builder().type(type).backgroundFormatName(targetFormatName).templateFormatName(matrixFormatName).build(), true);
178 | }
179 |
180 | @Override
181 | public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
182 | return generateCaptchaImage(param, true);
183 | }
184 |
185 | @Override
186 | public ImageCaptchaResourceManager getImageResourceManager() {
187 | return target.getImageResourceManager();
188 | }
189 |
190 | @Override
191 | public void setImageResourceManager(ImageCaptchaResourceManager imageCaptchaResourceManager) {
192 | target.setImageResourceManager(imageCaptchaResourceManager);
193 | }
194 |
195 | @Override
196 | public ImageTransform getImageTransform() {
197 | return target.getImageTransform();
198 | }
199 |
200 | @Override
201 | public void setImageTransform(ImageTransform imageTransform) {
202 | target.setImageTransform(imageTransform);
203 | }
204 |
205 | @Override
206 | public CaptchaInterceptor getInterceptor() {
207 | return target.getInterceptor();
208 | }
209 |
210 | @Override
211 | public void setInterceptor(CaptchaInterceptor interceptor) {
212 | target.setInterceptor(interceptor);
213 | }
214 |
215 | }
216 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl;
2 |
3 | import cloud.tianai.captcha.common.util.ObjectUtils;
4 | import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
5 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
6 | import cloud.tianai.captcha.generator.ImageCaptchaGeneratorProvider;
7 | import cloud.tianai.captcha.generator.ImageTransform;
8 | import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
9 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
10 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
11 | import cloud.tianai.captcha.generator.impl.provider.CommonImageCaptchaGeneratorProvider;
12 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
13 | import lombok.Getter;
14 | import lombok.Setter;
15 |
16 | import java.util.HashMap;
17 | import java.util.Map;
18 | import java.util.concurrent.ConcurrentHashMap;
19 |
20 | import static cloud.tianai.captcha.common.constant.CaptchaTypeConstant.*;
21 |
22 | /**
23 | * @Author: 天爱有情
24 | * @date 2022/4/24 9:27
25 | * @Description 根据type 匹配对应的验证码生成器
26 | */
27 | public class MultiImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
28 |
29 | protected Map imageCaptchaGeneratorMap = new ConcurrentHashMap<>(4);
30 | protected Map imageCaptchaGeneratorProviderMap = new HashMap<>(4);
31 | @Setter
32 | @Getter
33 | private String defaultCaptcha = SLIDER;
34 |
35 | public MultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
36 | super(imageCaptchaResourceManager);
37 | }
38 |
39 | public MultiImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
40 | super(imageCaptchaResourceManager);
41 | setImageTransform(imageTransform);
42 | }
43 |
44 | @Override
45 | protected void doInit() {
46 | // 滑块验证码
47 | addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SLIDER, StandardSliderImageCaptchaGenerator::new));
48 | // 旋转验证码
49 | addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(ROTATE, StandardRotateImageCaptchaGenerator::new));
50 | // 拼接验证码
51 | addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CONCAT, StandardConcatImageCaptchaGenerator::new));
52 | // 点选文字验证码
53 | addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(WORD_IMAGE_CLICK, StandardWordClickImageCaptchaGenerator::new));
54 | }
55 |
56 | public void addImageCaptchaGeneratorProvider(ImageCaptchaGeneratorProvider provider) {
57 | imageCaptchaGeneratorProviderMap.put(provider.getType(), provider);
58 | }
59 |
60 | public ImageCaptchaGeneratorProvider removeImageCaptchaGeneratorProvider(String type) {
61 | return imageCaptchaGeneratorProviderMap.remove(type);
62 | }
63 |
64 | public ImageCaptchaGeneratorProvider getImageCaptchaGeneratorProvider(String type) {
65 | return imageCaptchaGeneratorProviderMap.get(type);
66 | }
67 |
68 | public void addImageCaptchaGenerator(String key, ImageCaptchaGenerator captchaGenerator) {
69 | imageCaptchaGeneratorMap.put(key, captchaGenerator);
70 | }
71 |
72 | public ImageCaptchaGenerator removeImageCaptchaGenerator(String key) {
73 | return imageCaptchaGeneratorMap.remove(key);
74 | }
75 |
76 | public ImageCaptchaGenerator getImageCaptchaGenerator(String key) {
77 | return imageCaptchaGeneratorMap.get(key);
78 | }
79 |
80 | @Override
81 | public ImageCaptchaInfo generateCaptchaImage(GenerateParam param) {
82 | String type = param.getType();
83 | if (ObjectUtils.isEmpty(type)) {
84 | param.setType(defaultCaptcha);
85 | type = defaultCaptcha;
86 | }
87 | ImageCaptchaGenerator imageCaptchaGenerator = requireGetCaptchaGenerator(type);
88 | return imageCaptchaGenerator.generateCaptchaImage(param);
89 | }
90 |
91 |
92 | @Override
93 | protected void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
94 |
95 | }
96 |
97 | @Override
98 | protected ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
99 | return null;
100 | }
101 |
102 | public ImageCaptchaGenerator requireGetCaptchaGenerator(String type) {
103 | ImageCaptchaGenerator imageCaptchaGenerator = imageCaptchaGeneratorMap.computeIfAbsent(type, t -> {
104 | ImageCaptchaGeneratorProvider provider = imageCaptchaGeneratorProviderMap.get(t);
105 | if (provider == null) {
106 | throw new IllegalArgumentException("生成验证码失败,错误的type类型:" + t);
107 | }
108 | return provider.get(getImageResourceManager(), getImageTransform(), getInterceptor()).init();
109 | });
110 | return imageCaptchaGenerator;
111 | }
112 |
113 | @Override
114 | public void setImageResourceManager(ImageCaptchaResourceManager imageCaptchaResourceManager) {
115 | super.setImageResourceManager(imageCaptchaResourceManager);
116 | for (ImageCaptchaGenerator imageCaptchaGenerator : imageCaptchaGeneratorMap.values()) {
117 | imageCaptchaGenerator.setImageResourceManager(imageCaptchaResourceManager);
118 | }
119 | }
120 |
121 | @Override
122 | public void setImageTransform(ImageTransform imageTransform) {
123 | super.setImageTransform(imageTransform);
124 | for (ImageCaptchaGenerator imageCaptchaGenerator : imageCaptchaGeneratorMap.values()) {
125 | imageCaptchaGenerator.setImageTransform(imageTransform);
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/StandardConcatImageCaptchaGenerator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl;
2 |
3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
4 | import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
5 | import cloud.tianai.captcha.generator.ImageTransform;
6 | import cloud.tianai.captcha.generator.common.model.dto.*;
7 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
8 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
9 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
10 | import lombok.SneakyThrows;
11 |
12 | import java.awt.image.BufferedImage;
13 |
14 | import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.concatImage;
15 | import static cloud.tianai.captcha.generator.common.util.CaptchaImageUtils.splitImage;
16 |
17 | /**
18 | * @Author: 天爱有情
19 | * @date 2022/4/25 15:44
20 | * @Description 图片拼接滑动验证码生成器
21 | */
22 | public class StandardConcatImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
23 |
24 | public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
25 | super(imageCaptchaResourceManager);
26 | }
27 |
28 | public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
29 | super(imageCaptchaResourceManager);
30 | setImageTransform(imageTransform);
31 | }
32 |
33 | public StandardConcatImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
34 | super(imageCaptchaResourceManager);
35 | setImageTransform(imageTransform);
36 | setInterceptor(interceptor);
37 | }
38 |
39 | @Override
40 | protected void doInit() {
41 | }
42 |
43 |
44 | @Override
45 | public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
46 | GenerateParam param = captchaExchange.getParam();
47 | // 拼接验证码不需要模板 只需要背景图
48 | Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
49 | BufferedImage bgImage = getResourceImage(resourceImage);
50 | int spacingY = bgImage.getHeight() / 4;
51 | int randomY = randomInt(spacingY, bgImage.getHeight() - spacingY);
52 | BufferedImage[] bgImageSplit = splitImage(randomY, true, bgImage);
53 | int spacingX = bgImage.getWidth() / 8;
54 | int randomX = randomInt(spacingX, bgImage.getWidth() - bgImage.getWidth() / 5);
55 | BufferedImage[] bgImageTopSplit = splitImage(randomX, false, bgImageSplit[0]);
56 |
57 | BufferedImage sliderImage = concatImage(true,
58 | bgImageTopSplit[0].getWidth()
59 | + bgImageTopSplit[1].getWidth(), bgImageTopSplit[0].getHeight(), bgImageTopSplit[1], bgImageTopSplit[0]);
60 | bgImage = concatImage(false, bgImageSplit[1].getWidth(), sliderImage.getHeight() + bgImageSplit[1].getHeight(),
61 | sliderImage, bgImageSplit[1]);
62 | Data data = new Data();
63 | data.x = randomX;
64 | data.y = randomY;
65 |
66 | captchaExchange.setTransferData(data);
67 | captchaExchange.setBackgroundImage(bgImage);
68 | captchaExchange.setResourceImage(resourceImage);
69 | }
70 |
71 | public static class Data {
72 | int x;
73 | int y;
74 | }
75 |
76 | @SneakyThrows
77 | @Override
78 | public ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
79 | GenerateParam param = captchaExchange.getParam();
80 | BufferedImage bgImage = captchaExchange.getBackgroundImage();
81 | Resource resourceImage = captchaExchange.getResourceImage();
82 | CustomData customData = captchaExchange.getCustomData();
83 | ImageTransformData transform = getImageTransform().transform(param, bgImage, resourceImage, customData);
84 | Data data = (Data) captchaExchange.getTransferData();
85 | ImageCaptchaInfo imageCaptchaInfo = ImageCaptchaInfo.of(transform.getBackgroundImageUrl(),
86 | null,
87 | resourceImage.getTag(),
88 | null,
89 | bgImage.getWidth(),
90 | bgImage.getHeight(),
91 | null,
92 | null,
93 | data.x,
94 | CaptchaTypeConstant.CONCAT);
95 | customData.putViewData("randomY", data.y);
96 | imageCaptchaInfo.setTolerant(0.05F);
97 | return imageCaptchaInfo;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl;
2 |
3 | import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
4 | import cloud.tianai.captcha.generator.ImageTransform;
5 | import cloud.tianai.captcha.generator.common.model.dto.*;
6 | import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
7 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
8 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
9 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
10 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
11 | import lombok.SneakyThrows;
12 |
13 | import java.awt.image.BufferedImage;
14 | import java.util.Optional;
15 |
16 | /**
17 | * @Author: 天爱有情
18 | * @date 2022/4/22 16:43
19 | * @Description 旋转图片验证码生成器
20 | */
21 | public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
22 |
23 | /** 模板滑块固定名称. */
24 | public static String TEMPLATE_ACTIVE_IMAGE_NAME = "active.png";
25 | /** 模板凹槽固定名称. */
26 | public static String TEMPLATE_FIXED_IMAGE_NAME = "fixed.png";
27 | /** 模板蒙版. */
28 | public static String TEMPLATE_MASK_IMAGE_NAME = "mask.png";
29 |
30 | public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
31 | super(imageCaptchaResourceManager);
32 | }
33 |
34 | public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
35 | super(imageCaptchaResourceManager);
36 | setImageTransform(imageTransform);
37 | }
38 |
39 | public StandardRotateImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
40 | super(imageCaptchaResourceManager);
41 | setImageTransform(imageTransform);
42 | setInterceptor(interceptor);
43 | }
44 | @Override
45 | protected void doInit() {
46 | }
47 |
48 |
49 | @Override
50 | public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
51 | GenerateParam param = captchaExchange.getParam();
52 | CustomData data = new CustomData();
53 | ResourceMap templateResource = requiredRandomGetTemplate(param.getType(), param.getTemplateImageTag());
54 | Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
55 | BufferedImage background = getResourceImage(resourceImage);
56 |
57 | BufferedImage fixedTemplate = getTemplateImage(templateResource, TEMPLATE_FIXED_IMAGE_NAME);
58 | BufferedImage activeTemplate = getTemplateImage(templateResource, TEMPLATE_ACTIVE_IMAGE_NAME);
59 | BufferedImage maskTemplate = fixedTemplate;
60 | Optional maskTemplateOptional = getTemplateImageOfOptional(templateResource, TEMPLATE_MASK_IMAGE_NAME);
61 | if (maskTemplateOptional.isPresent()) {
62 | maskTemplate = maskTemplateOptional.get();
63 | }
64 |
65 | // 算出居中的x和y
66 | int x = background.getWidth() / 2 - fixedTemplate.getWidth() / 2;
67 | int y = background.getHeight() / 2 - fixedTemplate.getHeight() / 2;
68 |
69 | // 抠图部分
70 | BufferedImage cutImage = CaptchaImageUtils.cutImage(background, maskTemplate, x, y);
71 | BufferedImage rotateFixed = fixedTemplate;
72 | BufferedImage rotateActive = activeTemplate;
73 | if (param.getObfuscate()) {
74 | int randomDegree = randomInt(10, 350);
75 | rotateFixed = CaptchaImageUtils.rotateImage(fixedTemplate, randomDegree);
76 | randomDegree = randomInt(10, 350);
77 | rotateActive = CaptchaImageUtils.rotateImage(activeTemplate, randomDegree);
78 | }
79 | CaptchaImageUtils.overlayImage(background, rotateFixed, x, y);
80 | CaptchaImageUtils.overlayImage(cutImage, rotateActive, 0, 0);
81 | // 随机旋转抠图部分
82 | // 随机x, 转换为角度
83 | int randomX = randomInt(fixedTemplate.getWidth() + 10, background.getWidth() - 10);
84 | double degree = 360d - randomX / ((background.getWidth()) / 360d);
85 | // 旋转的透明图片是一张正方形的
86 | BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(cutImage.getWidth(), background.getHeight());
87 | CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree);
88 |
89 | RotateData rotateData = new RotateData();
90 | rotateData.degree = degree;
91 | rotateData.randomX = randomX;
92 | captchaExchange.setTransferData(rotateData);
93 | captchaExchange.setBackgroundImage(background);
94 | captchaExchange.setTemplateImage(matrixTemplate);
95 | captchaExchange.setTemplateResource(templateResource);
96 | captchaExchange.setResourceImage(resourceImage);
97 |
98 | // return wrapRotateCaptchaInfo(degree, randomX, background, matrixTemplate, param, templateResource, resourceImage, data);
99 | }
100 |
101 | public static class RotateData {
102 | double degree;
103 | int randomX;
104 | }
105 |
106 | private String getObfuscateTag(String templateTag) {
107 | if (templateTag == null) {
108 | return "obfuscate";
109 | }
110 | return templateTag + "_" + "obfuscate";
111 | }
112 |
113 | @SneakyThrows
114 | @Override
115 | public ImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
116 | GenerateParam param = captchaExchange.getParam();
117 | BufferedImage backgroundImage = captchaExchange.getBackgroundImage();
118 | BufferedImage sliderImage = captchaExchange.getTemplateImage();
119 | Resource resourceImage = captchaExchange.getResourceImage();
120 | ResourceMap templateResource = captchaExchange.getTemplateResource();
121 | CustomData data = captchaExchange.getCustomData();
122 | RotateData rotateData = (RotateData) captchaExchange.getTransferData();
123 | ImageTransformData transform = getImageTransform().transform(param, backgroundImage, sliderImage, resourceImage, templateResource, data);
124 | RotateImageCaptchaInfo imageCaptchaInfo = RotateImageCaptchaInfo.of(rotateData.degree,
125 | rotateData.randomX,
126 | transform.getBackgroundImageUrl(),
127 | transform.getTemplateImageUrl(),
128 | resourceImage.getTag(),
129 | templateResource.getTag(),
130 | backgroundImage.getWidth(), backgroundImage.getHeight(),
131 | sliderImage.getWidth(), sliderImage.getHeight()
132 | );
133 | imageCaptchaInfo.setData(data);
134 | return imageCaptchaInfo;
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/StandardSliderImageCaptchaGenerator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl;
2 |
3 | import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
4 | import cloud.tianai.captcha.generator.ImageTransform;
5 | import cloud.tianai.captcha.generator.common.model.dto.*;
6 | import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
7 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
8 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
9 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
10 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
11 | import lombok.SneakyThrows;
12 | import lombok.extern.slf4j.Slf4j;
13 |
14 | import java.awt.*;
15 | import java.awt.image.BufferedImage;
16 | import java.util.Optional;
17 | import java.util.concurrent.ThreadLocalRandom;
18 |
19 | /**
20 | * @Author: 天爱有情
21 | * @Date 2020/5/29 8:06
22 | * @Description 滑块验证码模板
23 | */
24 | @Slf4j
25 | public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
26 |
27 |
28 | /** 模板滑块固定名称. */
29 | public static String TEMPLATE_ACTIVE_IMAGE_NAME = "active.png";
30 | /** 模板凹槽固定名称. */
31 | public static String TEMPLATE_FIXED_IMAGE_NAME = "fixed.png";
32 | /** 模板蒙版. */
33 | public static String TEMPLATE_MASK_IMAGE_NAME = "mask.png";
34 | /** 混淆的凹槽. */
35 | public static String OBFUSCATE_TEMPLATE_FIXED_IMAGE_NAME = "obfuscate_" + TEMPLATE_FIXED_IMAGE_NAME;
36 |
37 |
38 | public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager) {
39 | super(imageCaptchaResourceManager);
40 | }
41 |
42 | public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform) {
43 | super(imageCaptchaResourceManager);
44 | setImageTransform(imageTransform);
45 | }
46 |
47 | public StandardSliderImageCaptchaGenerator(ImageCaptchaResourceManager imageCaptchaResourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
48 | super(imageCaptchaResourceManager);
49 | setImageTransform(imageTransform);
50 | setInterceptor(interceptor);
51 | }
52 |
53 |
54 | @Override
55 | protected void doInit() {
56 |
57 | }
58 |
59 | @SneakyThrows
60 | @Override
61 | public void doGenerateCaptchaImage(CaptchaExchange captchaExchange) {
62 | GenerateParam param = captchaExchange.getParam();
63 | Boolean obfuscate = param.getObfuscate();
64 | ResourceMap templateResource = requiredRandomGetTemplate(param.getType(), param.getTemplateImageTag());
65 | Resource resourceImage = requiredRandomGetResource(param.getType(), param.getBackgroundImageTag());
66 | BufferedImage background = getResourceImage(resourceImage);
67 | BufferedImage fixedTemplate = getTemplateImage(templateResource, TEMPLATE_FIXED_IMAGE_NAME);
68 | BufferedImage activeTemplate = getTemplateImage(templateResource, TEMPLATE_ACTIVE_IMAGE_NAME);
69 | BufferedImage maskTemplate = fixedTemplate;
70 | Optional maskTemplateOptional = getTemplateImageOfOptional(templateResource, TEMPLATE_MASK_IMAGE_NAME);
71 | if (maskTemplateOptional.isPresent()) {
72 | maskTemplate = maskTemplateOptional.get();
73 | }
74 | // 获取随机的 x 和 y 轴
75 | int randomX = randomInt(fixedTemplate.getWidth() + 5, background.getWidth() - fixedTemplate.getWidth() - 10);
76 | int randomY = randomInt(background.getHeight() - fixedTemplate.getHeight());
77 |
78 | BufferedImage cutImage = CaptchaImageUtils.cutImage(background, maskTemplate, randomX, randomY);
79 | CaptchaImageUtils.overlayImage(background, fixedTemplate, randomX, randomY);
80 | if (obfuscate) {
81 | Optional obfuscateFixedTemplate = getTemplateImageOfOptional(templateResource, OBFUSCATE_TEMPLATE_FIXED_IMAGE_NAME);
82 | BufferedImage obfuscateImage = obfuscateFixedTemplate.orElseGet(() -> createObfuscate(fixedTemplate));
83 | int obfuscateX = randomObfuscateX(randomX, fixedTemplate.getWidth(), background.getWidth());
84 | CaptchaImageUtils.overlayImage(background, obfuscateImage, obfuscateX, randomY);
85 | }
86 | CaptchaImageUtils.overlayImage(cutImage, activeTemplate, 0, 0);
87 | // 这里创建一张png透明图片
88 | BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(activeTemplate.getWidth(), background.getHeight());
89 | CaptchaImageUtils.overlayImage(matrixTemplate, cutImage, 0, randomY);
90 |
91 | XandY xandY = new XandY();
92 | xandY.x = randomX;
93 | xandY.y = randomY;
94 | captchaExchange.setBackgroundImage(background);
95 | captchaExchange.setTemplateImage(matrixTemplate);
96 | captchaExchange.setTemplateResource(templateResource);
97 | captchaExchange.setResourceImage(resourceImage);
98 | captchaExchange.setTransferData(xandY);
99 | // 后处理
100 | // applyPostProcessorBeforeWrapImageCaptchaInfo(captchaExchange, this);
101 | // imageCaptchaInfo = wrapSliderCaptchaInfo(randomX, randomY, captchaExchange);
102 | // applyPostProcessorAfterGenerateCaptchaImage(captchaExchange, imageCaptchaInfo, this);
103 | // return imageCaptchaInfo;
104 | }
105 |
106 | protected BufferedImage createObfuscate(BufferedImage fixedImage) {
107 | // 随机拉伸或缩放宽高, 每次只拉伸高或者宽
108 | int width = fixedImage.getWidth();
109 | int height = fixedImage.getHeight();
110 | int window = randomInt(-3, 4);
111 | if (randomBoolean()) {
112 | height = height + window * 5;
113 | } else {
114 | width = width + window * 5;
115 | }
116 | int type = fixedImage.getColorModel().getTransparency();
117 | BufferedImage image = new BufferedImage(width, height, type);
118 | Graphics2D graphics = image.createGraphics();
119 | // 透明度
120 | double alpha = ThreadLocalRandom.current().nextDouble(0.5, 0.8);
121 | AlphaComposite alphaComposite = AlphaComposite.Src.derive((float) alpha);
122 | graphics.setComposite(alphaComposite);
123 | graphics.drawImage(fixedImage, 0, 0, width, height, null);
124 | return image;
125 | }
126 |
127 |
128 | public static class XandY {
129 | int x;
130 | int y;
131 | }
132 |
133 | @SneakyThrows
134 | @Override
135 | public SliderImageCaptchaInfo doWrapImageCaptchaInfo(CaptchaExchange captchaExchange) {
136 | GenerateParam param = captchaExchange.getParam();
137 | BufferedImage backgroundImage = captchaExchange.getBackgroundImage();
138 | BufferedImage sliderImage = captchaExchange.getTemplateImage();
139 | Resource resourceImage = captchaExchange.getResourceImage();
140 | ResourceMap templateResource = captchaExchange.getTemplateResource();
141 | CustomData customData = captchaExchange.getCustomData();
142 | XandY data = (XandY) captchaExchange.getTransferData();
143 | ImageTransformData transform = getImageTransform().transform(param, backgroundImage, sliderImage, resourceImage, templateResource, customData);
144 |
145 | SliderImageCaptchaInfo imageCaptchaInfo = SliderImageCaptchaInfo.of(data.x, data.y,
146 | transform.getBackgroundImageUrl(),
147 | transform.getTemplateImageUrl(),
148 | resourceImage.getTag(),
149 | templateResource.getTag(),
150 | backgroundImage.getWidth(), backgroundImage.getHeight(),
151 | sliderImage.getWidth(), sliderImage.getHeight()
152 | );
153 | imageCaptchaInfo.setData(customData);
154 | return imageCaptchaInfo;
155 | }
156 |
157 | protected int randomObfuscateX(int sliderX, int slWidth, int bgWidth) {
158 | if (bgWidth / 2 > (sliderX + (slWidth / 2))) {
159 | // 右边混淆
160 | return randomInt(sliderX + slWidth, bgWidth - slWidth);
161 | }
162 | // 左边混淆
163 | return randomInt(slWidth, sliderX - slWidth);
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/provider/CommonImageCaptchaGeneratorProvider.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl.provider;
2 |
3 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
4 | import cloud.tianai.captcha.generator.ImageCaptchaGeneratorProvider;
5 | import cloud.tianai.captcha.generator.ImageTransform;
6 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
7 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
8 |
9 | public class CommonImageCaptchaGeneratorProvider implements ImageCaptchaGeneratorProvider {
10 |
11 | private String type;
12 | private ImageCaptchaGeneratorProvider provider;
13 |
14 | public CommonImageCaptchaGeneratorProvider(String type, ImageCaptchaGeneratorProvider provider) {
15 | this.type = type;
16 | this.provider = provider;
17 |
18 | }
19 |
20 | @Override
21 | public ImageCaptchaGenerator get(ImageCaptchaResourceManager resourceManager, ImageTransform imageTransform, CaptchaInterceptor interceptor) {
22 | return provider.get(resourceManager, imageTransform,interceptor);
23 | }
24 |
25 | @Override
26 | public String getType() {
27 | return type;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/generator/impl/transform/Base64ImageTransform.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.generator.impl.transform;
2 |
3 | import cloud.tianai.captcha.generator.ImageTransform;
4 | import cloud.tianai.captcha.generator.common.model.dto.CustomData;
5 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
6 | import cloud.tianai.captcha.generator.common.model.dto.ImageTransformData;
7 | import cloud.tianai.captcha.generator.common.util.CaptchaImageUtils;
8 | import cloud.tianai.captcha.generator.common.util.ImgWriter;
9 | import lombok.SneakyThrows;
10 |
11 | import javax.imageio.ImageIO;
12 | import java.awt.image.BufferedImage;
13 | import java.io.ByteArrayOutputStream;
14 | import java.io.IOException;
15 | import java.util.Base64;
16 |
17 | /**
18 | * @Author: 天爱有情
19 | * @date 2022/8/25 10:28
20 | * @Description base64 实现
21 | */
22 | public class Base64ImageTransform implements ImageTransform {
23 |
24 | @SneakyThrows(IOException.class)
25 | public String transform(BufferedImage bufferedImage, String transformType) {
26 | // 这里判断处理一下,加一些警告日志
27 | String result = beforeTransform(bufferedImage, transformType);
28 | if (result != null) {
29 | return result;
30 | }
31 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
32 | if (CaptchaImageUtils.isPng(transformType) || CaptchaImageUtils.isJpeg(transformType)) {
33 | // 如果是 jpg 或者 png图片的话 用hutool的生成
34 | ImgWriter.write(bufferedImage, transformType, byteArrayOutputStream, -1);
35 | } else {
36 | ImageIO.write(bufferedImage, transformType, byteArrayOutputStream);
37 | }
38 | //转换成字节码
39 | byte[] data = byteArrayOutputStream.toByteArray();
40 | String base64 = Base64.getEncoder().encodeToString(data);
41 | return "data:image/" + transformType + ";base64,".concat(base64);
42 | }
43 |
44 | public String beforeTransform(BufferedImage bufferedImage, String formatType) {
45 | // int type = bufferedImage.getType();
46 | // if (BufferedImage.TYPE_4BYTE_ABGR == type) {
47 | // // png , 如果转换的是jpg的话
48 | // if (CaptchaImageUtils.isJpeg(formatType)) {
49 | // // bufferedImage为 png, 但是转换的图片为 jpg
50 | // if (log.isWarnEnabled()) {
51 | // log.warn("图片验证码转换警告, 原图为 png格式时,指定转换的图片为jpg格式时可能会导致转换异常,如果转换的图片为出现错误,请设置指定转换的类型与原图的类型一致");
52 | // } else {
53 | // System.err.println("图片验证码转换警告, 原图为 png格式时,指定转换的图片为jpg格式时可能会导致转换异常,如果转换的图片为出现错误,请设置指定转换的类型与原图的类型一致");
54 | // }
55 | // }
56 | // }
57 | // 其它的暂时不考虑
58 | return null;
59 | }
60 |
61 | @Override
62 | public ImageTransformData transform(GenerateParam param, BufferedImage backgroundImage, BufferedImage templateImage, Object backgroundResource, Object templateResource, CustomData data) {
63 | ImageTransformData imageTransformData = new ImageTransformData();
64 | if (backgroundImage != null) {
65 | imageTransformData.setBackgroundImageUrl(transform(backgroundImage, param.getBackgroundFormatName()));
66 | }
67 | if (templateImage != null) {
68 | imageTransformData.setTemplateImageUrl(transform(templateImage, param.getTemplateFormatName()));
69 | }
70 | return imageTransformData;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptor.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.interceptor;
2 |
3 | import cloud.tianai.captcha.application.vo.CaptchaResponse;
4 | import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
5 | import cloud.tianai.captcha.common.AnyMap;
6 | import cloud.tianai.captcha.common.response.ApiResponse;
7 | import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
8 | import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
9 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
10 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
11 | import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
12 |
13 | // ============================ 拦截器执行顺序 ============================
14 |
15 | // =================== 生成验证码 ===================
16 | // beforeGenerateCaptcha(...) ↓
17 | // beforeGenerateCaptchaImage(...) ↓
18 | // beforeWrapImageCaptchaInfo(...) ↓
19 | // afterGenerateCaptchaImage(...) ↓
20 | // beforeGenerateImageCaptchaValidData(...) ↓
21 | // afterGenerateImageCaptchaValidData(...) ↓
22 | // afterGenerateCaptcha(...) ↓
23 | // =================== 验证码校验 ===================
24 | // beforeValid(...) ↓
25 | // afterValid(...) ↓
26 |
27 | // ============================ 拦截器执行顺序 ============================
28 |
29 | /**
30 | * @Author: 天爱有情
31 | * @date 2024/7/11 18:05
32 | * @Description 验证码拦截器
33 | */
34 | public interface CaptchaInterceptor {
35 |
36 | default String getName() {
37 | return "interceptor";
38 | }
39 |
40 | default Context createContext() {
41 | return new Context(getName(), null, -1, 1, EmptyCaptchaInterceptor.INSTANCE);
42 | }
43 |
44 | default CaptchaResponse beforeGenerateCaptcha(Context context, String type, GenerateParam param) {
45 | return null;
46 | }
47 |
48 | default CaptchaResponse beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) {
49 | return null;
50 | }
51 |
52 | default void afterGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, AnyMap validData) {
53 | }
54 |
55 | default void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, CaptchaResponse captchaResponse) {
56 | }
57 |
58 | default ApiResponse> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
59 | Object preReturn = context.getPreReturnData();
60 | if (preReturn != null) {
61 | return (ApiResponse>) preReturn;
62 | }
63 | return ApiResponse.ofSuccess();
64 | }
65 |
66 | default ApiResponse> afterValid(Context context, String type, MatchParam matchParam, AnyMap validData, ApiResponse> basicValid) {
67 | Object preReturn = context.getPreReturnData();
68 | if (preReturn != null) {
69 | return (ApiResponse>) preReturn;
70 | }
71 | return ApiResponse.ofSuccess();
72 | }
73 |
74 | default ImageCaptchaInfo beforeGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
75 | return null;
76 | }
77 |
78 | default void beforeWrapImageCaptchaInfo(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
79 |
80 | }
81 |
82 | default void afterGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo, AbstractImageCaptchaGenerator generator) {
83 |
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/interceptor/CaptchaInterceptorGroup.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.interceptor;
2 |
3 | import cloud.tianai.captcha.application.vo.CaptchaResponse;
4 | import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
5 | import cloud.tianai.captcha.common.AnyMap;
6 | import cloud.tianai.captcha.common.response.ApiResponse;
7 | import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
8 | import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
9 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
10 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
11 | import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
12 | import lombok.Getter;
13 | import lombok.Setter;
14 |
15 | import java.util.ArrayList;
16 | import java.util.List;
17 |
18 | public class CaptchaInterceptorGroup implements CaptchaInterceptor {
19 |
20 |
21 | private String name = "group_interceptor";
22 |
23 | @Getter
24 | @Setter
25 | private List validators = new ArrayList<>();
26 |
27 | public void addInterceptor(CaptchaInterceptor validator) {
28 | validators.add(validator);
29 | }
30 |
31 | public void addInterceptor(List validators) {
32 | this.validators.addAll(validators);
33 | }
34 |
35 | @Override
36 | public String getName() {
37 | return name;
38 | }
39 |
40 | public void setName(String name) {
41 | this.name = name;
42 | }
43 |
44 | public CaptchaInterceptorGroup() {
45 | }
46 |
47 | public CaptchaInterceptorGroup(String name) {
48 | this.name = name;
49 | }
50 |
51 | @Override
52 | public Context createContext() {
53 | return new Context(getName(), null, -1, validators.size(), this);
54 | }
55 |
56 | protected Context createContextIfNecessary(Context context) {
57 | if (context == null) {
58 | return createContext();
59 | }
60 | if (!context.getGroup().equals(this)) {
61 | Context innerContext = createContext();
62 | innerContext.setParent(context);
63 | context = innerContext;
64 | }
65 | return context;
66 | }
67 |
68 | @Override
69 | public CaptchaResponse beforeGenerateCaptcha(Context context, String type, GenerateParam param) {
70 | context = createContextIfNecessary(context);
71 | CaptchaResponse captchaResponse = null;
72 | while (context.next() < context.getCount()) {
73 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
74 | captchaResponse = interceptor.beforeGenerateCaptcha(context, type, param);
75 | context.setPreReturnData(captchaResponse);
76 | }
77 | return captchaResponse;
78 | }
79 |
80 | @Override
81 | public void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, CaptchaResponse captchaResponse) {
82 | context = createContextIfNecessary(context);
83 | while (context.next() < context.getCount()) {
84 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
85 | interceptor.afterGenerateCaptcha(context, type, imageCaptchaInfo, captchaResponse);
86 | }
87 | }
88 |
89 | @Override
90 | public ApiResponse> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
91 | context = createContextIfNecessary(context);
92 | ApiResponse> beforeValid = null;
93 | while (context.next() < context.getCount()) {
94 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
95 | beforeValid = interceptor.beforeValid(context, type, matchParam, validData);
96 | context.setPreReturnData(beforeValid);
97 | }
98 | return beforeValid == null ? ApiResponse.ofSuccess() : beforeValid;
99 | }
100 |
101 | @Override
102 | public ApiResponse> afterValid(Context context, String type, MatchParam matchParam, AnyMap validData, ApiResponse> basicValid) {
103 | context = createContextIfNecessary(context);
104 | ApiResponse> valid = null;
105 | while (context.next() < context.getCount()) {
106 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
107 | valid = interceptor.afterValid(context, type, matchParam, validData, basicValid);
108 | context.setPreReturnData(valid);
109 | }
110 | return valid == null ? ApiResponse.ofSuccess() : valid;
111 | }
112 |
113 | @Override
114 | public CaptchaResponse beforeGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo) {
115 | context = createContextIfNecessary(context);
116 | CaptchaResponse captchaResponse = null;
117 | while (context.next() < context.getCount()) {
118 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
119 | captchaResponse = interceptor.beforeGenerateImageCaptchaValidData(context, type, imageCaptchaInfo);
120 | context.setPreReturnData(captchaResponse);
121 | }
122 | return captchaResponse;
123 |
124 |
125 | }
126 |
127 | @Override
128 | public void afterGenerateImageCaptchaValidData(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, AnyMap validData) {
129 | context = createContextIfNecessary(context);
130 | while (context.next() < context.getCount()) {
131 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
132 | interceptor.afterGenerateImageCaptchaValidData(context, type, imageCaptchaInfo, validData);
133 | }
134 | }
135 |
136 | @Override
137 | public ImageCaptchaInfo beforeGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
138 | context = createContextIfNecessary(context);
139 | ImageCaptchaInfo response = null;
140 | while (context.next() < context.getCount()) {
141 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
142 | response = interceptor.beforeGenerateCaptchaImage(context, captchaExchange, generator);
143 | }
144 | return response;
145 | }
146 |
147 | @Override
148 | public void beforeWrapImageCaptchaInfo(Context context, CaptchaExchange captchaExchange, AbstractImageCaptchaGenerator generator) {
149 | context = createContextIfNecessary(context);
150 | while (context.next() < context.getCount()) {
151 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
152 | interceptor.beforeWrapImageCaptchaInfo(context, captchaExchange, generator);
153 | }
154 | }
155 |
156 | @Override
157 | public void afterGenerateCaptchaImage(Context context, CaptchaExchange captchaExchange, ImageCaptchaInfo imageCaptchaInfo, AbstractImageCaptchaGenerator generator) {
158 | context = createContextIfNecessary(context);
159 | while (context.next() < context.getCount()) {
160 | CaptchaInterceptor interceptor = validators.get(context.getCurrent());
161 | interceptor.afterGenerateCaptchaImage(context, captchaExchange, imageCaptchaInfo, generator);
162 | }
163 | }
164 |
165 |
166 | public String printTree() {
167 | return doPrintTree(1);
168 | }
169 |
170 | private String doPrintTree(int index) {
171 | StringBuilder sb = new StringBuilder();
172 | StringBuilder start = new StringBuilder();
173 |
174 | for (int i = 0; i < index; i++) {
175 | start.append("|-----");
176 | }
177 | for (int i = 0; i < validators.size(); i++) {
178 | CaptchaInterceptor validator = validators.get(i);
179 | sb.append(start).append("[").append(validator.getName()).append("]").append("\n");
180 | if (validator instanceof CaptchaInterceptorGroup) {
181 | sb.append(((CaptchaInterceptorGroup) validator).doPrintTree(index + 1));
182 | }
183 | }
184 | return sb.toString();
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/interceptor/Context.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.interceptor;
2 |
3 | import cloud.tianai.captcha.common.AnyMap;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 |
7 | /**
8 | * @Author: 天爱有情
9 | * @date 2024/7/11 16:22
10 | * @Description 拦截器的上下文参数
11 | */
12 | @Getter
13 | public class Context {
14 | /** 名称. */
15 | private String name;
16 | /** 父容器. */
17 | @Setter
18 | private Context parent;
19 | /** 当前拦截器数量. */
20 | private Integer current;
21 | /** 拦截器总数. */
22 | private Integer count;
23 | /** 拦截器组. */
24 | private CaptchaInterceptor group;
25 | /** The previous interceptor returns data. */
26 | @Setter
27 | private Object preReturnData;
28 | /** 传输数据. */
29 | private AnyMap data = new AnyMap();
30 |
31 | public Context(String name, Context parent, Integer current, Integer count, CaptchaInterceptor group) {
32 | this.name = name;
33 | this.parent = parent;
34 | this.current = current;
35 | this.count = count;
36 | this.group = group;
37 | }
38 |
39 | public Object getPreReturnData() {
40 | Object returnData = preReturnData;
41 | if (returnData == null && parent != null) {
42 | returnData = parent.getPreReturnData();
43 | }
44 | return returnData;
45 | }
46 |
47 | public void putCurrentData(String key, Object value) {
48 | data.put(key, value);
49 | }
50 |
51 | public T getCurrentData(String key, Class type) {
52 | return convert(data.get(key), type);
53 | }
54 |
55 | public void putData(String key, Object value) {
56 | putCurrentData(key, value);
57 | if (parent != null) {
58 | parent.putData(key, value);
59 | }
60 | }
61 |
62 | public T getData(String key, Class type) {
63 | T result = getCurrentData(key, type);
64 | if (result == null && parent != null) {
65 | result = parent.getData(key, type);
66 | }
67 | return result;
68 | }
69 |
70 |
71 | private T convert(Object data, Class clazz) {
72 | if (data == null || clazz == null) {
73 | return null;
74 | }
75 | // 判断转换的类型是否是number类型
76 | return (T) data;
77 | }
78 |
79 | public Integer next() {
80 | current++;
81 | return current;
82 | }
83 |
84 | public Integer end() {
85 | current = count;
86 | return count;
87 | }
88 |
89 | public Boolean isEnd() {
90 | return current >= count;
91 | }
92 |
93 | public Boolean isStart() {
94 | return current < 0;
95 | }
96 |
97 | public void allEnd() {
98 | Context context = parent;
99 | if (context != null) {
100 | context.allEnd();
101 | }
102 | // 结束自身
103 | end();
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/interceptor/EmptyCaptchaInterceptor.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.interceptor;
2 |
3 | public class EmptyCaptchaInterceptor implements CaptchaInterceptor{
4 |
5 | public static EmptyCaptchaInterceptor INSTANCE = new EmptyCaptchaInterceptor();
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/interceptor/impl/BasicTrackCaptchaInterceptor.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.interceptor.impl;
2 |
3 | import cloud.tianai.captcha.common.AnyMap;
4 | import cloud.tianai.captcha.common.response.ApiResponse;
5 | import cloud.tianai.captcha.common.response.CodeDefinition;
6 | import cloud.tianai.captcha.common.util.CaptchaTypeClassifier;
7 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
8 | import cloud.tianai.captcha.interceptor.Context;
9 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
10 | import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
11 |
12 | import java.util.List;
13 |
14 | /**
15 | * @Author: 天爱有情
16 | * @date 2023/1/4 10:00
17 | * @Description BasicCaptchaTrackValidator
18 | */
19 | public class BasicTrackCaptchaInterceptor implements CaptchaInterceptor {
20 | public static final CodeDefinition DEFINITION = new CodeDefinition(50001, "basic check fail");
21 |
22 | @Override
23 | public String getName() {
24 | return "basic_track_check";
25 | }
26 |
27 | @Override
28 | public ApiResponse> afterValid(Context context, String type, MatchParam matchData, AnyMap validData, ApiResponse> basicValid) {
29 | if (!basicValid.isSuccess()) {
30 | return context.getGroup().afterValid(context, type, matchData, validData, basicValid);
31 | }
32 | if (!CaptchaTypeClassifier.isSliderCaptcha(type)) {
33 | // 不是滑动验证码的话暂时跳过,点选验证码行为轨迹还没做
34 | return ApiResponse.ofSuccess();
35 | }
36 | ImageCaptchaTrack imageCaptchaTrack = matchData.getTrack();
37 | // 进行行为轨迹检测
38 | long startSlidingTime = imageCaptchaTrack.getStartTime().getTime();
39 | long endSlidingTime = imageCaptchaTrack.getStopTime().getTime();
40 | Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth();
41 | List trackList = imageCaptchaTrack.getTrackList();
42 | // 这里只进行基本检测, 用一些简单算法进行校验,如有需要可扩展
43 | // 检测1: 滑动时间如果小于300毫秒 返回false
44 | // 检测2: 轨迹数据要是少于背10,或者大于背景宽度的五倍 返回false
45 | // 检测3: x轴和y轴应该是从0开始的,要是一开始x轴和y轴乱跑,返回false
46 | // 检测4: 如果y轴是相同的,必然是机器操作,直接返回false
47 | // 检测5: x轴或者y轴直接的区间跳跃过大的话返回 false
48 | // 检测6: x轴应该是由快到慢的, 要是速率一致,返回false
49 | // 检测7: 如果x轴超过图片宽度的频率过高,返回false
50 |
51 | // 检测1
52 | if (startSlidingTime + 300 > endSlidingTime) {
53 | context.end();
54 | return ApiResponse.ofMessage(DEFINITION);
55 | }
56 | // 检测2
57 | if (trackList.size() < 10 || trackList.size() > bgImageWidth * 5) {
58 | context.end();
59 | return ApiResponse.ofMessage(DEFINITION);
60 | }
61 | // 检测3
62 | ImageCaptchaTrack.Track firstTrack = trackList.get(0);
63 | if (firstTrack.getX() > 10 || firstTrack.getX() < -10 || firstTrack.getY() > 10 || firstTrack.getY() < -10) {
64 | context.end();
65 | return ApiResponse.ofMessage(DEFINITION);
66 | }
67 | int check4 = 0;
68 | int check7 = 0;
69 | for (int i = 1; i < trackList.size(); i++) {
70 | ImageCaptchaTrack.Track track = trackList.get(i);
71 | float x = track.getX();
72 | float y = track.getY();
73 | // check4
74 | if (firstTrack.getY() == y) {
75 | check4++;
76 | }
77 | // check7
78 | if (x >= bgImageWidth) {
79 | check7++;
80 | }
81 | // check5
82 | ImageCaptchaTrack.Track preTrack = trackList.get(i - 1);
83 | if ((track.getX() - preTrack.getX()) > 50 || (track.getY() - preTrack.getY()) > 50) {
84 | context.end();
85 | return ApiResponse.ofMessage(DEFINITION);
86 | }
87 | }
88 | if (check4 == trackList.size() || check7 > 200) {
89 | context.end();
90 | return ApiResponse.ofMessage(DEFINITION);
91 | }
92 |
93 | // check6
94 | int splitPos = (int) (trackList.size() * 0.7);
95 | ImageCaptchaTrack.Track splitPostTrack = trackList.get(splitPos - 1);
96 | ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1);
97 | // bugfix: wuhaochao
98 | ImageCaptchaTrack.Track stepOneFirstTrack = trackList.get(0);
99 | ImageCaptchaTrack.Track stepOneTwoTrack = trackList.get(splitPos);
100 | float posTime = splitPostTrack.getT() - stepOneFirstTrack.getT();
101 | double startAvgPosTime = posTime / (float) splitPos;
102 | double endAvgPosTime = (lastTrack.getT() - stepOneTwoTrack.getT()) / (float) (trackList.size() - splitPos);
103 | boolean check = endAvgPosTime > startAvgPosTime;
104 | if (check) {
105 | return ApiResponse.ofSuccess();
106 | }
107 | context.end();
108 | return ApiResponse.ofMessage(DEFINITION);
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/interceptor/impl/ParamCheckCaptchaInterceptor.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.interceptor.impl;
2 |
3 | import cloud.tianai.captcha.common.AnyMap;
4 | import cloud.tianai.captcha.common.response.ApiResponse;
5 | import cloud.tianai.captcha.common.util.CollectionUtils;
6 | import cloud.tianai.captcha.common.util.ObjectUtils;
7 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
8 | import cloud.tianai.captcha.interceptor.Context;
9 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
10 | import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
11 |
12 | /**
13 | * @Author: 天爱有情
14 | * @date 2023/1/4 10:10
15 | * @Description 轨迹参数校验, 如果轨迹参数为空抛异常
16 | */
17 | public class ParamCheckCaptchaInterceptor implements CaptchaInterceptor {
18 | @Override
19 | public ApiResponse> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
20 | checkParam(matchParam.getTrack());
21 | return ApiResponse.ofSuccess();
22 | }
23 |
24 | @Override
25 | public String getName() {
26 | return "param_check";
27 | }
28 |
29 | public void checkParam(ImageCaptchaTrack imageCaptchaTrack) {
30 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageWidth())) {
31 | throw new IllegalArgumentException("bgImageWidth must not be null");
32 | }
33 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageHeight())) {
34 | throw new IllegalArgumentException("bgImageHeight must not be null");
35 | }
36 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getStartTime())) {
37 | throw new IllegalArgumentException("startTime must not be null");
38 | }
39 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getStopTime())) {
40 | throw new IllegalArgumentException("stopTime must not be null");
41 | }
42 | if (CollectionUtils.isEmpty(imageCaptchaTrack.getTrackList())) {
43 | throw new IllegalArgumentException("trackList must not be null");
44 | }
45 | for (ImageCaptchaTrack.Track track : imageCaptchaTrack.getTrackList()) {
46 | Float x = track.getX();
47 | Float y = track.getY();
48 | Float t = track.getT();
49 | String type = track.getType();
50 | if (x == null || y == null || t == null || ObjectUtils.isEmpty(type)) {
51 | throw new IllegalArgumentException("track[x,y,t,type] must not be null");
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/AbstractResourceProvider.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 |
5 | import java.io.InputStream;
6 |
7 | /**
8 | * @Author: 天爱有情
9 | * @date 2021/12/16 16:52
10 | * @Description 抽象的ResourceProvider
11 | */
12 | public abstract class AbstractResourceProvider implements ResourceProvider {
13 | @Override
14 | public InputStream getResourceInputStream(Resource data) {
15 | InputStream resourceInputStream = doGetResourceInputStream(data);
16 | if (resourceInputStream == null) {
17 | throw new IllegalArgumentException("无法读到指定的资源[" + getName() + "]" + data);
18 | }
19 | return resourceInputStream;
20 | }
21 |
22 | /**
23 | * 通过 Resource 获取 InputStream
24 | *
25 | * @param data data
26 | * @return InputStream
27 | */
28 | public abstract InputStream doGetResourceInputStream(Resource data);
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/AbstractResourceStore.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
5 | import lombok.Getter;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | public abstract class AbstractResourceStore implements ResourceStore {
11 |
12 | @Getter
13 | protected ChainListener listener = new ChainListener();
14 |
15 | boolean isInit = false;
16 |
17 | @Override
18 | public void init(ImageCaptchaResourceManager resourceManager) {
19 | if (isInit) {
20 | return;
21 | }
22 | doInit();
23 | isInit = true;
24 | listener.onInit(this, resourceManager);
25 | }
26 |
27 | @Override
28 | public void addListener(ResourceListener listener) {
29 | this.listener.removeListener(listener);
30 | this.listener.addListener(listener);
31 | }
32 |
33 | @Override
34 | public void addResource(String type, Resource resource) {
35 | doAddResource(type, resource);
36 | if (isInit) {
37 | listener.onAddResource(type, resource);
38 | }
39 | }
40 |
41 | @Override
42 | public void addTemplate(String type, ResourceMap template) {
43 | doAddTemplate(type, template);
44 | if (isInit) {
45 | listener.onAddTemplate(type, template);
46 | }
47 | }
48 |
49 | @Override
50 | public Resource deleteResource(String type, String id) {
51 | Resource resource = doDeleteResource(type, id);
52 | if (isInit && resource != null) {
53 | listener.onDeleteResource(type, resource);
54 | }
55 | return resource;
56 | }
57 |
58 | @Override
59 | public ResourceMap deleteTemplate(String type, String id) {
60 | ResourceMap resourceMap = doDeleteTemplate(type, id);
61 | if (isInit && resourceMap != null) {
62 | listener.onDeleteTemplate(type, resourceMap);
63 | }
64 | return resourceMap;
65 | }
66 |
67 | @Override
68 | public Resource randomGetResourceByTypeAndTag(String type, String tag) {
69 | Resource resource = doRandomGetResourceByTypeAndTag(type, tag);
70 | if (isInit && resource != null) {
71 | listener.onRandomGetResourceByTypeAndTag(type, tag, resource);
72 | }
73 | return resource;
74 | }
75 |
76 | @Override
77 | public ResourceMap randomGetTemplateByTypeAndTag(String type, String tag) {
78 | ResourceMap resourceMap = doRandomGetTemplateByTypeAndTag(type, tag);
79 | if (isInit && resourceMap != null) {
80 | listener.onRandomGetTemplateByTypeAndTag(type, tag, resourceMap);
81 | }
82 | return resourceMap;
83 | }
84 |
85 |
86 | @Override
87 | public void clearAllResources() {
88 | doClearAllResources();
89 | if (isInit) {
90 | listener.onClearAllResources();
91 | }
92 | }
93 |
94 | @Override
95 | public void clearAllTemplates() {
96 | doClearAllTemplates();
97 | if (isInit) {
98 | listener.onClearAllTemplates();
99 | }
100 | }
101 |
102 | public void doInit() {
103 |
104 | }
105 |
106 | public abstract void doClearAllResources();
107 |
108 | public abstract void doClearAllTemplates();
109 |
110 | public abstract Resource doRandomGetResourceByTypeAndTag(String type, String tag);
111 |
112 | public abstract ResourceMap doRandomGetTemplateByTypeAndTag(String type, String tag);
113 |
114 | public abstract ResourceMap doDeleteTemplate(String type, String id);
115 |
116 | public abstract Resource doDeleteResource(String type, String id);
117 |
118 | public abstract void doAddResource(String type, Resource resource);
119 |
120 | public abstract void doAddTemplate(String type, ResourceMap template);
121 |
122 |
123 | public static class ChainListener implements ResourceListener {
124 | protected List listeners = new ArrayList<>();
125 |
126 | public ChainListener() {
127 |
128 | }
129 |
130 | public void addListener(ResourceListener listener) {
131 | listeners.add(listener);
132 | }
133 |
134 | public void removeListener(ResourceListener listener) {
135 | listeners.remove(listener);
136 | }
137 |
138 | @Override
139 | public void onInit(ResourceStore resourceStore, ImageCaptchaResourceManager resourceManager) {
140 | for (ResourceListener listener : listeners) {
141 | listener.onInit(resourceStore, resourceManager);
142 | }
143 | }
144 |
145 | @Override
146 | public void onAddResource(String type, Resource resource) {
147 | for (ResourceListener listener : listeners) {
148 | listener.onAddResource(type, resource);
149 | }
150 | }
151 |
152 | @Override
153 | public void onAddTemplate(String type, ResourceMap template) {
154 | for (ResourceListener listener : listeners) {
155 | listener.onAddTemplate(type, template);
156 | }
157 | }
158 |
159 | @Override
160 | public void onClearAllResources() {
161 | for (ResourceListener listener : listeners) {
162 | listener.onClearAllResources();
163 | }
164 | }
165 |
166 | @Override
167 | public void onClearAllTemplates() {
168 | for (ResourceListener listener : listeners) {
169 | listener.onClearAllTemplates();
170 | }
171 | }
172 |
173 | @Override
174 | public void onRandomGetResourceByTypeAndTag(String type, String tag, Resource resource) {
175 | for (ResourceListener listener : listeners) {
176 | listener.onRandomGetResourceByTypeAndTag(type, tag, resource);
177 | }
178 | }
179 |
180 | @Override
181 | public void onRandomGetTemplateByTypeAndTag(String type, String tag, ResourceMap template) {
182 | for (ResourceListener listener : listeners) {
183 | listener.onRandomGetTemplateByTypeAndTag(type, tag, template);
184 | }
185 | }
186 |
187 | @Override
188 | public void onDeleteResource(String type, Resource resource) {
189 | for (ResourceListener listener : listeners) {
190 | listener.onDeleteResource(type, resource);
191 | }
192 | }
193 |
194 | @Override
195 | public void onDeleteTemplate(String type, ResourceMap template) {
196 | for (ResourceListener listener : listeners) {
197 | listener.onDeleteTemplate(type, template);
198 | }
199 | }
200 |
201 |
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/DefaultBuiltInResources.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
4 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
5 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import java.util.function.Consumer;
10 |
11 | import static cloud.tianai.captcha.common.constant.CommonConstant.DEFAULT_TAG;
12 | import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.TEMPLATE_ACTIVE_IMAGE_NAME;
13 | import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.TEMPLATE_FIXED_IMAGE_NAME;
14 |
15 |
16 | /**
17 | * @Author: 天爱有情
18 | * @date 2024/7/15 9:10
19 | * @Description 默认资源配置
20 | * 注意: 不推荐使用该类,应该将资源模板自己设置,而不是使用默认的,这里编写的目的只是为了演示方便
21 | */
22 | public class DefaultBuiltInResources {
23 |
24 | public static final String PATH_PREFIX = "classpath:META-INF/cut-image/template";
25 |
26 | private static Map> defaultTemplateResource = new HashMap<>(8);
27 |
28 |
29 | public DefaultBuiltInResources(String defaultPathPrefix) {
30 | init(defaultPathPrefix);
31 | }
32 |
33 | private void init(String defaultPathPrefix) {
34 | String[] split = defaultPathPrefix.split(":");
35 | String type;
36 | String pathPrefix;
37 | if (split.length < 1) {
38 | type = "file";
39 | pathPrefix = defaultPathPrefix;
40 | } else {
41 | type = split[0];
42 | pathPrefix = split[1];
43 | }
44 | if (pathPrefix.endsWith("/")) {
45 | pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
46 | }
47 | // 滑动验证
48 | String finalPathPrefix = pathPrefix;
49 | defaultTemplateResource.put(CaptchaTypeConstant.SLIDER, resourceStore -> {
50 | ResourceMap template1 = new ResourceMap(DEFAULT_TAG, 4);
51 | template1.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_1/active.png")));
52 | template1.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_1/fixed.png")));
53 | resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template1);
54 |
55 | ResourceMap template2 = new ResourceMap(DEFAULT_TAG, 4);
56 | template2.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_2/active.png")));
57 | template2.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/slider_2/fixed.png")));
58 | resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template2);
59 | });
60 |
61 | // 旋转验证
62 | defaultTemplateResource.put(CaptchaTypeConstant.ROTATE, resourceStore -> {
63 | // 添加一些系统的 模板文件
64 | ResourceMap template1 = new ResourceMap(DEFAULT_TAG, 4);
65 | template1.put(TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/rotate_1/active.png")));
66 | template1.put(TEMPLATE_FIXED_IMAGE_NAME, new Resource(type, finalPathPrefix.concat("/rotate_1/fixed.png")));
67 | resourceStore.addTemplate(CaptchaTypeConstant.ROTATE, template1);
68 | });
69 |
70 | // 字体包
71 | defaultTemplateResource.put(FontCache.FONT_TYPE, resourceStore -> {
72 | resourceStore.addResource(FontCache.FONT_TYPE,new Resource(type, finalPathPrefix.concat("/fontS/SIMSUN.TTC")));
73 | });
74 | }
75 |
76 |
77 | public void addDefaultTemplate(String type, ResourceStore resourceStore) {
78 | Consumer resourceStoreConsumer = defaultTemplateResource.get(type);
79 | if (resourceStoreConsumer == null) {
80 | return;
81 | }
82 | resourceStoreConsumer.accept(resourceStore);
83 | }
84 |
85 | public void addDefaultTemplate(ResourceStore resourceStore) {
86 | defaultTemplateResource.forEach((type, consumer) -> {
87 | consumer.accept(resourceStore);
88 | });
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/FontCache.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.generator.common.FontWrapper;
4 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
5 | import lombok.Data;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import lombok.extern.slf4j.Slf4j;
9 |
10 | import java.awt.*;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.util.List;
14 | import java.util.Map;
15 | import java.util.concurrent.ConcurrentHashMap;
16 |
17 | /**
18 | * @Author: 天爱有情
19 | * @date 2024/11/19 11:25
20 | * @Description 一个用于统一缓存字体文件的对象
21 | */
22 | @Slf4j
23 | public class FontCache implements ResourceListener {
24 |
25 |
26 | public static final String FONT_TYPE = "font";
27 | private final Map fontMap = new ConcurrentHashMap<>();
28 |
29 | private ResourceStore resourceStore;
30 | private ImageCaptchaResourceManager resourceManager;
31 | @Setter
32 | @Getter
33 | private int fontSize = 70;
34 | public static FontCache getInstance() {
35 | return INSTANCE.INSTANCE;
36 | }
37 |
38 | public FontCache() {
39 | }
40 |
41 | @Override
42 | public void onInit(ResourceStore resourceStore, ImageCaptchaResourceManager resourceManager) {
43 | this.resourceStore = resourceStore;
44 | this.resourceManager = resourceManager;
45 | }
46 |
47 | public FontWrapper getFont(Resource resource) {
48 | try (InputStream stream = resourceManager.getResourceInputStream(resource)) {
49 | Font font = Font.createFont(0, stream);
50 | return new FontWrapper(font, fontSize);
51 | } catch (FontFormatException | IOException e) {
52 | throw new RuntimeException(e);
53 | }
54 | }
55 |
56 | @Override
57 | public void onAddResource(String type, Resource resource) {
58 | if (FONT_TYPE.equalsIgnoreCase(type)) {
59 | fontMap.computeIfAbsent(resource.getId(), v -> getFont(resource));
60 | }
61 | }
62 |
63 | @Override
64 | public void onDeleteResource(String type, Resource resource) {
65 | if (FONT_TYPE.equalsIgnoreCase(type)) {
66 | fontMap.remove(resource.getId());
67 | }
68 | }
69 |
70 | @Override
71 | public void onClearAllResources() {
72 | fontMap.clear();
73 | }
74 |
75 | @Override
76 | public void onRandomGetResourceByTypeAndTag(String type, String tag, Resource resource) {
77 | if (FONT_TYPE.equalsIgnoreCase(type)) {
78 | FontWrapper fontWrapper = fontMap.computeIfAbsent(resource.getId(), v -> getFont(resource));
79 |
80 | resource.setExtra(fontWrapper);
81 | }
82 | }
83 |
84 | public void loadAllFonts() {
85 | List resources = resourceStore.listResourcesByTypeAndTag(FONT_TYPE, null);
86 | for (Resource resource : resources) {
87 | fontMap.computeIfAbsent(resource.getId(), v -> getFont(resource));
88 | }
89 | }
90 |
91 |
92 | private static class INSTANCE {
93 | private static final FontCache INSTANCE = new FontCache();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/ImageCaptchaResourceManager.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
5 |
6 | import java.io.InputStream;
7 | import java.util.List;
8 |
9 | /**
10 | * @Author: 天爱有情
11 | * @date 2021/8/7 15:26
12 | * @Description 验证码图片资源管理器
13 | */
14 | public interface ImageCaptchaResourceManager {
15 |
16 | /**
17 | * 随机获取某个模板
18 | *
19 | * @param type 验证码类型
20 | * @param tag 二级过滤,可以为空
21 | * @return Map
22 | */
23 | ResourceMap randomGetTemplate(String type, String tag);
24 |
25 | /**
26 | * 随机获取某个资源对象
27 | *
28 | * @param type 验证码类型
29 | * @param tag 二级过滤,可以为空
30 | * @return Resource
31 | */
32 | Resource randomGetResource(String type, String tag);
33 |
34 | /**
35 | * 获取真正的资源流通过资源对象
36 | *
37 | * @param resource resource
38 | * @return InputStream
39 | */
40 | InputStream getResourceInputStream(Resource resource);
41 |
42 | /**
43 | * 获取所有资源提供者
44 | *
45 | * @return List
46 | */
47 | List listResourceProviders();
48 |
49 | /**
50 | * 注册资源提供者
51 | *
52 | * @param resourceProvider 资源提供者
53 | */
54 | void registerResourceProvider(ResourceProvider resourceProvider);
55 |
56 | /**
57 | * 删除资源提供者
58 | *
59 | * @param name 资源提供者名称
60 | * @return ResourceProvider
61 | */
62 | boolean deleteResourceProviderByName(String name);
63 |
64 | /**
65 | * 设置资源存储
66 | *
67 | * @param resourceStore resourceStore
68 | */
69 | void setResourceStore(ResourceStore resourceStore);
70 |
71 | /**
72 | * 获取资源存储
73 | *
74 | * @return ResourceStore
75 | */
76 | ResourceStore getResourceStore();
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/ResourceListener.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
5 |
6 | /**
7 | * @Author: 天爱有情
8 | * @date 2024/11/19 9:26
9 | * @Description 此类负责对 ResourceStore 进行一下扩展增强, 对ResourceStore的相关方法添加一个hook回调
10 | */
11 | public interface ResourceListener {
12 |
13 | default void onInit(ResourceStore resourceStore, ImageCaptchaResourceManager resourceManager) {
14 | }
15 |
16 |
17 | default void onAddResource(String type, Resource resource) {
18 |
19 | }
20 |
21 | default void onAddTemplate(String type, ResourceMap template) {
22 |
23 | }
24 |
25 | default void onDeleteResource(String type, Resource resource) {
26 |
27 | }
28 |
29 | default void onDeleteTemplate(String type, ResourceMap template) {
30 |
31 | }
32 |
33 | default void onClearAllResources() {
34 |
35 | }
36 |
37 | default void onClearAllTemplates() {
38 |
39 | }
40 |
41 | default void onRandomGetResourceByTypeAndTag(String type, String tag, Resource resource) {
42 |
43 | }
44 |
45 | default void onRandomGetTemplateByTypeAndTag(String type, String tag, ResourceMap template) {
46 |
47 | }
48 |
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/ResourceProvider.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 |
5 | import java.io.InputStream;
6 |
7 | /**
8 | * @Author: 天爱有情
9 | * @date 2021/8/7 15:07
10 | * @Description 资源提供者
11 | */
12 | public interface ResourceProvider {
13 |
14 | /**
15 | * 获取资源
16 | *
17 | * @param data data
18 | * @return InputStream
19 | */
20 | InputStream getResourceInputStream(Resource data);
21 |
22 | /**
23 | * 是否支持
24 | *
25 | * @param resource resource
26 | * @return boolean
27 | */
28 | boolean supported(Resource resource);
29 |
30 | /**
31 | * 放弃资源提供者名称
32 | *
33 | * @return String
34 | */
35 | String getName();
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/ResourceProviders.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 | import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
5 | import cloud.tianai.captcha.resource.impl.provider.FileResourceProvider;
6 | import cloud.tianai.captcha.resource.impl.provider.URLResourceProvider;
7 |
8 | import java.io.InputStream;
9 | import java.util.ArrayList;
10 | import java.util.Collections;
11 | import java.util.List;
12 |
13 | public class ResourceProviders {
14 |
15 | private final List resourceProviderList = new ArrayList<>(8);
16 |
17 |
18 | public ResourceProviders() {
19 | registerResourceProvider(new URLResourceProvider());
20 | registerResourceProvider(new ClassPathResourceProvider());
21 | registerResourceProvider(new FileResourceProvider());
22 | }
23 |
24 | public void registerResourceProvider(ResourceProvider resourceProvider) {
25 | deleteResourceProviderByName(resourceProvider.getName());
26 | resourceProviderList.add(resourceProvider);
27 | }
28 |
29 | public boolean deleteResourceProviderByName(String name) {
30 | return resourceProviderList.removeIf(r -> r.getName().equals(name));
31 | }
32 |
33 | public List listResourceProviders() {
34 | return Collections.unmodifiableList(resourceProviderList);
35 | }
36 |
37 |
38 | public InputStream getResourceInputStream(Resource resource) {
39 | for (ResourceProvider resourceProvider : resourceProviderList) {
40 | if (resourceProvider.supported(resource)) {
41 | InputStream resourceInputStream = resourceProvider.getResourceInputStream(resource);
42 | if (resourceInputStream == null) {
43 | throw new IllegalArgumentException("滑块验证码 ResourceProvider 读到的图片资源为空,providerName=["
44 | + resourceProvider.getName() + "], resource=[" + resource + "]");
45 | }
46 | return resourceInputStream;
47 | }
48 | }
49 | throw new IllegalStateException("没有找到Resource [" + resource.getType() + "]对应的资源提供者");
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/ResourceStore.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource;
2 |
3 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
4 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
5 |
6 | import java.util.List;
7 |
8 | /**
9 | * @Author: 天爱有情
10 | * @date 2022/5/7 9:04
11 | * @Description 资源存储
12 | */
13 | public interface ResourceStore {
14 |
15 | void init(ImageCaptchaResourceManager resourceManager);
16 | /**
17 | * 给ResourceStore添加hook,用于一些扩展
18 | *
19 | * @param hook
20 | */
21 | void addListener(ResourceListener hook);
22 |
23 | /**
24 | * 添加资源
25 | *
26 | * @param type 验证码类型
27 | * @param resource 资源
28 | */
29 | void addResource(String type, Resource resource);
30 |
31 |
32 | /**
33 | * 添加模板
34 | *
35 | * @param type 验证码类型
36 | * @param template 模板
37 | */
38 | void addTemplate(String type, ResourceMap template);
39 |
40 | /**
41 | * 删除资源
42 | *
43 | * @param type 验证码类型
44 | * @param id 资源ID
45 | * @return Resource
46 | */
47 | Resource deleteResource(String type, String id);
48 |
49 | /**
50 | * 删除模板
51 | *
52 | * @param type 验证码类型
53 | * @param id 资源ID
54 | * @return ResourceMap
55 | */
56 | ResourceMap deleteTemplate(String type, String id);
57 |
58 | /**
59 | * 获取某个资源列表
60 | *
61 | * @param type 验证码类型
62 | * @param tag 资源标签(可为空)
63 | * @return List
64 | */
65 | List listResourcesByTypeAndTag(String type, String tag);
66 |
67 | /**
68 | * 获取某个模板列表
69 | *
70 | * @param type 验证码类型
71 | * @param tag 资源标签(可为空)
72 | * @return List
73 | */
74 | List listTemplatesByTypeAndTag(String type, String tag);
75 |
76 | /**
77 | * 随机获取某个资源
78 | *
79 | * @param type type
80 | * @return Resource
81 | */
82 | Resource randomGetResourceByTypeAndTag(String type, String tag);
83 |
84 | /**
85 | * 随机获取某个模板通过type
86 | *
87 | * @param type type
88 | * @return Map
89 | */
90 | ResourceMap randomGetTemplateByTypeAndTag(String type, String tag);
91 |
92 | /**
93 | * 清除所有内置模板
94 | */
95 | void clearAllTemplates();
96 |
97 | /**
98 | * 清除所有内置资源
99 | */
100 | void clearAllResources();
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/common/model/dto/Resource.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource.common.model.dto;
2 |
3 | import cloud.tianai.captcha.common.util.UUIDUtils;
4 | import cloud.tianai.captcha.resource.ResourceProvider;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | /**
9 | * @Author: 天爱有情
10 | * @date 2021/8/7 15:15
11 | * @Description 资源对象
12 | */
13 | @Data
14 | @NoArgsConstructor
15 | public class Resource {
16 | /** 唯一ID. */
17 | private String id;
18 | /** 类型. */
19 | private String type;
20 | /** 数据,传输给 {@link ResourceProvider} 的参数 */
21 | public String data;
22 | /** 标签. */
23 | private String tag;
24 | /** 提示. */
25 | private String tip;
26 | /** 扩展. */
27 | private Object extra;
28 |
29 | public Resource(String type, String data) {
30 | this(type, data, null);
31 | }
32 |
33 | public Resource(String type, String data, String tag) {
34 | this(type, data, tag, null);
35 | }
36 |
37 | public Resource(String type, String data, String tag, String tip) {
38 | this(UUIDUtils.getUUID(), type, data, tag, tip);
39 | }
40 |
41 | public Resource(String id, String type, String data, String tag, String tip) {
42 | this.id = id;
43 | this.type = type;
44 | this.data = data;
45 | this.tag = tag;
46 | this.tip = tip;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/common/model/dto/ResourceMap.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource.common.model.dto;
2 |
3 | import cloud.tianai.captcha.common.util.UUIDUtils;
4 | import lombok.Data;
5 | import lombok.EqualsAndHashCode;
6 |
7 | import java.util.Collection;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 | import java.util.function.BiConsumer;
11 |
12 | /**
13 | * @Author: 天爱有情
14 | * @date 2022/12/30 9:23
15 | * @Description 存储一组Resource的Map, 增加tag标记
16 | */
17 | @Data
18 | @EqualsAndHashCode
19 | public class ResourceMap {
20 | /** 唯一ID. */
21 | private String id;
22 | private Map resourceMap;
23 | private String tag;
24 |
25 | public ResourceMap(String tag) {
26 | this(tag, 10);
27 | }
28 |
29 | public ResourceMap(String tag, int initialCapacity) {
30 | this(UUIDUtils.getUUID(), tag, initialCapacity);
31 | }
32 |
33 | public ResourceMap(String id, String tag, int initialCapacity) {
34 | this.tag = tag;
35 | this.resourceMap = new HashMap<>(initialCapacity);
36 | this.id = id;
37 | }
38 |
39 | public ResourceMap(int initialCapacity) {
40 | this(null, initialCapacity);
41 | }
42 |
43 | public ResourceMap() {
44 | this(null);
45 | }
46 |
47 | private Map getResourceMapOfCreate() {
48 | if (resourceMap == null) {
49 | resourceMap = new HashMap<>(2);
50 | }
51 | return resourceMap;
52 | }
53 |
54 | // ================== Map ==================
55 |
56 | public Resource put(String key, Resource value) {
57 | return getResourceMapOfCreate().put(key, value);
58 | }
59 |
60 | public Resource get(Object key) {
61 | return getResourceMapOfCreate().get(key);
62 | }
63 |
64 | public Resource remove(Object key) {
65 | return getResourceMapOfCreate().remove(key);
66 | }
67 |
68 | public Collection values() {
69 | return getResourceMapOfCreate().values();
70 | }
71 |
72 | public void forEach(BiConsumer action) {
73 | getResourceMapOfCreate().forEach(action);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/impl/DefaultImageCaptchaResourceManager.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource.impl;
2 |
3 | import cloud.tianai.captcha.resource.*;
4 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
5 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
6 | import lombok.Getter;
7 |
8 | import java.io.InputStream;
9 | import java.util.List;
10 |
11 | /**
12 | * @Author: 天爱有情
13 | * @date 2021/8/7 15:35
14 | * @Description 默认的滑块验证码资源管理
15 | */
16 | public class DefaultImageCaptchaResourceManager implements ImageCaptchaResourceManager {
17 |
18 | /** 资源存储. */
19 | private ResourceStore resourceStore;
20 | /** 资源转换 转换为stream流. */
21 | @Getter
22 | private ResourceProviders resourceProviders;
23 |
24 | public DefaultImageCaptchaResourceManager() {
25 | init();
26 | }
27 |
28 | public DefaultImageCaptchaResourceManager(ResourceStore resourceStore, ResourceProviders resourceProviders) {
29 | this.resourceStore = resourceStore;
30 | this.resourceProviders = resourceProviders;
31 | init();
32 | }
33 |
34 | private void init() {
35 | if (this.resourceStore == null) {
36 | this.resourceStore = new LocalMemoryResourceStore();
37 | }
38 | // 在这里临时加上字体缓存器
39 | resourceStore.addListener(FontCache.getInstance());
40 | resourceStore.init(this);
41 | }
42 |
43 | @Override
44 | public ResourceMap randomGetTemplate(String type, String tag) {
45 | ResourceMap resourceMap = resourceStore.randomGetTemplateByTypeAndTag(type, tag);
46 | if (resourceMap == null) {
47 | throw new IllegalStateException("随机获取模板错误,store中模板为空, type:" + type);
48 | }
49 | return resourceMap;
50 | }
51 |
52 | @Override
53 | public Resource randomGetResource(String type, String tag) {
54 | Resource resource = resourceStore.randomGetResourceByTypeAndTag(type, tag);
55 | if (resource == null) {
56 | throw new IllegalStateException("随机获取资源错误,store中资源为空, type:" + type);
57 | }
58 | return resource;
59 | }
60 |
61 | @Override
62 | public InputStream getResourceInputStream(Resource resource) {
63 | return resourceProviders.getResourceInputStream(resource);
64 | }
65 |
66 | @Override
67 | public List listResourceProviders() {
68 | return resourceProviders.listResourceProviders();
69 | }
70 |
71 | @Override
72 | public void registerResourceProvider(ResourceProvider resourceProvider) {
73 | resourceProviders.registerResourceProvider(resourceProvider);
74 | }
75 |
76 | @Override
77 | public boolean deleteResourceProviderByName(String name) {
78 | return resourceProviders.deleteResourceProviderByName(name);
79 | }
80 |
81 | @Override
82 | public void setResourceStore(ResourceStore resourceStore) {
83 | this.resourceStore = resourceStore;
84 | }
85 |
86 | @Override
87 | public ResourceStore getResourceStore() {
88 | return resourceStore;
89 | }
90 |
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/impl/provider/ClassPathResourceProvider.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource.impl.provider;
2 |
3 | import cloud.tianai.captcha.resource.AbstractResourceProvider;
4 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
5 |
6 | import java.io.InputStream;
7 |
8 | /**
9 | * @Author: 天爱有情
10 | * @date 2021/8/7 16:07
11 | * @Description classPath
12 | */
13 | public class ClassPathResourceProvider extends AbstractResourceProvider {
14 |
15 | public static final String NAME = "classpath";
16 |
17 | @Override
18 | public InputStream doGetResourceInputStream(Resource data) {
19 | return getClassLoader().getResourceAsStream(data.getData());
20 | }
21 |
22 | @Override
23 | public boolean supported(Resource resource) {
24 | return NAME.equalsIgnoreCase(resource.getType());
25 | }
26 |
27 | @Override
28 | public String getName() {
29 | return NAME;
30 | }
31 |
32 | private static ClassLoader getClassLoader() {
33 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
34 | if (classLoader == null) {
35 | classLoader = ClassPathResourceProvider.getClassLoader();
36 | }
37 | if (classLoader == null) {
38 | classLoader = ClassLoader.getSystemClassLoader();
39 | }
40 | return classLoader;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/impl/provider/FileResourceProvider.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource.impl.provider;
2 |
3 | import cloud.tianai.captcha.resource.AbstractResourceProvider;
4 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
5 | import lombok.SneakyThrows;
6 |
7 | import java.io.FileInputStream;
8 | import java.io.InputStream;
9 |
10 | /**
11 | * @Author: 天爱有情
12 | * @date 2022/2/21 14:43
13 | * @Description file
14 | */
15 | public class FileResourceProvider extends AbstractResourceProvider {
16 |
17 | public static final String NAME = "file";
18 |
19 | @SneakyThrows
20 | @Override
21 | public InputStream doGetResourceInputStream(Resource data) {
22 | FileInputStream fileInputStream = new FileInputStream(data.getData());
23 | return fileInputStream;
24 | }
25 |
26 | @Override
27 | public boolean supported(Resource resource) {
28 | return NAME.equalsIgnoreCase(resource.getType());
29 | }
30 |
31 | @Override
32 | public String getName() {
33 | return NAME;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/resource/impl/provider/URLResourceProvider.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.resource.impl.provider;
2 |
3 | import cloud.tianai.captcha.resource.AbstractResourceProvider;
4 | import cloud.tianai.captcha.resource.common.model.dto.Resource;
5 | import lombok.SneakyThrows;
6 |
7 | import java.io.InputStream;
8 | import java.net.URL;
9 |
10 | /**
11 | * @Author: 天爱有情
12 | * @date 2021/8/7 16:05
13 | * @Description url
14 | */
15 | public class URLResourceProvider extends AbstractResourceProvider {
16 |
17 | public static final String NAME = "URL";
18 |
19 | @SneakyThrows
20 | @Override
21 | public InputStream doGetResourceInputStream(Resource data) {
22 | URL url = new URL(data.getData());
23 | return url.openStream();
24 | }
25 |
26 | @Override
27 | public boolean supported(Resource resource) {
28 | return NAME.equalsIgnoreCase(resource.getType());
29 | }
30 |
31 | @Override
32 | public String getName() {
33 | return NAME;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/validator/ImageCaptchaValidator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.validator;
2 |
3 | import cloud.tianai.captcha.common.AnyMap;
4 | import cloud.tianai.captcha.common.response.ApiResponse;
5 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
6 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
7 |
8 | /**
9 | * @Author: 天爱有情
10 | * @date 2022/2/17 10:54
11 | * @Description 图片验证码校验器
12 | */
13 | public interface ImageCaptchaValidator {
14 |
15 | /**
16 | * 用于生成验证码校验时需要的回传参数
17 | *
18 | * @param imageCaptchaInfo 生成的验证码数据
19 | * @return AnyMap
20 | */
21 | AnyMap generateImageCaptchaValidData(ImageCaptchaInfo imageCaptchaInfo);
22 |
23 | /**
24 | * 校验用户滑动滑块是否正确
25 | *
26 | * @param imageCaptchaTrack 包含了滑动轨迹,展示的图片宽高,滑动时间等参数
27 | * @param imageCaptchaValidData generateImageCaptchaValidData(生成的数据)
28 | * @return ApiResponse>
29 | */
30 | ApiResponse> valid(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData);
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/validator/SliderCaptchaPercentageValidator.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.validator;
2 |
3 | /**
4 | * @Author: 天爱有情
5 | * @date 2023/1/19 10:40
6 | * @Description 滑动类验证码百分比校验
7 | */
8 | public interface SliderCaptchaPercentageValidator {
9 |
10 | /**
11 | * 计算滑块要背景图的百分比,基本校验
12 | * 用于计算滑动类验证码的缺口位置
13 | *
14 | * @param pos 移动的位置
15 | * @param maxPos 最大可移动的位置
16 | * @return float
17 | */
18 | float calcPercentage(Number pos, Number maxPos);
19 |
20 | /**
21 | * 校验滑块百分比
22 | * 用于校验滑动类验证码是否滑动到缺口
23 | *
24 | * @param newPercentage 用户滑动的百分比
25 | * @param oriPercentage 正确的滑块百分比
26 | * @return boolean
27 | */
28 | boolean checkPercentage(Float newPercentage, Float oriPercentage);
29 |
30 | /**
31 | * 校验滑块百分比
32 | * 用于校验滑动类验证码是否滑动到缺口
33 | *
34 | * @param newPercentage 用户滑动的百分比
35 | * @param oriPercentage 正确的滑块百分比
36 | * @param tolerant 容错值
37 | * @return boolean
38 | */
39 | boolean checkPercentage(Float newPercentage, Float oriPercentage, float tolerant);
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/validator/common/constant/TrackTypeConstant.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.validator.common.constant;
2 |
3 | /**
4 | * @Author: 天爱有情
5 | * @date 2022/4/29 8:33
6 | * @Description 滑动轨迹类型
7 | */
8 | public interface TrackTypeConstant {
9 |
10 | /** 抬起.*/
11 | String UP = "UP";
12 | /** 按下.*/
13 | String DOWN = "DOWN";
14 | /** 移动.*/
15 | String MOVE = "MOVE";
16 | /** 点击.*/
17 | String CLICK = "CLICK";
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/validator/common/model/dto/Drives.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.validator.common.model.dto;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class Drives {
7 | private Integer hardwareConcurrency;
8 | private Boolean hasXhr = false;
9 | private String href;
10 | private String language;
11 | private Long start;
12 | private Long now;
13 | private String platform;
14 | private Integer scripts;
15 | private String userAgent;
16 | private Integer windowHeight;
17 | private Integer windowWidth;
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/cloud/tianai/captcha/validator/common/model/dto/ImageCaptchaTrack.java:
--------------------------------------------------------------------------------
1 | package cloud.tianai.captcha.validator.common.model.dto;
2 |
3 | import cloud.tianai.captcha.validator.common.constant.TrackTypeConstant;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | import java.util.Date;
9 | import java.util.List;
10 |
11 | /**
12 | * @Author: 天爱有情
13 | * @date 2022/2/17 9:23
14 | * @Description 图片验证码滑动轨迹
15 | */
16 | @Data
17 | public class ImageCaptchaTrack {
18 |
19 | /** 背景图片宽度. */
20 | private Integer bgImageWidth;
21 | /** 背景图片高度. */
22 | private Integer bgImageHeight;
23 | /** 模板图片宽度. */
24 | private Integer templateImageWidth;
25 | /** 模板图片高度. */
26 | private Integer templateImageHeight;
27 | /** 滑动开始时间. */
28 | private Date startTime;
29 | /** 滑动结束时间. */
30 | private Date stopTime;
31 | private Integer left;
32 | private Integer top;
33 | /** 滑动的轨迹. */
34 | private List