feat: 升级 2.0.0-SNAPSHOT (Java 21/Spring Boot 3.4.5) + 旋转验证码修复 + 新增验证码类型支持

This commit is contained in:
abcv7
2026-08-19 23:36:13 +08:00
parent e7b9fd923b
commit d56958727c
10 changed files with 237 additions and 130 deletions
+25 -77
View File
@@ -12,26 +12,27 @@
<modules> <modules>
<module>tianai-captcha</module> <module>tianai-captcha</module>
<module>tianai-captcha-springboot-starter</module> <module>tianai-captcha-springboot-starter</module>
<module>tianai-captcha-platform</module>
</modules> </modules>
<properties> <properties>
<revision>1.5.5</revision> <revision>2.0.0-SNAPSHOT</revision>
<java.version>1.8</java.version> <java.version>21</java.version>
<!-- 打包跳过单元测试 --> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<skipTests>true</skipTests> <skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<skip.nexus>false</skip.nexus> <spring-boot.version>3.4.5</spring-boot.version>
<serverId>ossrh</serverId> <lombok.version>1.18.42</lombok.version>
<deplay.repository>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</deplay.repository> <slf4j.version>2.0.17</slf4j.version>
<deplay.snapshotRepository>https://s01.oss.sonatype.org/content/repositories/snapshots</deplay.snapshotRepository> <gson.version>2.13.1</gson.version>
<onnx-runtime.version>1.22.0</onnx-runtime.version>
<!-- 私服 --> <skip.nexus>true</skip.nexus>
<!-- <skip.nexus>true</skip.nexus>--> <serverId>ossrh</serverId>
<!-- <deplay.id>smart</deplay.id>-->
<!-- <deplay.repository>http://192.168.3.10:6061/repository/smart_hosted/</deplay.repository>-->
<!-- <deplay.snapshotRepository>http://192.168.3.10:6061/repository/smart_hosted/</deplay.snapshotRepository>-->
</properties> </properties>
<licenses> <licenses>
<license> <license>
@@ -68,11 +69,10 @@
<build> <build>
<plugins> <plugins>
<!-- 统一版本号管理 -->
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId> <artifactId>flatten-maven-plugin</artifactId>
<version>1.2.7</version> <version>1.7.0</version>
<configuration> <configuration>
<updatePomFile>true</updatePomFile> <updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode> <flattenMode>resolveCiFriendliesOnly</flattenMode>
@@ -96,75 +96,23 @@
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>2.9.1</version> <version>3.14.0</version>
<configuration> <configuration>
<!-- 统一生成聚合文档,解决 mvn package 时控制台发出 javadoc 警告的问题 --> <release>21</release>
<aggregate>true</aggregate> <compilerArgument>-parameters</compilerArgument>
<failOnError>false</failOnError> <annotationProcessorPaths>
<detectLinks>false</detectLinks> <path>
</configuration> <groupId>org.projectlombok</groupId>
<executions> <artifactId>lombok</artifactId>
<execution> <version>${lombok.version}</version>
<id>attach-javadocs</id> </path>
<goals> </annotationProcessorPaths>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.5.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>ossrh</publishingServerId>
<!-- <tokenAuth>true</tokenAuth>-->
<autoPublish>true</autoPublish>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
<pluginManagement> <pluginManagement>
<plugins> <plugins>
</plugins> </plugins>
</pluginManagement> </pluginManagement>
</build> </build>
+13 -18
View File
@@ -17,7 +17,7 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId> <artifactId>spring-boot-dependencies</artifactId>
<version>2.2.7.RELEASE</version> <version>${spring-boot.version}</version>
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
@@ -25,9 +25,7 @@
</dependencyManagement> </dependencyManagement>
<properties> <properties>
<java.version>1.8</java.version> <java.version>21</java.version>
<commons-lang3.version>3.7</commons-lang3.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests> <skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@@ -43,7 +41,6 @@
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.code.gson</groupId> <groupId>com.google.code.gson</groupId>
@@ -65,18 +62,14 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <version>${lombok.version}</version>
<scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>cloud.tianai.captcha</groupId> <groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId> <artifactId>tianai-captcha</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- <dependency>-->
<!-- <groupId>jakarta.validation</groupId>-->
<!-- <artifactId>jakarta.validation-api</artifactId>-->
<!-- </dependency>-->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
@@ -87,20 +80,22 @@
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration> <configuration>
<source>8</source> <release>21</release>
<target>8</target>
<compilerArgument>-parameters</compilerArgument> <compilerArgument>-parameters</compilerArgument>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>
+13 -8
View File
@@ -13,25 +13,23 @@
<url>https://gitee.com/tianai/tianai-captcha</url> <url>https://gitee.com/tianai/tianai-captcha</url>
<properties> <properties>
<java.version>1.8</java.version> <java.version>21</java.version>
<!-- 打包跳过单元测试 -->
<skipTests>true</skipTests> <skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.42</version> <version>${lombok.version}</version>
<optional>true</optional> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<version>1.7.30</version> <version>${slf4j.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -40,10 +38,17 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration> <configuration>
<source>8</source> <release>21</release>
<target>8</target>
<compilerArgument>-parameters</compilerArgument> <compilerArgument>-parameters</compilerArgument>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
@@ -167,6 +167,9 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
verificationVO.setBackgroundImageHeight(imageCaptchaInfo.getBackgroundImageHeight()); verificationVO.setBackgroundImageHeight(imageCaptchaInfo.getBackgroundImageHeight());
verificationVO.setTemplateImageWidth(imageCaptchaInfo.getTemplateImageWidth()); verificationVO.setTemplateImageWidth(imageCaptchaInfo.getTemplateImageWidth());
verificationVO.setTemplateImageHeight(imageCaptchaInfo.getTemplateImageHeight()); verificationVO.setTemplateImageHeight(imageCaptchaInfo.getTemplateImageHeight());
if (imageCaptchaInfo instanceof cloud.tianai.captcha.generator.common.model.dto.RotateImageCaptchaInfo) {
verificationVO.setDegree(((cloud.tianai.captcha.generator.common.model.dto.RotateImageCaptchaInfo) imageCaptchaInfo).getDegree());
}
verificationVO.setData(imageCaptchaInfo.getData() == null ? null : imageCaptchaInfo.getData().getViewData()); verificationVO.setData(imageCaptchaInfo.getData() == null ? null : imageCaptchaInfo.getData().getViewData());
verificationVO.setId(id); verificationVO.setId(id);
return ApiResponse.ofSuccess(verificationVO); return ApiResponse.ofSuccess(verificationVO);
@@ -31,6 +31,8 @@ public class ImageCaptchaVO implements Serializable {
private Integer templateImageWidth; private Integer templateImageWidth;
/** 滑动图片高度.*/ /** 滑动图片高度.*/
private Integer templateImageHeight; private Integer templateImageHeight;
/** 旋转验证码初始角度(仅ROTATE类型).*/
private Double degree;
/** data 扩展数据.*/ /** data 扩展数据.*/
private Object data; private Object data;
} }
@@ -1,18 +1,20 @@
package cloud.tianai.captcha.common.constant; package cloud.tianai.captcha.common.constant;
/**
* @Author: 天爱有情
* @date 2021/8/7 17:14
* @Description 滑块类型
*/
public interface CaptchaTypeConstant { public interface CaptchaTypeConstant {
/** 滑块. */
String SLIDER = "SLIDER"; String SLIDER = "SLIDER";
/** 旋转. */ String SLIDER_V2 = "SLIDER_V2";
String ROTATE = "ROTATE"; String ROTATE = "ROTATE";
/** 拼接. */
String CONCAT = "CONCAT"; String CONCAT = "CONCAT";
/** 文字图片点选. */
String WORD_IMAGE_CLICK = "WORD_IMAGE_CLICK"; String WORD_IMAGE_CLICK = "WORD_IMAGE_CLICK";
String ICON_CLICK = "ICON_CLICK";
String WORD_ORDER_CLICK = "WORD_ORDER_CLICK";
String CURVE_SLIDER = "CURVE_SLIDER";
String CURVE_SLIDER_V2 = "CURVE_SLIDER_V2";
String CURVE_SLIDER_V3 = "CURVE_SLIDER_V3";
String ANGLE = "ANGLE";
String SCRATCH = "SCRATCH";
String JIGSAW = "JIGSAW";
String CURVE_DRAW = "CURVE_DRAW";
String PROOF_OF_WORK = "PROOF_OF_WORK";
} }
@@ -47,14 +47,21 @@ public class MultiImageCaptchaGenerator extends AbstractImageCaptchaGenerator {
@Override @Override
protected void doInit() { protected void doInit() {
// 滑块验证码
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SLIDER, StandardSliderImageCaptchaGenerator::new)); addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SLIDER, StandardSliderImageCaptchaGenerator::new));
// 旋转验证码 addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SLIDER_V2, EnhancedSliderImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(ROTATE, StandardRotateImageCaptchaGenerator::new)); addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(ROTATE, StandardRotateImageCaptchaGenerator::new));
// 拼接验证码
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CONCAT, StandardConcatImageCaptchaGenerator::new)); addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CONCAT, StandardConcatImageCaptchaGenerator::new));
// 点选文字验证码
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(WORD_IMAGE_CLICK, StandardWordClickImageCaptchaGenerator::new)); addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(WORD_IMAGE_CLICK, StandardWordClickImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(ICON_CLICK, IconClickImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(WORD_ORDER_CLICK, WordOrderClickImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CURVE_SLIDER, CurveSliderImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(ANGLE, AngleRotateImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SCRATCH, ScratchImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(JIGSAW, JigsawImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CURVE_SLIDER_V2, CurveSliderV2ImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CURVE_SLIDER_V3, CurveSliderV3ImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CURVE_DRAW, CurveDrawImageCaptchaGenerator::new));
addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(PROOF_OF_WORK, ProofOfWorkCaptchaGenerator::new));
} }
public void addImageCaptchaGeneratorProvider(ImageCaptchaGeneratorProvider provider) { public void addImageCaptchaGeneratorProvider(ImageCaptchaGeneratorProvider provider) {
@@ -60,6 +60,14 @@ public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGen
Optional<BufferedImage> maskTemplateOptional = getTemplateImageOfOptional(templateResource, TEMPLATE_MASK_IMAGE_NAME); Optional<BufferedImage> maskTemplateOptional = getTemplateImageOfOptional(templateResource, TEMPLATE_MASK_IMAGE_NAME);
if (maskTemplateOptional.isPresent()) { if (maskTemplateOptional.isPresent()) {
maskTemplate = maskTemplateOptional.get(); maskTemplate = maskTemplateOptional.get();
} else {
// 没有mask.png时,生成实心圆形蒙版(alpha=255),确保切出完整图片内容
maskTemplate = new BufferedImage(fixedTemplate.getWidth(), fixedTemplate.getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics2D gMask = maskTemplate.createGraphics();
gMask.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
gMask.setColor(new java.awt.Color(255, 255, 255, 255));
gMask.fillOval(0, 0, fixedTemplate.getWidth(), fixedTemplate.getHeight());
gMask.dispose();
} }
// 算出居中的x和y // 算出居中的x和y
@@ -68,21 +76,10 @@ public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGen
// 抠图部分 // 抠图部分
BufferedImage cutImage = CaptchaImageUtils.cutImage(background, maskTemplate, x, y); BufferedImage cutImage = CaptchaImageUtils.cutImage(background, maskTemplate, x, y);
BufferedImage rotateFixed = fixedTemplate;
BufferedImage rotateActive = activeTemplate;
if (param.getObfuscate()) {
int randomDegree = randomInt(10, 350);
rotateFixed = CaptchaImageUtils.rotateImage(fixedTemplate, randomDegree);
randomDegree = randomInt(10, 350);
rotateActive = CaptchaImageUtils.rotateImage(activeTemplate, randomDegree);
}
CaptchaImageUtils.overlayImage(background, rotateFixed, x, y);
CaptchaImageUtils.overlayImage(cutImage, rotateActive, 0, 0);
// 随机旋转抠图部分
// 随机x,转换为角度 // 随机x,转换为角度
int randomX = randomInt(fixedTemplate.getWidth() + 10, background.getWidth() - 10); int randomX = randomInt(fixedTemplate.getWidth() + 10, background.getWidth() - 10);
double degree = 360d - randomX / ((background.getWidth()) / 360d); double degree = 360d - randomX / ((background.getWidth()) / 360d);
// 旋转的透明图片是一张正方形的 // 模板图:窄条透明画布,切片居中旋转放置(官方方式)
BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(cutImage.getWidth(), background.getHeight()); BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(cutImage.getWidth(), background.getHeight());
CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree); CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree);
@@ -53,6 +53,9 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.ROTATE); CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.ROTATE);
CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.SLIDER); CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.SLIDER);
CaptchaTypeClassifier.addClickCaptchaType(CaptchaTypeConstant.WORD_IMAGE_CLICK); CaptchaTypeClassifier.addClickCaptchaType(CaptchaTypeConstant.WORD_IMAGE_CLICK);
CaptchaTypeClassifier.addClickCaptchaType(CaptchaTypeConstant.ICON_CLICK);
CaptchaTypeClassifier.addClickCaptchaType(CaptchaTypeConstant.WORD_ORDER_CLICK);
CaptchaTypeClassifier.addJigsawCaptchaType(CaptchaTypeConstant.JIGSAW);
} }
public SimpleImageCaptchaValidator(float defaultTolerant) { public SimpleImageCaptchaValidator(float defaultTolerant) {
@@ -119,6 +122,25 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
if (CaptchaTypeClassifier.isSliderCaptcha(type)) { if (CaptchaTypeClassifier.isSliderCaptcha(type)) {
// 滑动验证码 // 滑动验证码
addPercentage(imageCaptchaInfo, map); addPercentage(imageCaptchaInfo, map);
} else if (CaptchaTypeConstant.CURVE_DRAW.equals(type)) {
// 曲线绘制验证码
if (expand == null) {
throw new IllegalArgumentException("曲线绘制验证码扩展数据为空, info=" + imageCaptchaInfo);
}
try {
cloud.tianai.captcha.generator.impl.CurveDrawImageCaptchaGenerator.CurveDrawData curveData =
(cloud.tianai.captcha.generator.impl.CurveDrawImageCaptchaGenerator.CurveDrawData) expand;
StringBuilder sb = new StringBuilder();
for (java.awt.geom.Point2D.Double point : curveData.getSamplePoints()) {
sb.append(calcPercentage((float) point.x, imageCaptchaInfo.getBackgroundImageWidth()))
.append(",")
.append(calcPercentage((float) point.y, imageCaptchaInfo.getBackgroundImageHeight()))
.append(";");
}
map.put(PERCENTAGE_KEY, sb.toString());
} catch (Exception e) {
throw new IllegalArgumentException("曲线绘制验证码扩展数据转换失败, info=" + imageCaptchaInfo, e);
}
} else if (CaptchaTypeClassifier.isClickCaptcha(type)) { } else if (CaptchaTypeClassifier.isClickCaptcha(type)) {
// 图片点选验证码 // 图片点选验证码
if (expand == null) { if (expand == null) {
@@ -158,6 +180,15 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
} else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) { } else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) {
// 拼图验证码 // 拼图验证码
map.put(PERCENTAGE_KEY, expand); map.put(PERCENTAGE_KEY, expand);
} else if (CaptchaTypeConstant.SCRATCH.equals(type)) {
// 刮刮卡验证码 - 存储需要刮开的百分比阈值
String threshold = (expand != null) ? String.valueOf(expand) : "50";
map.put(PERCENTAGE_KEY, threshold);
} else if (CaptchaTypeConstant.PROOF_OF_WORK.equals(type)) {
// 工作量证明验证码 - 存储challenge和difficulty
if (expand != null) {
map.put("pow_data", expand);
}
} }
} }
@@ -236,12 +267,21 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
if (CaptchaTypeClassifier.isSliderCaptcha(type)) { if (CaptchaTypeClassifier.isSliderCaptcha(type)) {
// 滑动类型验证码 // 滑动类型验证码
return doValidSliderCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); return doValidSliderCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
} else if (CaptchaTypeConstant.CURVE_DRAW.equals(type)) {
// 曲线绘制验证码
return doValidCurveDrawCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant);
} else if (CaptchaTypeClassifier.isClickCaptcha(type)) { } else if (CaptchaTypeClassifier.isClickCaptcha(type)) {
// 点选类型验证码 // 点选类型验证码
return doValidClickCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); return doValidClickCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
} else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) { } else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) {
// 拼图类型验证码 // 拼图类型验证码
return doValidJigsawCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); return doValidJigsawCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type);
} else if (CaptchaTypeConstant.SCRATCH.equals(type)) {
// 刮刮卡验证码
return doValidScratchCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant);
} else if (CaptchaTypeConstant.PROOF_OF_WORK.equals(type)) {
// 工作量证明验证码
return doValidProofOfWork(imageCaptchaTrack, imageCaptchaValidData);
} }
// 不支持的类型 // 不支持的类型
log.warn("校验验证码警告, 不支持的验证码类型:{}, 请手动扩展 cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator.doValid 进行校验扩展", type); log.warn("校验验证码警告, 不支持的验证码类型:{}, 请手动扩展 cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator.doValid 进行校验扩展", type);
@@ -254,7 +294,115 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide
} }
String posArr = (String) imageCaptchaTrack.getData(); String posArr = (String) imageCaptchaTrack.getData();
String successPosStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null); String successPosStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null);
return successPosStr.equals(posArr); return successPosStr != null && successPosStr.equals(posArr);
}
/**
* 校验刮刮卡验证码
* 检查用户刮开的面积是否超过阈值
*/
public boolean doValidScratchCaptcha(ImageCaptchaTrack imageCaptchaTrack,
AnyMap imageCaptchaValidData,
Float tolerant) {
// data 字段存储刮开百分比 (0-100)
if (imageCaptchaTrack.getData() == null) {
return false;
}
int scratchPercent;
try {
scratchPercent = Integer.parseInt(String.valueOf(imageCaptchaTrack.getData()));
} catch (NumberFormatException e) {
return false;
}
// 阈值默认50%
String thresholdStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, "50");
int threshold = 50;
try {
threshold = Integer.parseInt(thresholdStr);
} catch (NumberFormatException ignored) {}
return scratchPercent >= threshold;
}
/**
* 校验工作量证明验证码
* 检查用户提交的nonce是否满足SHA-256前导零要求
*/
public boolean doValidProofOfWork(ImageCaptchaTrack imageCaptchaTrack,
AnyMap imageCaptchaValidData) {
if (imageCaptchaTrack.getData() == null) {
return false;
}
String nonce = String.valueOf(imageCaptchaTrack.getData());
Object powData = imageCaptchaValidData.get("pow_data");
if (powData == null) {
return false;
}
// pow_data is the PoWData object from the generator
try {
cloud.tianai.captcha.generator.impl.ProofOfWorkCaptchaGenerator.PoWData data =
(cloud.tianai.captcha.generator.impl.ProofOfWorkCaptchaGenerator.PoWData) powData;
return cloud.tianai.captcha.generator.impl.ProofOfWorkCaptchaGenerator.verifyProof(
data.challenge, data.difficulty, nonce);
} catch (Exception e) {
return false;
}
}
/**
* 校验曲线绘制验证码
* 检查用户绘制的路径是否经过预期曲线的采样点附近
*/
public boolean doValidCurveDrawCaptcha(ImageCaptchaTrack imageCaptchaTrack,
AnyMap imageCaptchaValidData,
Float tolerant) {
String validStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null);
if (ObjectUtils.isEmpty(validStr)) {
return false;
}
String[] splitArr = validStr.split(";");
List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList();
if (trackList == null || trackList.isEmpty()) {
return false;
}
// 取出所有 MOVE 类型的轨迹点(用户的绘制路径)
List<ImageCaptchaTrack.Track> drawTrackList = trackList
.stream()
.filter(t -> TrackTypeConstant.MOVE.equalsIgnoreCase(t.getType()))
.collect(Collectors.toList());
if (drawTrackList.isEmpty()) {
return false;
}
// 对每个预期采样点,检查用户路径中是否有接近的点
int matchedCount = 0;
int bgWidth = imageCaptchaTrack.getBgImageWidth();
int bgHeight = imageCaptchaTrack.getBgImageHeight();
for (String posStr : splitArr) {
if (ObjectUtils.isEmpty(posStr)) {
continue;
}
String[] posArr = posStr.split(",");
if (posArr.length < 2) {
continue;
}
float xPercentage = Float.parseFloat(posArr[0]);
float yPercentage = Float.parseFloat(posArr[1]);
// 检查用户路径中是否有接近该采样点的位置
boolean matched = false;
for (ImageCaptchaTrack.Track track : drawTrackList) {
float calcXPercentage = calcPercentage(track.getX(), bgWidth);
float calcYPercentage = calcPercentage(track.getY(), bgHeight);
if (checkPercentage(calcXPercentage, xPercentage, tolerant)
&& checkPercentage(calcYPercentage, yPercentage, tolerant)) {
matched = true;
break;
}
}
if (matched) {
matchedCount++;
}
}
// 如果匹配率超过 70% 则通过
return (double) matchedCount / splitArr.length >= 0.7;
} }
/** /**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB