diff --git a/pom.xml b/pom.xml index a1a517a..24ab43f 100644 --- a/pom.xml +++ b/pom.xml @@ -12,26 +12,27 @@ tianai-captcha tianai-captcha-springboot-starter + tianai-captcha-platform - 1.5.5 - 1.8 - + 2.0.0-SNAPSHOT + 21 + 21 + 21 + 21 true UTF-8 UTF-8 - false - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - https://s01.oss.sonatype.org/content/repositories/snapshots + 3.4.5 + 1.18.42 + 2.0.17 + 2.13.1 + 1.22.0 - - - - - + true + ossrh @@ -68,11 +69,10 @@ - org.codehaus.mojo flatten-maven-plugin - 1.2.7 + 1.7.0 true resolveCiFriendliesOnly @@ -96,75 +96,23 @@ org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 + maven-compiler-plugin + 3.14.0 - - true - false - false - - - - attach-javadocs - - jar - - - -Xdoclint:none - - - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - org.apache.maven.plugins - maven-resources-plugin - 2.6 - - - org.sonatype.central - central-publishing-maven-plugin - 0.5.0 - true - - ossrh - - true + 21 + -parameters + + + org.projectlombok + lombok + ${lombok.version} + + - - diff --git a/tianai-captcha-springboot-starter/pom.xml b/tianai-captcha-springboot-starter/pom.xml index 968dcb1..4ca279e 100644 --- a/tianai-captcha-springboot-starter/pom.xml +++ b/tianai-captcha-springboot-starter/pom.xml @@ -17,7 +17,7 @@ org.springframework.boot spring-boot-dependencies - 2.2.7.RELEASE + ${spring-boot.version} pom import @@ -25,9 +25,7 @@ - 1.8 - 3.7 - + 21 true UTF-8 UTF-8 @@ -43,7 +41,6 @@ org.apache.commons commons-lang3 - ${commons-lang3.version} com.google.code.gson @@ -65,18 +62,14 @@ org.projectlombok lombok - true + ${lombok.version} + provided cloud.tianai.captcha tianai-captcha ${revision} - - - - - org.springframework.boot spring-boot-starter-web @@ -87,20 +80,22 @@ - - org.springframework.boot - spring-boot-maven-plugin - org.apache.maven.plugins maven-compiler-plugin + 3.14.0 - 8 - 8 + 21 -parameters + + + org.projectlombok + lombok + ${lombok.version} + + - diff --git a/tianai-captcha/pom.xml b/tianai-captcha/pom.xml index b8902e8..70c31d6 100644 --- a/tianai-captcha/pom.xml +++ b/tianai-captcha/pom.xml @@ -13,25 +13,23 @@ https://gitee.com/tianai/tianai-captcha - 1.8 - + 21 true UTF-8 UTF-8 - org.projectlombok lombok - 1.18.42 - true + ${lombok.version} + provided org.slf4j slf4j-api - 1.7.30 + ${slf4j.version} @@ -40,10 +38,17 @@ org.apache.maven.plugins maven-compiler-plugin + 3.14.0 - 8 - 8 + 21 -parameters + + + org.projectlombok + lombok + ${lombok.version} + + diff --git a/tianai-captcha/src/main/java/cloud/tianai/captcha/application/DefaultImageCaptchaApplication.java b/tianai-captcha/src/main/java/cloud/tianai/captcha/application/DefaultImageCaptchaApplication.java index b8088b3..da6837d 100644 --- a/tianai-captcha/src/main/java/cloud/tianai/captcha/application/DefaultImageCaptchaApplication.java +++ b/tianai-captcha/src/main/java/cloud/tianai/captcha/application/DefaultImageCaptchaApplication.java @@ -167,6 +167,9 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication { verificationVO.setBackgroundImageHeight(imageCaptchaInfo.getBackgroundImageHeight()); verificationVO.setTemplateImageWidth(imageCaptchaInfo.getTemplateImageWidth()); 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.setId(id); return ApiResponse.ofSuccess(verificationVO); diff --git a/tianai-captcha/src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java b/tianai-captcha/src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java index 1aa3b08..ad9df1a 100644 --- a/tianai-captcha/src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java +++ b/tianai-captcha/src/main/java/cloud/tianai/captcha/application/vo/ImageCaptchaVO.java @@ -31,6 +31,8 @@ public class ImageCaptchaVO implements Serializable { private Integer templateImageWidth; /** 滑动图片高度.*/ private Integer templateImageHeight; + /** 旋转验证码初始角度(仅ROTATE类型).*/ + private Double degree; /** data 扩展数据.*/ private Object data; } diff --git a/tianai-captcha/src/main/java/cloud/tianai/captcha/common/constant/CaptchaTypeConstant.java b/tianai-captcha/src/main/java/cloud/tianai/captcha/common/constant/CaptchaTypeConstant.java index 41bfa49..f91ebeb 100644 --- a/tianai-captcha/src/main/java/cloud/tianai/captcha/common/constant/CaptchaTypeConstant.java +++ b/tianai-captcha/src/main/java/cloud/tianai/captcha/common/constant/CaptchaTypeConstant.java @@ -1,18 +1,20 @@ package cloud.tianai.captcha.common.constant; -/** - * @Author: 天爱有情 - * @date 2021/8/7 17:14 - * @Description 滑块类型 - */ public interface CaptchaTypeConstant { - /** 滑块. */ String SLIDER = "SLIDER"; - /** 旋转. */ + String SLIDER_V2 = "SLIDER_V2"; String ROTATE = "ROTATE"; - /** 拼接. */ String CONCAT = "CONCAT"; - /** 文字图片点选. */ 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"; } diff --git a/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java b/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java index 3e674a7..5accec8 100644 --- a/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java +++ b/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/MultiImageCaptchaGenerator.java @@ -47,14 +47,21 @@ public class MultiImageCaptchaGenerator extends AbstractImageCaptchaGenerator { @Override protected void doInit() { - // 滑块验证码 addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SLIDER, StandardSliderImageCaptchaGenerator::new)); - // 旋转验证码 + addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(SLIDER_V2, EnhancedSliderImageCaptchaGenerator::new)); addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(ROTATE, StandardRotateImageCaptchaGenerator::new)); - // 拼接验证码 addImageCaptchaGeneratorProvider(new CommonImageCaptchaGeneratorProvider(CONCAT, StandardConcatImageCaptchaGenerator::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) { diff --git a/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java b/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java index a5dd5a8..e3bf428 100644 --- a/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java +++ b/tianai-captcha/src/main/java/cloud/tianai/captcha/generator/impl/StandardRotateImageCaptchaGenerator.java @@ -60,6 +60,14 @@ public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGen Optional maskTemplateOptional = getTemplateImageOfOptional(templateResource, TEMPLATE_MASK_IMAGE_NAME); if (maskTemplateOptional.isPresent()) { 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 @@ -68,21 +76,10 @@ public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGen // 抠图部分 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); double degree = 360d - randomX / ((background.getWidth()) / 360d); - // 旋转的透明图片是一张正方形的 + // 模板图:窄条透明画布,切片居中旋转放置(官方方式) BufferedImage matrixTemplate = CaptchaImageUtils.createTransparentImage(cutImage.getWidth(), background.getHeight()); CaptchaImageUtils.centerOverlayAndRotateImage(matrixTemplate, cutImage, degree); diff --git a/tianai-captcha/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java b/tianai-captcha/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java index 00951f8..af2e950 100644 --- a/tianai-captcha/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java +++ b/tianai-captcha/src/main/java/cloud/tianai/captcha/validator/impl/SimpleImageCaptchaValidator.java @@ -53,6 +53,9 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.ROTATE); CaptchaTypeClassifier.addSliderCaptchaType(CaptchaTypeConstant.SLIDER); 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) { @@ -119,6 +122,25 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide if (CaptchaTypeClassifier.isSliderCaptcha(type)) { // 滑动验证码 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)) { // 图片点选验证码 if (expand == null) { @@ -158,6 +180,15 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide } else if (CaptchaTypeClassifier.isJigsawCaptcha(type)) { // 拼图验证码 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)) { // 滑动类型验证码 return doValidSliderCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); + } else if (CaptchaTypeConstant.CURVE_DRAW.equals(type)) { + // 曲线绘制验证码 + return doValidCurveDrawCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant); } else if (CaptchaTypeClassifier.isClickCaptcha(type)) { // 点选类型验证码 return doValidClickCaptcha(imageCaptchaTrack, imageCaptchaValidData, tolerant, type); } else if (CaptchaTypeClassifier.isJigsawCaptcha(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); @@ -254,7 +294,115 @@ public class SimpleImageCaptchaValidator implements ImageCaptchaValidator, Slide } String posArr = (String) imageCaptchaTrack.getData(); 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 trackList = imageCaptchaTrack.getTrackList(); + if (trackList == null || trackList.isEmpty()) { + return false; + } + // 取出所有 MOVE 类型的轨迹点(用户的绘制路径) + List 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; } /** diff --git a/tianai-captcha/src/main/resources/META-INF/cut-image/template/rotate_1/fixed.png b/tianai-captcha/src/main/resources/META-INF/cut-image/template/rotate_1/fixed.png index 4295ba3..a2f1eb4 100644 Binary files a/tianai-captcha/src/main/resources/META-INF/cut-image/template/rotate_1/fixed.png and b/tianai-captcha/src/main/resources/META-INF/cut-image/template/rotate_1/fixed.png differ