├── .gitignore ├── LICENSE ├── pom.xml ├── readme.md └── src └── main ├── java └── cloud │ └── tianai │ └── captcha │ ├── application │ ├── CaptchaImageType.java │ ├── DefaultImageCaptchaApplication.java │ ├── FilterImageCaptchaApplication.java │ ├── ImageCaptchaApplication.java │ ├── ImageCaptchaProperties.java │ ├── TACBuilder.java │ └── vo │ │ ├── CaptchaResponse.java │ │ └── ImageCaptchaVO.java │ ├── cache │ ├── CacheStore.java │ └── impl │ │ ├── ConCurrentExpiringMap.java │ │ ├── ExpiringMap.java │ │ └── LocalCacheStore.java │ ├── common │ ├── AnyMap.java │ ├── constant │ │ ├── CaptchaTypeConstant.java │ │ └── CommonConstant.java │ ├── exception │ │ └── ImageCaptchaException.java │ ├── response │ │ ├── ApiResponse.java │ │ ├── ApiResponseStatusConstant.java │ │ └── CodeDefinition.java │ └── util │ │ ├── CaptchaTypeClassifier.java │ │ ├── CollectionUtils.java │ │ ├── FontUtils.java │ │ ├── NamedThreadFactory.java │ │ ├── ObjectUtils.java │ │ └── UUIDUtils.java │ ├── generator │ ├── AbstractImageCaptchaGenerator.java │ ├── ImageCaptchaGenerator.java │ ├── ImageCaptchaGeneratorProvider.java │ ├── ImageTransform.java │ ├── common │ │ ├── FontWrapper.java │ │ ├── model │ │ │ └── dto │ │ │ │ ├── CaptchaExchange.java │ │ │ │ ├── ClickImageCheckDefinition.java │ │ │ │ ├── CustomData.java │ │ │ │ ├── GenerateParam.java │ │ │ │ ├── ImageCaptchaInfo.java │ │ │ │ ├── ImageTransformData.java │ │ │ │ ├── ParamKey.java │ │ │ │ ├── ParamKeyEnum.java │ │ │ │ ├── RotateImageCaptchaInfo.java │ │ │ │ └── SliderImageCaptchaInfo.java │ │ └── util │ │ │ ├── CaptchaImageUtils.java │ │ │ └── ImgWriter.java │ └── impl │ │ ├── AbstractClickImageCaptchaGenerator.java │ │ ├── CacheImageCaptchaGenerator.java │ │ ├── MultiImageCaptchaGenerator.java │ │ ├── StandardConcatImageCaptchaGenerator.java │ │ ├── StandardRotateImageCaptchaGenerator.java │ │ ├── StandardSliderImageCaptchaGenerator.java │ │ ├── StandardWordClickImageCaptchaGenerator.java │ │ ├── provider │ │ └── CommonImageCaptchaGeneratorProvider.java │ │ └── transform │ │ └── Base64ImageTransform.java │ ├── interceptor │ ├── CaptchaInterceptor.java │ ├── CaptchaInterceptorGroup.java │ ├── Context.java │ ├── EmptyCaptchaInterceptor.java │ └── impl │ │ ├── BasicTrackCaptchaInterceptor.java │ │ └── ParamCheckCaptchaInterceptor.java │ ├── resource │ ├── AbstractResourceProvider.java │ ├── AbstractResourceStore.java │ ├── DefaultBuiltInResources.java │ ├── FontCache.java │ ├── ImageCaptchaResourceManager.java │ ├── ResourceListener.java │ ├── ResourceProvider.java │ ├── ResourceProviders.java │ ├── ResourceStore.java │ ├── common │ │ └── model │ │ │ └── dto │ │ │ ├── Resource.java │ │ │ └── ResourceMap.java │ └── impl │ │ ├── DefaultImageCaptchaResourceManager.java │ │ ├── LocalMemoryResourceStore.java │ │ └── provider │ │ ├── ClassPathResourceProvider.java │ │ ├── FileResourceProvider.java │ │ └── URLResourceProvider.java │ └── validator │ ├── ImageCaptchaValidator.java │ ├── SliderCaptchaPercentageValidator.java │ ├── common │ ├── constant │ │ └── TrackTypeConstant.java │ └── model │ │ └── dto │ │ ├── Drives.java │ │ ├── ImageCaptchaTrack.java │ │ └── MatchParam.java │ └── impl │ ├── BasicCaptchaTrackValidator.java │ └── SimpleImageCaptchaValidator.java ├── resources └── META-INF │ └── cut-image │ ├── resource │ └── 1.jpg │ └── template │ ├── fonts │ └── SIMSUN.TTC │ ├── rotate_1 │ ├── active.png │ └── fixed.png │ ├── slider_1 │ ├── active.png │ └── fixed.png │ └── slider_2 │ ├── active.png │ └── fixed.png └── test └── java └── example └── readme ├── ApplicationTest.java ├── SimpleDemo.java ├── TACBuilderTest.java ├── TACBuilderTest2.java ├── Test.java ├── Test2.java ├── Test3.java ├── Test4.java ├── Test6.java ├── Test7.java ├── Test8.java └── TestImageCaptcha.java /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | .mvn 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | 27 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | cloud.tianai.captcha 6 | tianai-captcha 7 | 1.5.2 8 | 9 | tianai-captcha 10 | 行为验证码 11 | https://gitee.com/tianai/tianai-captcha 12 | 13 | 14 | 1.8 15 | 16 | true 17 | UTF-8 18 | UTF-8 19 | 20 | false 21 | ossrh 22 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 23 | https://oss.sonatype.org/content/repositories/snapshots/ 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | The MulanPSL2 License, Version 2.0 35 | http://license.coscl.org.cn/MulanPSL2 36 | 37 | 38 | 39 | 40 | 41 | tianaiyouqing 42 | tianaiyouqing@163.com 43 | tianaiyouqing 44 | http://tianai.cloud 45 | 46 | 47 | 48 | https://gitee.com/tianai/tianai-captcha 49 | 50 | 51 | 52 | ${deplay.id} 53 | ${deplay.snapshotRepository} 54 | 55 | 56 | ${deplay.id} 57 | ${deplay.repository} 58 | 59 | 60 | 61 | 62 | 63 | org.projectlombok 64 | lombok 65 | 1.18.12 66 | true 67 | 68 | 69 | org.slf4j 70 | slf4j-api 71 | 1.7.30 72 | 73 | 74 | 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-compiler-plugin 80 | 81 | 8 82 | 8 83 | -parameters 84 | 85 | 86 | 87 | org.sonatype.plugins 88 | nexus-staging-maven-plugin 89 | 1.6.7 90 | true 91 | 92 | ${skip.nexus} 93 | ossrh 94 | https://oss.sonatype.org/ 95 | true 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-source-plugin 101 | 2.2.1 102 | 103 | 104 | attach-sources 105 | 106 | jar-no-fork 107 | 108 | 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-javadoc-plugin 114 | 2.9.1 115 | 116 | 117 | attach-javadocs 118 | 119 | jar 120 | 121 | 122 | -Xdoclint:none 123 | 124 | 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-gpg-plugin 130 | 1.5 131 | 132 | 133 | sign-artifacts 134 | verify 135 | 136 | sign 137 | 138 | 139 | 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-resources-plugin 144 | 2.6 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![][image-logo] 4 | 5 | ### tianaiCAPTCHA - 天爱验证码(TAC) 6 | #### 基于 JAVA实现的行为验证码 7 | ### **[在线体验 🚀][online-demo-link]** 8 | ### **[在线文档 🚀][doc-link]** 9 |
10 | 11 | 12 | ![](https://minio.tianai.cloud/public/%E6%A0%87%E9%A2%98%E5%9B%BE%E7%89%87.jpg) 13 | 14 | ## 简单介绍 15 | 16 | - tianai-captcha 目前支持的行为验证码类型 17 | - 滑块验证码 18 | - 旋转验证码 19 | - 滑动还原验证码 20 | - 文字点选验证码 21 | - 后面会陆续支持市面上更多好玩的验证码玩法... 敬请期待 22 | 23 | ## 快速上手 24 | 25 | > 注意: 如果你项目是使用的**Springboot**, 26 | > 27 | > 28 | 请使用SpringBoot脚手架工具[tianai-captcha-springboot-starter](https://gitee.com/tianai/tianai-captcha-springboot-starter); 29 | > 30 | > 该工具对tianai-captcha验证码进行了封装,使其使用更加方便快捷 31 | 32 | 33 | > **写好的验证码demo移步 [tianai-captcha-demo](https://gitee.com/tianai/tianai-captcha-demo)** 34 | 35 | ### 1. 导入xml 36 | 37 | ```xml 38 | 39 | 40 | cloud.tianai.captcha 41 | tianai-captcha 42 | 1.5.2 43 | 44 | ``` 45 | 46 | ### 2. 构建 `ImageCaptchaApplication`负责生成和校验验证码 47 | 48 | ```java 49 | import cloud.tianai.captcha.validator.common.model.dto.MatchParam; 50 | 51 | public class ApplicationTest { 52 | 53 | public static void main(String[] args) { 54 | ImageCaptchaApplication application = TACBuilder.builder() 55 | .addDefaultTemplate() // 添加默认模板 56 | // 给滑块验证码 添加背景图片,宽高为600*360, Resource 参数1为 classpath/file/url , 参数2 为具体url 57 | .addResource("SLIDER", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) // 滑块验证的背景图 58 | .addResource("WORD_IMAGE_CLICK", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) // 文字点选的背景图 59 | .addResource("ROTATE", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) // 旋转验证的背景图 60 | .build(); 61 | // 生成验证码数据, 可以将该数据直接返回给前端 , 可配合 tianai-captcha-web-sdk 使用 62 | // 支持生成 滑动验证码(SLIDER)、旋转验证码(ROTATE)、滑动还原验证码(CONCAT)、文字点选验证码(WORD_IMAGE_CLICK) 63 | CaptchaResponse res = application.generateCaptcha("SLIDER"); 64 | System.out.println(res); 65 | 66 | // 校验验证码, ImageCaptchaTrack 和 id 均为前端传开的参数, 可将 valid数据直接返回给 前端 67 | // 注意: 该项目只负责生成和校验验证码数据, 至于二次验证等需要自行扩展 68 | String id = res.getId(); 69 | ImageCaptchaTrack imageCaptchaTrack = null; 70 | ApiResponse valid = application.matching(id, new MatchParam(imageCaptchaTrack)); 71 | System.out.println(valid.isSuccess()); 72 | 73 | 74 | // 扩展: 一个简单的二次验证 75 | CacheStore cacheStore = new LocalCacheStore(); 76 | if (valid.isSuccess()) { 77 | // 如果验证成功,生成一个token并存储, 将该token返回给客户端,客户端下次请求数据时携带该token, 后台判断是否有效 78 | String token = UUID.randomUUID().toString(); 79 | cacheStore.setCache(token, new AnyMap(), 5L, TimeUnit.MINUTES); 80 | } 81 | 82 | } 83 | } 84 | 85 | ``` 86 | ### 3.详细文档请点击 [在线文档](http://doc.captcha.tianai.cloud) 87 | # qq群: 197340494 88 | 89 | # 微信群: 90 | ![](https://minio.tianai.cloud/public/qun2.jpg) 91 | 92 | 93 | ## 微信群加不上的话 加微信好友 微信号: youseeseeyou-1ttd 拉你入群 94 | 95 | 96 | 97 | [image-logo]: https://minio.tianai.cloud/public/captcha/logo/logo-519x100.png 98 | [github-release-shield]: https://img.shields.io/github/v/release/tianaiyouqing/tianai-captcha-go?color=369eff&labelColor=black&logo=github&style=flat-square 99 | [github-release-link]: https://github.com/tianaiyouqing/tianai-captcha-go/releases 100 | [github-license-link]: https://github.com/tianaiyouqing/tianai-captcha-go/blob/master/LICENSE 101 | [github-license-shield]: https://img.shields.io/badge/MulanPSL-2.0-white?labelColor=black&style=flat-square 102 | [tianai-captcha-java-link]: https://github.com/dromara/tianai-captcha 103 | [captcha-go-demo-link]: https://gitee.com/tianai/captcha-go-demo 104 | [tianai-captcha-web-sdk-link]: https://github.com/tianaiyouqing/captcha-web-sdk 105 | [online-demo-link]: http://captcha.tianai.cloud 106 | [doc-link]: http://doc.captcha.tianai.cloud 107 | [qrcode-link]: https://minio.tianai.cloud/public/qun4.png 108 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/application/CaptchaImageType.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.application; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @Author: 天爱有情 7 | * @date 2022/2/24 16:01 8 | * @Description 验证码图片类型 9 | */ 10 | @Getter 11 | public enum CaptchaImageType { 12 | 13 | /** webp类型. */ 14 | WEBP, 15 | /** jpg+png类型. */ 16 | JPEG_PNG; 17 | 18 | public static CaptchaImageType getType(String bgImageType, String sliderImageType) { 19 | if ("webp".equalsIgnoreCase(bgImageType) && "webp".equalsIgnoreCase(sliderImageType)) { 20 | return WEBP; 21 | } 22 | if (("jpeg".equalsIgnoreCase(bgImageType) || "jpg".equalsIgnoreCase(bgImageType)) && "png".equalsIgnoreCase(sliderImageType)) { 23 | return JPEG_PNG; 24 | } 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/application/FilterImageCaptchaApplication.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.application; 2 | 3 | import cloud.tianai.captcha.application.vo.CaptchaResponse; 4 | import cloud.tianai.captcha.application.vo.ImageCaptchaVO; 5 | import cloud.tianai.captcha.cache.CacheStore; 6 | import cloud.tianai.captcha.common.response.ApiResponse; 7 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 8 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam; 9 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor; 10 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 11 | import cloud.tianai.captcha.validator.ImageCaptchaValidator; 12 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; 13 | import cloud.tianai.captcha.validator.common.model.dto.MatchParam; 14 | 15 | /** 16 | * @Author: 天爱有情 17 | * @date 2022/3/2 14:22 18 | * @Description 用于SliderCaptchaApplication增加附属功能 19 | */ 20 | public class FilterImageCaptchaApplication implements ImageCaptchaApplication { 21 | 22 | 23 | protected ImageCaptchaApplication target; 24 | 25 | public FilterImageCaptchaApplication(ImageCaptchaApplication target) { 26 | this.target = target; 27 | } 28 | 29 | @Override 30 | public CaptchaResponse generateCaptcha() { 31 | return target.generateCaptcha(); 32 | } 33 | 34 | @Override 35 | public CaptchaResponse generateCaptcha(String type) { 36 | return target.generateCaptcha(type); 37 | } 38 | 39 | @Override 40 | public CaptchaResponse generateCaptcha(CaptchaImageType captchaImageType) { 41 | return target.generateCaptcha(captchaImageType); 42 | } 43 | 44 | @Override 45 | public CaptchaResponse generateCaptcha(String type, CaptchaImageType captchaImageType) { 46 | return target.generateCaptcha(type, captchaImageType); 47 | } 48 | 49 | @Override 50 | public CaptchaResponse generateCaptcha(GenerateParam param) { 51 | return target.generateCaptcha(param); 52 | } 53 | 54 | @Override 55 | public ApiResponse matching(String id, MatchParam matchParam) { 56 | return target.matching(id, matchParam); 57 | } 58 | 59 | @Override 60 | public ApiResponse matching(String id, ImageCaptchaTrack track) { 61 | return target.matching(id, track); 62 | } 63 | 64 | @Override 65 | public boolean matching(String id, Float percentage) { 66 | return target.matching(id, percentage); 67 | } 68 | 69 | @Override 70 | public String getCaptchaTypeById(String id) { 71 | return target.getCaptchaTypeById(id); 72 | } 73 | 74 | @Override 75 | public ImageCaptchaResourceManager getImageCaptchaResourceManager() { 76 | return target.getImageCaptchaResourceManager(); 77 | } 78 | 79 | @Override 80 | public void setImageCaptchaValidator(ImageCaptchaValidator sliderCaptchaValidator) { 81 | target.setImageCaptchaValidator(sliderCaptchaValidator); 82 | } 83 | 84 | @Override 85 | public void setImageCaptchaGenerator(ImageCaptchaGenerator imageCaptchaGenerator) { 86 | target.setImageCaptchaGenerator(imageCaptchaGenerator); 87 | } 88 | 89 | @Override 90 | public CaptchaInterceptor getCaptchaInterceptor() { 91 | return target.getCaptchaInterceptor(); 92 | } 93 | 94 | @Override 95 | public void setCaptchaInterceptor(CaptchaInterceptor captchaInterceptor) { 96 | target.setCaptchaInterceptor(captchaInterceptor); 97 | } 98 | 99 | @Override 100 | public void setCacheStore(CacheStore cacheStore) { 101 | target.setCacheStore(cacheStore); 102 | } 103 | 104 | @Override 105 | public ImageCaptchaValidator getImageCaptchaValidator() { 106 | return target.getImageCaptchaValidator(); 107 | } 108 | 109 | @Override 110 | public ImageCaptchaGenerator getImageCaptchaGenerator() { 111 | return target.getImageCaptchaGenerator(); 112 | } 113 | 114 | @Override 115 | public CacheStore getCacheStore() { 116 | return target.getCacheStore(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/application/ImageCaptchaApplication.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.application; 2 | 3 | 4 | import cloud.tianai.captcha.application.vo.CaptchaResponse; 5 | import cloud.tianai.captcha.application.vo.ImageCaptchaVO; 6 | import cloud.tianai.captcha.cache.CacheStore; 7 | import cloud.tianai.captcha.common.response.ApiResponse; 8 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 9 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam; 10 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor; 11 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 12 | import cloud.tianai.captcha.validator.ImageCaptchaValidator; 13 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; 14 | import cloud.tianai.captcha.validator.common.model.dto.MatchParam; 15 | 16 | /** 17 | * @Author: 天爱有情 18 | * @Date 2020/5/29 8:33 19 | * @Description 滑块验证码应用程序 20 | */ 21 | public interface ImageCaptchaApplication { 22 | 23 | /** 24 | * 生成滑块验证码 25 | * 26 | * @return 27 | */ 28 | CaptchaResponse generateCaptcha(); 29 | 30 | /** 31 | * 生成滑块验证码 32 | * 33 | * @param type type类型 34 | * @return CaptchaResponse 35 | */ 36 | CaptchaResponse generateCaptcha(String type); 37 | 38 | /** 39 | * 生成滑块验证码 40 | * 41 | * @param captchaImageType 要生成webp还是jpg类型的图片 42 | * @return CaptchaResponse 43 | */ 44 | CaptchaResponse generateCaptcha(CaptchaImageType captchaImageType); 45 | 46 | /** 47 | * 生成验证码 48 | * 49 | * @param type type 50 | * @param captchaImageType CaptchaImageType 51 | * @return CaptchaResponse 52 | */ 53 | CaptchaResponse generateCaptcha(String type, CaptchaImageType captchaImageType); 54 | 55 | 56 | /** 57 | * 生成滑块验证码 58 | * 59 | * @param param param 60 | * @return CaptchaResponse 61 | */ 62 | CaptchaResponse generateCaptcha(GenerateParam param); 63 | 64 | /** 65 | * 匹配 66 | * 67 | * @param id 验证码的ID 68 | * @param matchParam 匹配数据,包含鼠标轨迹,设备信息等 69 | * @return 匹配成功返回true, 否则返回false 70 | */ 71 | ApiResponse matching(String id, MatchParam matchParam); 72 | 73 | /** 74 | * 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, MatchParam)} 75 | * 76 | * @param id 验证码的ID 77 | * @param track 轨迹数据 78 | * @return 匹配成功返回true, 否则返回false 79 | */ 80 | ApiResponse matching(String id, ImageCaptchaTrack track); 81 | 82 | /** 83 | * 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, MatchParam)} 84 | * 85 | * @param id id 86 | * @param percentage 百分比数据 87 | * @return boolean 88 | */ 89 | @Deprecated 90 | boolean matching(String id, Float percentage); 91 | 92 | /** 93 | * 查询该ID是属于哪个验证码类型 94 | * 95 | * @param id id 96 | * @return String 97 | */ 98 | String getCaptchaTypeById(String id); 99 | 100 | /** 101 | * 获取验证码资源管理器 102 | * 103 | * @return SliderCaptchaResourceManager 104 | */ 105 | ImageCaptchaResourceManager getImageCaptchaResourceManager(); 106 | 107 | /** 108 | * 设置 SliderCaptchaValidator 验证码验证器 109 | * 110 | * @param imageCaptchaValidator imageCaptchaValidator 111 | */ 112 | void setImageCaptchaValidator(ImageCaptchaValidator imageCaptchaValidator); 113 | 114 | /** 115 | * 设置 ImageCaptchaGenerator 验证码生成器 116 | * 117 | * @param imageCaptchaGenerator SliderCaptchaGenerator 118 | */ 119 | void setImageCaptchaGenerator(ImageCaptchaGenerator imageCaptchaGenerator); 120 | 121 | /** 122 | * 获取拦截器 123 | * 124 | * @return CaptchaInterceptor 125 | */ 126 | CaptchaInterceptor getCaptchaInterceptor(); 127 | 128 | /** 129 | * 设置 拦截器 130 | * 131 | * @param captchaInterceptor captchaInterceptor 132 | */ 133 | void setCaptchaInterceptor(CaptchaInterceptor captchaInterceptor); 134 | 135 | /** 136 | * 设置 缓存存储器 137 | * 138 | * @param cacheStore cacheStore 139 | */ 140 | void setCacheStore(CacheStore cacheStore); 141 | 142 | /** 143 | * 获取验证码验证器 144 | * 145 | * @return SliderCaptchaValidator 146 | */ 147 | ImageCaptchaValidator getImageCaptchaValidator(); 148 | 149 | /** 150 | * 获取验证码生成器 151 | * 152 | * @return SliderCaptchaTemplate 153 | */ 154 | ImageCaptchaGenerator getImageCaptchaGenerator(); 155 | 156 | /** 157 | * 获取缓存存储器 158 | * 159 | * @return CacheStore 160 | */ 161 | CacheStore getCacheStore(); 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/application/ImageCaptchaProperties.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.application; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * @Author: 天爱有情 10 | * @date 2020/10/19 18:41 11 | * @Description 滑块验证码属性 12 | */ 13 | @Data 14 | public class ImageCaptchaProperties { 15 | /** 过期key prefix. */ 16 | private String prefix = "captcha"; 17 | /** 过期时间. */ 18 | private Map expire = new HashMap<>(); 19 | 20 | // 本地提前缓存 21 | private boolean localCacheEnabled = false; 22 | private int localCacheSize = 10; 23 | private int localCacheWaitTime = 1000; 24 | private int localCachePeriod = 5000; 25 | private Long localCacheExpireTime; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/application/TACBuilder.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.application; 2 | 3 | import cloud.tianai.captcha.cache.CacheStore; 4 | import cloud.tianai.captcha.cache.impl.LocalCacheStore; 5 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 6 | import cloud.tianai.captcha.generator.ImageTransform; 7 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 8 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor; 9 | import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor; 10 | import cloud.tianai.captcha.resource.DefaultBuiltInResources; 11 | import cloud.tianai.captcha.resource.FontCache; 12 | import cloud.tianai.captcha.resource.ResourceProviders; 13 | import cloud.tianai.captcha.resource.ResourceStore; 14 | import cloud.tianai.captcha.resource.common.model.dto.Resource; 15 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap; 16 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 17 | import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore; 18 | import cloud.tianai.captcha.validator.ImageCaptchaValidator; 19 | import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator; 20 | 21 | /** 22 | * @Author: 天爱有情 23 | * @date 2024/7/14 16:41 24 | * @Description 一个构建ImageCaptchaApplication的工具, 免去一些繁琐的配置,方便新手用户一键使用 25 | */ 26 | public class TACBuilder { 27 | 28 | private CacheStore cacheStore; 29 | private ImageCaptchaGenerator generator; 30 | private ImageCaptchaValidator validator; 31 | private CaptchaInterceptor interceptor = EmptyCaptchaInterceptor.INSTANCE; 32 | private ImageCaptchaProperties prop = new ImageCaptchaProperties(); 33 | private ResourceStore resourceStore; 34 | private ImageTransform imageTransform; 35 | // private List fontWrappers = new ArrayList<>(); 36 | 37 | public static TACBuilder builder() { 38 | return TACBuilder.builder(new LocalMemoryResourceStore()); 39 | } 40 | 41 | public static TACBuilder builder(ResourceStore resourceStore) { 42 | TACBuilder builder = new TACBuilder(resourceStore); 43 | builder.prop = new ImageCaptchaProperties(); 44 | return builder; 45 | } 46 | 47 | private TACBuilder(ResourceStore resourceStore) { 48 | this.resourceStore = resourceStore; 49 | } 50 | 51 | public TACBuilder addDefaultTemplate(String defaultPathPrefix) { 52 | DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix); 53 | defaultBuiltInResources.addDefaultTemplate(resourceStore); 54 | return this; 55 | } 56 | 57 | public TACBuilder addDefaultTemplate() { 58 | return addDefaultTemplate(DefaultBuiltInResources.PATH_PREFIX); 59 | } 60 | 61 | public TACBuilder setCacheStore(CacheStore cacheStore) { 62 | this.cacheStore = cacheStore; 63 | return this; 64 | } 65 | 66 | public TACBuilder setGenerator(ImageCaptchaGenerator generator) { 67 | this.generator = generator; 68 | return this; 69 | } 70 | 71 | public TACBuilder setValidator(ImageCaptchaValidator validator) { 72 | this.validator = validator; 73 | return this; 74 | } 75 | 76 | public TACBuilder setInterceptor(CaptchaInterceptor interceptor) { 77 | this.interceptor = interceptor; 78 | return this; 79 | } 80 | 81 | public TACBuilder addFont(Resource resource) { 82 | this.addResource(FontCache.FONT_TYPE, resource); 83 | return this; 84 | } 85 | 86 | 87 | public TACBuilder cached(int size, int waitTime, int period, Long expireTime) { 88 | prop.setLocalCacheEnabled(true); 89 | prop.setLocalCacheSize(size); 90 | prop.setLocalCacheWaitTime(waitTime); 91 | prop.setLocalCachePeriod(period); 92 | prop.setLocalCacheExpireTime(expireTime); 93 | return this; 94 | } 95 | 96 | public TACBuilder prefix(String prefix) { 97 | this.prop.setPrefix(prefix); 98 | return this; 99 | } 100 | 101 | public TACBuilder expire(String captchaType, Long expireTime) { 102 | prop.getExpire().put(captchaType, expireTime); 103 | return this; 104 | } 105 | 106 | public TACBuilder setProp(ImageCaptchaProperties prop) { 107 | this.prop = prop; 108 | return this; 109 | } 110 | 111 | // public TACBuilder setResourceStore(ResourceStore resourceStore) { 112 | // this.resourceStore = resourceStore; 113 | // return this; 114 | // } 115 | 116 | 117 | public TACBuilder addResource(String captchaType, Resource imageResource) { 118 | this.resourceStore.addResource(captchaType, imageResource); 119 | return this; 120 | } 121 | 122 | public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) { 123 | this.resourceStore.addTemplate(captchaType, resourceMap); 124 | return this; 125 | } 126 | 127 | public TACBuilder setTransform(ImageTransform imageTransform) { 128 | this.imageTransform = imageTransform; 129 | return this; 130 | } 131 | 132 | public ImageCaptchaApplication build() { 133 | if (cacheStore == null) { 134 | cacheStore = new LocalCacheStore(); 135 | } 136 | if (generator == null) { 137 | ResourceProviders resourceProviders = new ResourceProviders(); 138 | DefaultImageCaptchaResourceManager resourceManager = new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders); 139 | generator = new MultiImageCaptchaGenerator(resourceManager, imageTransform); 140 | } 141 | // if (generator instanceof MultiImageCaptchaGenerator) { 142 | // ((MultiImageCaptchaGenerator) generator).setFontWrappers(fontWrappers); 143 | // } 144 | if (validator == null) { 145 | validator = new SimpleImageCaptchaValidator(); 146 | } 147 | if (interceptor == null) { 148 | interceptor = EmptyCaptchaInterceptor.INSTANCE; 149 | } 150 | DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor); 151 | return application; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/application/vo/CaptchaResponse.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.application.vo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * @Author: 天爱有情 11 | * @Date 2020/5/29 8:31 12 | * @Description 验证码返回对象 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class CaptchaResponse implements Serializable { 18 | private String id; 19 | private T captcha; 20 | 21 | public static CaptchaResponse of(String id, T data) { 22 | return new CaptchaResponse(id, data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.application.vo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class ImageCaptchaVO implements Serializable { 13 | /** 验证码类型.*/ 14 | private String type; 15 | /** 背景图.*/ 16 | private String backgroundImage; 17 | /** 移动图.*/ 18 | private String templateImage; 19 | /** 背景图片所属标签. */ 20 | private String backgroundImageTag; 21 | /** 模板图片所属标签. */ 22 | private String templateImageTag; 23 | /** 背景图片宽度.*/ 24 | private Integer backgroundImageWidth; 25 | /** 背景图片高度.*/ 26 | private Integer backgroundImageHeight; 27 | /** 滑动图片宽度.*/ 28 | private Integer templateImageWidth; 29 | /** 滑动图片高度.*/ 30 | private Integer templateImageHeight; 31 | /** data 扩展数据.*/ 32 | private Object data; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/cache/CacheStore.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.cache; 2 | 3 | import cloud.tianai.captcha.common.AnyMap; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * @Author: 天爱有情 9 | * @date 2022/3/2 14:35 10 | * @Description 提取出用于缓存的接口 11 | */ 12 | public interface CacheStore { 13 | 14 | /** 15 | * 读取缓存数据通过key 16 | * 17 | * @param key key 18 | * @return AnyMap 19 | */ 20 | AnyMap getCache(String key); 21 | 22 | /** 23 | * 获取并删除数据 通过key 24 | * 25 | * @param key key 26 | * @return AnyMap 27 | */ 28 | AnyMap getAndRemoveCache(String key); 29 | 30 | /** 31 | * 添加缓存数据 32 | * 33 | * @param key key 34 | * @param data data 35 | * @param expire 过期时间 36 | * @param timeUnit 过期时间单位 37 | * @return boolean 38 | */ 39 | boolean setCache(String key, AnyMap data, Long expire, TimeUnit timeUnit); 40 | 41 | 42 | /** 43 | * incr 数字 44 | * 45 | * @param key key 46 | * @param delta 境量 47 | * @param expire 过期时间 48 | * @param timeUnit 过期时间单位 49 | * @return Long 50 | */ 51 | Long incr(String key, long delta, Long expire, TimeUnit timeUnit); 52 | 53 | /** 54 | * get 数字 55 | * 56 | * @param key key 57 | * @return Long 58 | */ 59 | Long getLong(String key); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/cache/impl/ExpiringMap.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.cache.impl; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | public interface ExpiringMap extends Map { 10 | /** 11 | * 默认-1 无超时时间. 12 | */ 13 | Long DEFAULT_EXPIRE = -1L; 14 | 15 | /** 16 | * 添加值 17 | * @param k key 18 | * @param v value 19 | * @param timeout 超时时间, 20 | * @param timeUnit 超时时间单位 21 | * @return 返回旧的数据,如果没有,返回null 22 | */ 23 | TimeMapEntity put(K k, V v, Long timeout, TimeUnit timeUnit); 24 | 25 | /** 26 | * 获取value值 27 | * @param k key 28 | * @return 29 | */ 30 | Optional> getData(K k); 31 | 32 | /** 33 | * 获取某个key的过期时间 34 | * @param k key 35 | * @return 单位毫秒 36 | */ 37 | Long getExpire(K k); 38 | 39 | /** 40 | * 增加过期时间 41 | * @param k key 42 | * @param expire 过期时间 43 | * @param timeUnit 超时时间单位 44 | * @return 45 | */ 46 | boolean incr(K k, Long expire, TimeUnit timeUnit); 47 | 48 | /** 49 | * 初始化 50 | */ 51 | void init(); 52 | 53 | @Data 54 | class TimeMapEntity { 55 | private K key; 56 | private V value; 57 | private Long expire; 58 | private Long createTime; 59 | private long timeout = -1; 60 | 61 | TimeMapEntity(K k, V value, Long expire, Long createTime) { 62 | this.key = k; 63 | this.value = value; 64 | this.expire = expire; 65 | this.createTime = createTime; 66 | if (expire > 0) { 67 | this.timeout = createTime + expire; 68 | } 69 | } 70 | 71 | public TimeMapEntity(TimeMapEntity entity) { 72 | this.key = entity.getKey(); 73 | this.value = entity.getValue(); 74 | this.expire = entity.getExpire(); 75 | this.createTime = entity.getCreateTime(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/cache/impl/LocalCacheStore.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.cache.impl; 2 | 3 | 4 | import cloud.tianai.captcha.cache.CacheStore; 5 | import cloud.tianai.captcha.common.AnyMap; 6 | 7 | import java.util.Collections; 8 | import java.util.Map; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * @Author: 天爱有情 13 | * @date 2022/3/2 14:39 14 | * @Description 本地缓存 15 | */ 16 | public class LocalCacheStore implements CacheStore { 17 | 18 | protected ExpiringMap cache; 19 | 20 | public LocalCacheStore() { 21 | cache = new ConCurrentExpiringMap<>(); 22 | cache.init(); 23 | } 24 | 25 | @Override 26 | public AnyMap getCache(String key) { 27 | return cache.get(key); 28 | } 29 | 30 | @Override 31 | public AnyMap getAndRemoveCache(String key) { 32 | return cache.remove(key); 33 | } 34 | 35 | @Override 36 | public boolean setCache(String key, AnyMap data, Long expire, TimeUnit timeUnit) { 37 | cache.remove(key); 38 | cache.put(key, data, expire, timeUnit); 39 | return true; 40 | } 41 | 42 | @Override 43 | public Long incr(String key, long delta, Long expire, TimeUnit timeUnit) { 44 | Map value = cache.remove(key); 45 | if (value != null) { 46 | Long incr = (Long) value.get("___incr___"); 47 | if (incr == null) { 48 | incr = 0L; 49 | } 50 | incr += delta; 51 | cache.put(key, AnyMap.of(Collections.singletonMap("___incr___", incr)), expire, timeUnit); 52 | return incr; 53 | } 54 | cache.put(key, AnyMap.of(Collections.singletonMap("___incr___", delta)), expire, timeUnit); 55 | return delta; 56 | } 57 | 58 | @Override 59 | public Long getLong(String key) { 60 | Map stringObjectMap = cache.get(key); 61 | if (stringObjectMap != null) { 62 | return (Long) stringObjectMap.get("___incr___"); 63 | } 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/common/AnyMap.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.common; 2 | 3 | import lombok.EqualsAndHashCode; 4 | 5 | import java.util.Collection; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.BiFunction; 11 | import java.util.function.Function; 12 | 13 | @EqualsAndHashCode 14 | public class AnyMap implements Map { 15 | 16 | private Map target; 17 | 18 | public AnyMap() { 19 | target = new LinkedHashMap<>(); 20 | } 21 | 22 | public AnyMap(Map map) { 23 | this.target = map; 24 | } 25 | 26 | public Float getFloat(String key) { 27 | return getFloat(key, null); 28 | } 29 | 30 | public Float getFloat(String key, Float defaultData) { 31 | Object data = get(key); 32 | if (data != null) { 33 | if (data instanceof Number) { 34 | return ((Number) data).floatValue(); 35 | } 36 | try { 37 | if (data instanceof String) { 38 | return Float.parseFloat((String) data); 39 | } 40 | } catch (NumberFormatException e) { 41 | throw e; 42 | } 43 | } 44 | return defaultData; 45 | } 46 | 47 | public Integer getInt(String key, Integer defaultData) { 48 | Object data = get(key); 49 | if (data != null) { 50 | if (data instanceof Number) { 51 | return ((Number) data).intValue(); 52 | } 53 | try { 54 | if (data instanceof String) { 55 | return Integer.parseInt((String) data); 56 | } 57 | } catch (NumberFormatException e) { 58 | throw e; 59 | } 60 | } 61 | return defaultData; 62 | } 63 | 64 | public String getString(String key, String defaultData) { 65 | Object data = get(key); 66 | if (data != null) { 67 | if (data instanceof String) { 68 | return (String) data; 69 | } 70 | return String.valueOf(data); 71 | } 72 | return defaultData; 73 | } 74 | 75 | 76 | public static AnyMap of(Map map) { 77 | return new AnyMap(map); 78 | } 79 | 80 | // ================== implement Map ======================= 81 | 82 | 83 | @Override 84 | public int size() { 85 | return target.size(); 86 | } 87 | 88 | @Override 89 | public boolean isEmpty() { 90 | return target.isEmpty(); 91 | } 92 | 93 | @Override 94 | public boolean containsKey(Object key) { 95 | return target.containsKey(key); 96 | } 97 | 98 | @Override 99 | public boolean containsValue(Object value) { 100 | return target.containsValue(value); 101 | } 102 | 103 | @Override 104 | public Object get(Object key) { 105 | return target.get(key); 106 | } 107 | 108 | @Override 109 | public Object put(String key, Object value) { 110 | return target.put(key, value); 111 | } 112 | 113 | @Override 114 | public Object remove(Object key) { 115 | return target.remove(key); 116 | } 117 | 118 | @Override 119 | public void putAll(Map m) { 120 | target.putAll(m); 121 | } 122 | 123 | @Override 124 | public void clear() { 125 | target.clear(); 126 | } 127 | 128 | @Override 129 | public Set keySet() { 130 | return target.keySet(); 131 | } 132 | 133 | @Override 134 | public Collection values() { 135 | return target.values(); 136 | } 137 | 138 | @Override 139 | public Set> entrySet() { 140 | return target.entrySet(); 141 | } 142 | 143 | @Override 144 | public Object getOrDefault(Object key, Object defaultValue) { 145 | return target.getOrDefault(key, defaultValue); 146 | } 147 | 148 | @Override 149 | public void forEach(BiConsumer action) { 150 | target.forEach(action); 151 | } 152 | 153 | @Override 154 | public void replaceAll(BiFunction function) { 155 | target.replaceAll(function); 156 | } 157 | 158 | @Override 159 | public Object putIfAbsent(String key, Object value) { 160 | return target.putIfAbsent(key, value); 161 | } 162 | 163 | @Override 164 | public boolean remove(Object key, Object value) { 165 | return target.remove(key, value); 166 | } 167 | 168 | @Override 169 | public boolean replace(String key, Object oldValue, Object newValue) { 170 | return target.replace(key, oldValue, newValue); 171 | } 172 | 173 | @Override 174 | public Object replace(String key, Object value) { 175 | return target.replace(key, value); 176 | } 177 | 178 | @Override 179 | public Object computeIfAbsent(String key, Function mappingFunction) { 180 | return target.computeIfAbsent(key, mappingFunction); 181 | } 182 | 183 | @Override 184 | public Object computeIfPresent(String key, BiFunction remappingFunction) { 185 | return target.computeIfPresent(key, remappingFunction); 186 | } 187 | 188 | @Override 189 | public Object compute(String key, BiFunction remappingFunction) { 190 | return target.compute(key, remappingFunction); 191 | } 192 | 193 | @Override 194 | public Object merge(String key, Object value, BiFunction remappingFunction) { 195 | return target.merge(key, value, remappingFunction); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/common/constant/CaptchaTypeConstant.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.common.constant; 2 | 3 | /** 4 | * @Author: 天爱有情 5 | * @date 2021/8/7 17:14 6 | * @Description 滑块类型 7 | */ 8 | public interface CaptchaTypeConstant { 9 | 10 | /** 滑块. */ 11 | String SLIDER = "SLIDER"; 12 | /** 旋转. */ 13 | String ROTATE = "ROTATE"; 14 | /** 拼接. */ 15 | String CONCAT = "CONCAT"; 16 | /** 文字图片点选. */ 17 | String WORD_IMAGE_CLICK = "WORD_IMAGE_CLICK"; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/common/constant/CommonConstant.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.common.constant; 2 | 3 | public interface CommonConstant { 4 | 5 | String DEFAULT_TAG = "default"; 6 | /** 图标点选资源存储类型. */ 7 | String IMAGE_CLICK_ICON = "ICON"; 8 | /** 蜂窝点选.*/ 9 | String HONEYCOMB_CLICK_ICON = "HONEYCOMB_ICON"; 10 | /** 刮刮卡图标. */ 11 | String SCRAPE_ICON = "SCRAPE_ICON"; 12 | 13 | 14 | /** 15 | * 默认的resource资源文件路径. 16 | */ 17 | String DEFAULT_SLIDER_IMAGE_RESOURCE_PATH = "META-INF/cut-image/resource"; 18 | /** 19 | * 默认的template资源文件路径. 20 | */ 21 | String DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH = "META-INF/cut-image/template"; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/common/exception/ImageCaptchaException.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.common.exception; 2 | 3 | /** 4 | * @Author: 天爱有情 5 | * @date 2022/5/7 9:04 6 | * @Description 图片验证码异常 7 | */ 8 | public class ImageCaptchaException extends RuntimeException{ 9 | public ImageCaptchaException() { 10 | } 11 | 12 | public ImageCaptchaException(String message) { 13 | super(message); 14 | } 15 | 16 | public ImageCaptchaException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public ImageCaptchaException(Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | public ImageCaptchaException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 25 | super(message, cause, enableSuppression, writableStackTrace); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/common/response/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.common.response; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * @Author: 天爱有情 9 | * @date 2023/4/20 9:53 10 | * @Description 可能是最好用的API统一返回格式类 11 | */ 12 | @Data 13 | @SuppressWarnings({"unchecked", "rawtypes"}) 14 | public class ApiResponse implements Serializable { 15 | 16 | public static final ApiResponse SUCCESS; 17 | 18 | static { 19 | CodeDefinition definition = ApiResponseStatusConstant.SUCCESS; 20 | SUCCESS = new ApiResponse(definition.getCode(), definition.getMessage(), null); 21 | } 22 | 23 | /** 24 | * code码. 25 | */ 26 | private Integer code; 27 | /** 28 | * 信息. 29 | */ 30 | private String msg; 31 | /** 32 | * 成功时返回的数据. 33 | */ 34 | private T data; 35 | 36 | public ApiResponse(Integer code, String errMsg, T data) { 37 | this.code = code; 38 | this.msg = errMsg; 39 | this.data = data; 40 | } 41 | 42 | public ApiResponse(CodeDefinition definition, T data) { 43 | this.code = definition.getCode(); 44 | this.msg = definition.getMessage(); 45 | this.data = data; 46 | } 47 | 48 | public ApiResponse() { 49 | CodeDefinition definition = ApiResponseStatusConstant.SUCCESS; 50 | this.code = definition.getCode(); 51 | this.msg = definition.getMessage(); 52 | } 53 | 54 | public ApiResponse convert() { 55 | ApiResponse result = new ApiResponse<>(); 56 | result.setCode(this.getCode()); 57 | result.setMsg(this.getMsg()); 58 | return result; 59 | } 60 | 61 | 62 | public boolean isSuccess() { 63 | return ApiResponseStatusConstant.SUCCESS.getCode().equals(getCode()); 64 | } 65 | 66 | public static ApiResponse of(Integer code, String msg, T data) { 67 | return new ApiResponse(code, msg, data); 68 | } 69 | 70 | public static ApiResponse of(CodeDefinition definition, T data) { 71 | return new ApiResponse(definition.getCode(), definition.getMessage(), data); 72 | } 73 | 74 | public static ApiResponse ofMessage(CodeDefinition definition) { 75 | return new ApiResponse(definition.getCode(), definition.getMessage(), null); 76 | } 77 | 78 | public static ApiResponse ofError(String message) { 79 | return new ApiResponse(ApiResponseStatusConstant.INTERNAL_SERVER_ERROR.getCode(), message, null); 80 | } 81 | 82 | public static ApiResponse ofError(String message, Object obj) { 83 | return new ApiResponse(ApiResponseStatusConstant.INTERNAL_SERVER_ERROR.getCode(), message, obj); 84 | } 85 | 86 | public static ApiResponse ofCheckError(String message) { 87 | return new ApiResponse(ApiResponseStatusConstant.NOT_VALID_PARAM.getCode(), message, null); 88 | } 89 | 90 | public static ApiResponse ofSuccess(T data) { 91 | CodeDefinition definition = ApiResponseStatusConstant.SUCCESS; 92 | return new ApiResponse(definition.getCode(), definition.getMessage(), data); 93 | } 94 | 95 | public static ApiResponse ofSuccess() { 96 | return (ApiResponse) 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 trackList; 35 | /** 扩展数据,用户传输加密数据等.*/ 36 | private Object data; 37 | @Data 38 | @NoArgsConstructor 39 | @AllArgsConstructor 40 | public static class Track { 41 | /** x. */ 42 | private Float x; 43 | /** y. */ 44 | private Float y; 45 | /** 时间. */ 46 | private Float t; 47 | /** 类型. */ 48 | private String type = TrackTypeConstant.MOVE; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/validator/common/model/dto/MatchParam.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.validator.common.model.dto; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | /** 7 | * @Author: 天爱有情 8 | * @date 2024/8/19 15:12 9 | * @Description 验证码匹配的对象 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | public class MatchParam { 14 | /** 轨迹信息. */ 15 | private ImageCaptchaTrack track; 16 | /** 检测到的设备信息. */ 17 | private Drives drives; 18 | /** 留一个扩展属性. */ 19 | private Object extendData; 20 | 21 | 22 | public MatchParam(ImageCaptchaTrack track) { 23 | this.track = track; 24 | } 25 | 26 | public MatchParam(ImageCaptchaTrack track, Drives drives) { 27 | this.track = track; 28 | this.drives = drives; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/cloud/tianai/captcha/validator/impl/BasicCaptchaTrackValidator.java: -------------------------------------------------------------------------------- 1 | package cloud.tianai.captcha.validator.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.common.util.CollectionUtils; 8 | import cloud.tianai.captcha.common.util.ObjectUtils; 9 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * @Author: 天爱有情 15 | * @date 2022/2/17 11:01 16 | * @Description 基本的行为轨迹校验 17 | */ 18 | public class BasicCaptchaTrackValidator extends SimpleImageCaptchaValidator { 19 | public static final CodeDefinition DEFINITION = new CodeDefinition(50001, "basic check fail"); 20 | 21 | public BasicCaptchaTrackValidator() { 22 | } 23 | 24 | public BasicCaptchaTrackValidator(float defaultTolerant) { 25 | super(defaultTolerant); 26 | } 27 | 28 | @Override 29 | public ApiResponse beforeValid(ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) { 30 | // 校验参数 31 | checkParam(imageCaptchaTrack); 32 | return ApiResponse.ofSuccess(); 33 | } 34 | 35 | @Override 36 | public ApiResponse afterValid(Boolean basicValid, ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) { 37 | if (!basicValid){ 38 | return ApiResponse.ofSuccess(); 39 | } 40 | if (!CaptchaTypeClassifier.isSliderCaptcha(type)) { 41 | // 不是滑动验证码的话暂时跳过,点选验证码行为轨迹还没做 42 | return ApiResponse.ofSuccess(); 43 | } 44 | // 进行行为轨迹检测 45 | long startSlidingTime = imageCaptchaTrack.getStartTime().getTime(); 46 | long endSlidingTime = imageCaptchaTrack.getStopTime().getTime(); 47 | Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth(); 48 | List trackList = imageCaptchaTrack.getTrackList(); 49 | // 这里只进行基本检测, 用一些简单算法进行校验,如有需要可扩展 50 | // 检测1: 滑动时间如果小于300毫秒 返回false 51 | // 检测2: 轨迹数据要是少于背10,或者大于背景宽度的五倍 返回false 52 | // 检测3: x轴和y轴应该是从0开始的,要是一开始x轴和y轴乱跑,返回false 53 | // 检测4: 如果y轴是相同的,必然是机器操作,直接返回false 54 | // 检测5: x轴或者y轴直接的区间跳跃过大的话返回 false 55 | // 检测6: x轴应该是由快到慢的, 要是速率一致,返回false 56 | // 检测7: 如果x轴超过图片宽度的频率过高,返回false 57 | 58 | // 检测1 59 | if (startSlidingTime + 300 > endSlidingTime) { 60 | return ApiResponse.ofMessage(DEFINITION); 61 | } 62 | // 检测2 63 | if (trackList.size() < 10 || trackList.size() > bgImageWidth * 5) { 64 | return ApiResponse.ofMessage(DEFINITION); 65 | } 66 | // 检测3 67 | ImageCaptchaTrack.Track firstTrack = trackList.get(0); 68 | if (firstTrack.getX() > 10 || firstTrack.getX() < -10 || firstTrack.getY() > 10 || firstTrack.getY() < -10) { 69 | return ApiResponse.ofMessage(DEFINITION); 70 | } 71 | int check4 = 0; 72 | int check7 = 0; 73 | for (int i = 1; i < trackList.size(); i++) { 74 | ImageCaptchaTrack.Track track = trackList.get(i); 75 | float x = track.getX(); 76 | float y = track.getY(); 77 | // check4 78 | if (firstTrack.getY() == y) { 79 | check4++; 80 | } 81 | // check7 82 | if (x >= bgImageWidth) { 83 | check7++; 84 | } 85 | // check5 86 | ImageCaptchaTrack.Track preTrack = trackList.get(i - 1); 87 | if ((track.getX() - preTrack.getX()) > 50 || (track.getY() - preTrack.getY()) > 50) { 88 | return ApiResponse.ofMessage(DEFINITION); 89 | } 90 | } 91 | if (check4 == trackList.size() || check7 > 200) { 92 | return ApiResponse.ofMessage(DEFINITION); 93 | } 94 | 95 | // check6 96 | int splitPos = (int) (trackList.size() * 0.7); 97 | ImageCaptchaTrack.Track splitPostTrack = trackList.get(splitPos - 1); 98 | float posTime = splitPostTrack.getT(); 99 | float startAvgPosTime = posTime / (float) splitPos; 100 | 101 | ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); 102 | double endAvgPosTime = lastTrack.getT() / (float) (trackList.size() - splitPos); 103 | 104 | boolean check = endAvgPosTime > startAvgPosTime; 105 | if (check) { 106 | return ApiResponse.ofSuccess(); 107 | } 108 | return ApiResponse.ofMessage(DEFINITION); 109 | } 110 | 111 | public void checkParam(ImageCaptchaTrack imageCaptchaTrack) { 112 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageWidth())) { 113 | throw new IllegalArgumentException("bgImageWidth must not be null"); 114 | } 115 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getBgImageHeight())) { 116 | throw new IllegalArgumentException("bgImageHeight must not be null"); 117 | } 118 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getStartTime())) { 119 | throw new IllegalArgumentException("startSlidingTime must not be null"); 120 | } 121 | if (ObjectUtils.isEmpty(imageCaptchaTrack.getStopTime())) { 122 | throw new IllegalArgumentException("endSlidingTime must not be null"); 123 | } 124 | if (CollectionUtils.isEmpty(imageCaptchaTrack.getTrackList())) { 125 | throw new IllegalArgumentException("trackList must not be null"); 126 | } 127 | for (ImageCaptchaTrack.Track track : imageCaptchaTrack.getTrackList()) { 128 | Float x = track.getX(); 129 | Float y = track.getY(); 130 | Float t = track.getT(); 131 | String type = track.getType(); 132 | if (x == null || y == null || t == null || ObjectUtils.isEmpty(type)) { 133 | throw new IllegalArgumentException("track[x,y,t,type] must not be null"); 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/resource/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/resource/1.jpg -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/template/fonts/SIMSUN.TTC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/template/fonts/SIMSUN.TTC -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/template/rotate_1/active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/template/rotate_1/active.png -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/template/rotate_1/fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/template/rotate_1/fixed.png -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/template/slider_1/active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/template/slider_1/active.png -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/template/slider_1/fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/template/slider_1/fixed.png -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/template/slider_2/active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/template/slider_2/active.png -------------------------------------------------------------------------------- /src/main/resources/META-INF/cut-image/template/slider_2/fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/tianai-captcha/6d8736e52ede9ae7cb173d32b58d8ed1d09465ee/src/main/resources/META-INF/cut-image/template/slider_2/fixed.png -------------------------------------------------------------------------------- /src/main/test/java/example/readme/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.application.DefaultImageCaptchaApplication; 4 | import cloud.tianai.captcha.application.ImageCaptchaApplication; 5 | import cloud.tianai.captcha.application.ImageCaptchaProperties; 6 | import cloud.tianai.captcha.application.vo.CaptchaResponse; 7 | import cloud.tianai.captcha.application.vo.ImageCaptchaVO; 8 | import cloud.tianai.captcha.cache.CacheStore; 9 | import cloud.tianai.captcha.cache.impl.LocalCacheStore; 10 | import cloud.tianai.captcha.common.AnyMap; 11 | import cloud.tianai.captcha.common.response.ApiResponse; 12 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 13 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 14 | import cloud.tianai.captcha.interceptor.CaptchaInterceptorGroup; 15 | import cloud.tianai.captcha.interceptor.impl.BasicTrackCaptchaInterceptor; 16 | import cloud.tianai.captcha.interceptor.impl.ParamCheckCaptchaInterceptor; 17 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 18 | import cloud.tianai.captcha.resource.ResourceStore; 19 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 20 | import cloud.tianai.captcha.validator.ImageCaptchaValidator; 21 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; 22 | import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator; 23 | 24 | import java.util.UUID; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | public class ApplicationTest { 28 | 29 | public static void main(String[] args) { 30 | ImageCaptchaApplication application = createImageCaptchaApplication(); 31 | // 生成验证码数据, 可以将该数据直接返回给前端 , 可配合 tianai-captcha-web-sdk 使用 32 | CaptchaResponse res = application.generateCaptcha("SLIDER"); 33 | System.out.println(res); 34 | 35 | // 校验验证码, ImageCaptchaTrack 和 id 均为前端传开的参数, 可将 valid数据直接返回给 前端 36 | // 注意: 该项目只负责生成和校验验证码数据, 至于二次验证等需要自行扩展 37 | String id =res.getId(); 38 | ImageCaptchaTrack imageCaptchaTrack = null; 39 | ApiResponse valid = application.matching(id, imageCaptchaTrack); 40 | System.out.println(valid.isSuccess()); 41 | 42 | 43 | // 扩展: 一个简单的二次验证 44 | CacheStore cacheStore = new LocalCacheStore(); 45 | if (valid.isSuccess()) { 46 | // 如果验证成功,生成一个token并存储, 将该token返回给客户端,客户端下次请求数据时携带该token, 后台判断是否有效 47 | String token = UUID.randomUUID().toString(); 48 | cacheStore.setCache(token, new AnyMap(), 5L, TimeUnit.MINUTES); 49 | } 50 | 51 | } 52 | 53 | public static ImageCaptchaApplication createImageCaptchaApplication() { 54 | // 验证码资源管理器 该类负责管理验证码背景图和模板图等数据 55 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 56 | // 验证码生成器; 注意: 生成器必须调用init(...)初始化方法 true为加载默认资源,false为不加载, 57 | ImageCaptchaGenerator generator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager).init(); 58 | // 验证码校验器 59 | ImageCaptchaValidator imageCaptchaValidator = new SimpleImageCaptchaValidator(); 60 | // 缓存, 用于存放校验数据 61 | CacheStore cacheStore = new LocalCacheStore(); 62 | // 验证码拦截器, 可以是单个,也可以是一组拦截器,可以嵌套, 这里演示加载参数校验拦截,和 滑动轨迹拦截 63 | CaptchaInterceptorGroup group = new CaptchaInterceptorGroup(); 64 | group.addInterceptor(new ParamCheckCaptchaInterceptor()); 65 | group.addInterceptor(new BasicTrackCaptchaInterceptor()); 66 | 67 | ImageCaptchaProperties prop = new ImageCaptchaProperties(); 68 | // application 验证码封装, prop为所需的一些扩展参数 69 | ImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, imageCaptchaValidator, cacheStore, prop, group); 70 | return application; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/SimpleDemo.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.AnyMap; 4 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 5 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 6 | import cloud.tianai.captcha.generator.ImageTransform; 7 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo; 8 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 9 | import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform; 10 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 11 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 12 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; 13 | import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator; 14 | 15 | import java.util.Map; 16 | 17 | /** 18 | * 基础 SimpleDemo 19 | */ 20 | public class SimpleDemo { 21 | 22 | public static void main(String[] args) throws InterruptedException { 23 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 24 | ImageTransform imageTransform = new Base64ImageTransform(); 25 | ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(); 26 | BasicCaptchaTrackValidator imageCaptchaValidator = new BasicCaptchaTrackValidator(); 27 | // 注意: 上面这个四个对象都是单例的, 整个项目创建一次即可 28 | 29 | // 这里生成一个滑块验证码数据, 里面包括背景图、滑块图等等,按需传给前端进行展示 30 | ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER); 31 | 32 | // 这个数据是根据当前生成的这条验证码数据生成对应的验证数据, 该数据要存到缓存中 33 | AnyMap map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo); 34 | 35 | 36 | 37 | // 这是用户移动滑块后的校验接口 38 | // imageCaptchaTrack 对象为前端传来的滑动轨迹数据, 这里进行验证滑块, 返回 true 说明校验通过 39 | ImageCaptchaTrack imageCaptchaTrack = null; 40 | boolean check = imageCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/TACBuilderTest.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.application.ImageCaptchaApplication; 4 | import cloud.tianai.captcha.application.ImageCaptchaProperties; 5 | import cloud.tianai.captcha.application.TACBuilder; 6 | import cloud.tianai.captcha.application.vo.CaptchaResponse; 7 | import cloud.tianai.captcha.application.vo.ImageCaptchaVO; 8 | import cloud.tianai.captcha.cache.impl.LocalCacheStore; 9 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 10 | import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator; 11 | import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform; 12 | import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor; 13 | import cloud.tianai.captcha.resource.common.model.dto.Resource; 14 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap; 15 | import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore; 16 | import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider; 17 | 18 | import java.awt.*; 19 | 20 | 21 | public class TACBuilderTest { 22 | 23 | public static void main(String[] args) throws InterruptedException { 24 | Font font= null; 25 | // ResourceMap template1 = new ResourceMap("default", 4); 26 | // template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/active.png")); 27 | // template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/fixed.png")); 28 | 29 | ImageCaptchaApplication application = TACBuilder.builder(new LocalMemoryResourceStore()) 30 | // 加载系统自带的默认资源 31 | .addDefaultTemplate() 32 | // 设置验证码过期时间 33 | .expire("default", 10000L) 34 | .expire("WORD_IMAGE_CLICK", 60000L) 35 | // 设置拦截器 36 | .setInterceptor(EmptyCaptchaInterceptor.INSTANCE) 37 | // 添加验证码背景图片 38 | .addResource("SLIDER", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) 39 | .addResource("WORD_IMAGE_CLICK", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) 40 | .addResource("ROTATE", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) 41 | // 添加验证码模板图片 42 | // .addTemplate("SLIDER",template1) 43 | // 设置缓冲器,可提前生成验证码,用于增加并发性 44 | .cached(10, 1000, 5000, 10000L) 45 | // 添加字体包,用于给文字点选验证码提供字体 46 | .addFont(new Resource("file", "C:\\Users\\Thinkpad\\Desktop\\captcha\\手写字体\\ttf\\千图小兔体.ttf")) 47 | // 设置缓存存储器,如果要支持分布式,需要把这里改成分布式缓存,比如通过redis实现的 CacheStore 缓存 48 | .setCacheStore(new LocalCacheStore()) 49 | // 设置资源存储器,如果想在分布式环境或者想统一管理以及扩展 实现 ResourceStore 接口,自定义 50 | // .setResourceStore(new LocalMemoryResourceStore()) 51 | // 图片转换器,默认是将图片转换成base64格式, 背景图为jpg, 模板图为png, 如果想要扩展,可替换成自己实现的 52 | .setTransform(new Base64ImageTransform()) 53 | .build(); 54 | 55 | while (true){ 56 | long start = System.currentTimeMillis(); 57 | CaptchaResponse response = application.generateCaptcha("SLIDER"); 58 | System.out.println("耗时:" + (System.currentTimeMillis() - start)); 59 | // System.out.println(response); 60 | Thread.sleep(1000); 61 | } 62 | // System.out.println(response); 63 | 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/TACBuilderTest2.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.application.ImageCaptchaApplication; 4 | import cloud.tianai.captcha.application.TACBuilder; 5 | import cloud.tianai.captcha.application.vo.CaptchaResponse; 6 | import cloud.tianai.captcha.application.vo.ImageCaptchaVO; 7 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam; 8 | import cloud.tianai.captcha.interceptor.CaptchaInterceptor; 9 | import cloud.tianai.captcha.interceptor.Context; 10 | import cloud.tianai.captcha.resource.common.model.dto.Resource; 11 | 12 | import java.awt.*; 13 | import java.io.FileInputStream; 14 | import java.io.FileNotFoundException; 15 | import java.io.IOException; 16 | 17 | public class TACBuilderTest2 { 18 | 19 | public static void main(String[] args) throws IOException, FontFormatException { 20 | FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Thinkpad\\Desktop\\captcha\\手写字体\\ttf\\千图小兔体.ttf"); 21 | Font font = Font.createFont(Font.TRUETYPE_FONT, fileInputStream); 22 | fileInputStream.close(); 23 | ImageCaptchaApplication application = TACBuilder.builder() 24 | .addDefaultTemplate() 25 | .expire("default", 10000L) 26 | .expire("WORD_IMAGE_CLICK", 60000L) 27 | .addResource("SLIDER", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) 28 | .addResource("WORD_IMAGE_CLICK", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) 29 | .addResource("ROTATE", new Resource("classpath", "META-INF/cut-image/resource/1.jpg")) 30 | .setInterceptor(new CaptchaInterceptor() { 31 | @Override 32 | public CaptchaResponse beforeGenerateCaptcha(Context context, String type, GenerateParam param) { 33 | System.out.println("before generator"); 34 | return CaptchaInterceptor.super.beforeGenerateCaptcha(context, type, param); 35 | } 36 | }) 37 | .addFont(new Resource("file", "C:\\Users\\Thinkpad\\Desktop\\captcha\\手写字体\\ttf\\千图小兔体.ttf")) 38 | .build(); 39 | CaptchaResponse response = application.generateCaptcha("WORD_IMAGE_CLICK"); 40 | System.out.println(response); 41 | 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/Test.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 4 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 5 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo; 6 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 7 | import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform; 8 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 9 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 10 | import cloud.tianai.captcha.validator.ImageCaptchaValidator; 11 | import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator; 12 | 13 | import java.util.Map; 14 | 15 | public class Test { 16 | public static void main(String[] args) throws InterruptedException { 17 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 18 | Base64ImageTransform imageTransform = new Base64ImageTransform(); 19 | ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(); 20 | /* 21 | 生成滑块验证码图片, 可选项 22 | SLIDER (滑块验证码) 23 | ROTATE (旋转验证码) 24 | CONCAT (滑动还原验证码) 25 | WORD_IMAGE_CLICK (文字点选验证码) 26 | 27 | 更多验证码支持 详见 cloud.tianai.captcha.common.constant.CaptchaTypeConstant 28 | */ 29 | ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER); 30 | System.out.println(imageCaptchaInfo); 31 | 32 | // 负责计算一些数据存到缓存中,用于校验使用 33 | // ImageCaptchaValidator负责校验用户滑动滑块是否正确和生成滑块的一些校验数据; 比如滑块到凹槽的百分比值 34 | ImageCaptchaValidator imageCaptchaValidator = new BasicCaptchaTrackValidator(); 35 | // 这个map数据应该存到缓存中,校验的时候需要用到该数据 36 | Map map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/Test2.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.AnyMap; 4 | import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; 5 | import cloud.tianai.captcha.validator.impl.BasicCaptchaTrackValidator; 6 | 7 | import java.util.Map; 8 | 9 | public class Test2 { 10 | public static void main(String[] args) { 11 | BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator(); 12 | 13 | ImageCaptchaTrack imageCaptchaTrack = null; 14 | AnyMap map = null; 15 | Float percentage = null; 16 | // 用户传来的行为轨迹和进行校验 17 | // - imageCaptchaTrack为前端传来的滑动轨迹数据 18 | // - map 为生成验证码时缓存的map数据 19 | boolean check = sliderCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess(); 20 | // // 如果只想校验用户是否滑到指定凹槽即可,也可以使用 21 | // // - 参数1 用户传来的百分比数据 22 | // // - 参数2 生成滑块是真实的百分比数据 23 | check = sliderCaptchaValidator.checkPercentage(0.2f, percentage); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/Test3.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 4 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 5 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam; 6 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo; 7 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 8 | import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform; 9 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 10 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 11 | 12 | public class Test3 { 13 | public static void main(String[] args) { 14 | // 资源管理器 15 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 16 | Base64ImageTransform imageTransform = new Base64ImageTransform(); 17 | // 标准验证码生成器 18 | ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(); 19 | // 生成 具有混淆的 滑块验证码 (目前只有滑块验证码支持混淆滑块, 旋转验证,滑动还原,点选验证 均不支持混淆功能) 20 | ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(GenerateParam.builder() 21 | // 设置验证码类型 22 | .type(CaptchaTypeConstant.SLIDER) 23 | .templateFormatName("jpeg") 24 | // 设置背景图片格式 25 | .backgroundFormatName("png") 26 | // 是否添加混淆滑块 27 | .obfuscate(true) 28 | .build()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/Test4.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 4 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 5 | import cloud.tianai.captcha.generator.common.model.dto.GenerateParam; 6 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo; 7 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 8 | import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform; 9 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 10 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 11 | 12 | public class Test4 { 13 | public static void main(String[] args) { 14 | // 资源管理器 15 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 16 | // 标准验证码生成器 17 | ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,new Base64ImageTransform()).init(); 18 | // 生成旋转验证码 图片类型为 webp 19 | // 注意 tianai-captcha 后面默认删除了生成webp格式图片需要用户自定义添加webp转换的工具,需要用户自定义添加和扩展 20 | // 参考 https://bitbucket.org/luciad/webp-imageio 21 | ImageCaptchaInfo slideImageInfo = imageCaptchaGenerator.generateCaptchaImage(GenerateParam.builder() 22 | .type(CaptchaTypeConstant.ROTATE) 23 | .templateFormatName("webp") 24 | .backgroundFormatName("webp") 25 | .build()); 26 | System.out.println(slideImageInfo); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/Test6.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 4 | import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator; 5 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 6 | import cloud.tianai.captcha.resource.ResourceStore; 7 | import cloud.tianai.captcha.resource.common.model.dto.Resource; 8 | import cloud.tianai.captcha.resource.common.model.dto.ResourceMap; 9 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 10 | import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider; 11 | 12 | public class Test6 { 13 | public static void main(String[] args) { 14 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 15 | // 通过资源管理器或者资源存储器 16 | ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore(); 17 | // 添加滑块验证码模板.模板图片由三张图片组成 18 | ResourceMap template1 = new ResourceMap("default", 4); 19 | template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/active.png")); 20 | template1.put(StandardSliderImageCaptchaGenerator.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, "/fixed.png")); 21 | resourceStore.addTemplate(CaptchaTypeConstant.SLIDER, template1); 22 | // 模板与三张图片组成 滑块、凹槽、背景图 23 | // 同样默认支持 classpath 和 url 两种获取图片资源, 如果想扩展可实现 ResourceProvider 接口,进行自定义扩展 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/Test7.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 4 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 5 | import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform; 6 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 7 | import cloud.tianai.captcha.resource.ResourceProvider; 8 | import cloud.tianai.captcha.resource.common.model.dto.Resource; 9 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 10 | 11 | import java.io.InputStream; 12 | 13 | public class Test7 { 14 | public static void main(String[] args) { 15 | // 自定义 ResourceProvider 16 | ResourceProvider resourceProvider = new ResourceProvider() { 17 | @Override 18 | public InputStream getResourceInputStream(Resource data) { 19 | return null; 20 | } 21 | 22 | @Override 23 | public boolean supported(Resource type) { 24 | return false; 25 | } 26 | 27 | @Override 28 | public String getName() { 29 | return null; 30 | } 31 | }; 32 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 33 | ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,new Base64ImageTransform()).init(); 34 | // 注册 35 | imageCaptchaResourceManager.registerResourceProvider(resourceProvider); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/Test8.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 4 | import cloud.tianai.captcha.generator.ImageCaptchaGenerator; 5 | import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo; 6 | import cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator; 7 | import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator; 8 | import cloud.tianai.captcha.generator.impl.transform.Base64ImageTransform; 9 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 10 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 11 | 12 | public class Test8 { 13 | public static void main(String[] args) throws InterruptedException { 14 | // 使用 CacheSliderCaptchaGenerator 对滑块验证码进行缓存,使其提前生成滑块图片 15 | // 参数一: 真正实现 滑块的 SliderCaptchaGenerator 16 | // 参数二: 默认提前缓存多少个 17 | // 参数三: 出错后 等待xx时间再进行生成 18 | // 参数四: 检查时间间隔 19 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 20 | ImageCaptchaGenerator imageCaptchaGenerator = new CacheImageCaptchaGenerator(new MultiImageCaptchaGenerator(imageCaptchaResourceManager,new Base64ImageTransform()), 10, 1000, 100); 21 | imageCaptchaGenerator.init(); 22 | // 生成滑块图片 23 | ImageCaptchaInfo slideImageInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER); 24 | // 获取背景图片的base64 25 | String backgroundImage = slideImageInfo.getBackgroundImage(); 26 | // 获取滑块图片 27 | String sliderImage = slideImageInfo.getTemplateImage(); 28 | System.out.println(slideImageInfo); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/test/java/example/readme/TestImageCaptcha.java: -------------------------------------------------------------------------------- 1 | package example.readme; 2 | 3 | import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; 4 | import cloud.tianai.captcha.resource.ImageCaptchaResourceManager; 5 | import cloud.tianai.captcha.resource.ResourceStore; 6 | import cloud.tianai.captcha.resource.common.model.dto.Resource; 7 | import cloud.tianai.captcha.resource.impl.DefaultImageCaptchaResourceManager; 8 | import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider; 9 | import cloud.tianai.captcha.resource.impl.provider.URLResourceProvider; 10 | 11 | /** 12 | * 图片验证码测试 13 | */ 14 | public class TestImageCaptcha { 15 | public static void main(String[] args) { 16 | ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager(); 17 | // 通过资源管理器或者资源存储器 18 | ResourceStore resourceStore = imageCaptchaResourceManager.getResourceStore(); 19 | // 添加classpath目录下的 aa.jpg 图片 20 | resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource(ClassPathResourceProvider.NAME, "/aa.jpg")); 21 | // 添加远程url图片资源 22 | resourceStore.addResource(CaptchaTypeConstant.SLIDER,new Resource(URLResourceProvider.NAME, "http://www.xx.com/aa.jpg")); 23 | // 内置了通过url 和 classpath读取图片资源,如果想扩展可实现 ResourceProvider 接口,进行自定义扩展 24 | } 25 | } 26 | --------------------------------------------------------------------------------