From d56958727c6092fd2be293e83c300e3ef7ea1dae Mon Sep 17 00:00:00 2001 From: abcv7 Date: Wed, 19 Aug 2026 23:36:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=87=E7=BA=A7=202.0.0-SNAPSHOT=20(?= =?UTF-8?q?Java=2021/Spring=20Boot=203.4.5)=20+=20=E6=97=8B=E8=BD=AC?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E4=BF=AE=E5=A4=8D=20+=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E9=AA=8C=E8=AF=81=E7=A0=81=E7=B1=BB=E5=9E=8B=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 102 +++--------- tianai-captcha-springboot-starter/pom.xml | 31 ++-- tianai-captcha/pom.xml | 21 ++- .../DefaultImageCaptchaApplication.java | 3 + .../application/vo/ImageCaptchaVO.java | 2 + .../common/constant/CaptchaTypeConstant.java | 20 +-- .../impl/MultiImageCaptchaGenerator.java | 15 +- .../StandardRotateImageCaptchaGenerator.java | 23 ++- .../impl/SimpleImageCaptchaValidator.java | 150 +++++++++++++++++- .../cut-image/template/rotate_1/fixed.png | Bin 3171 -> 5186 bytes 10 files changed, 237 insertions(+), 130 deletions(-) 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 4295ba380d0dac7114a0fd0d0a2c73f9981fb6ce..a2f1eb4fea79f377b0992ff682c253335952f179 100644 GIT binary patch literal 5186 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 literal 3171 zcmW+&c{~)%8{Z9!CC5+YUW+y&S1MOzu~=Fm=az~bt@~VeV%@}ASLE1O=j$lBCCZVq zj+Vq)MXuJJ^F{cr_m7!*=9y>a`<)PQZDwCyP3Au~)I+2&|lddIZnX`y1NeJ+1 z#c!aF&Kx->gA{s7r-+QnrkN7-+X zoRTLETo#1So`}kIwVXphku_lr-Pw1s0 zlC!U&DJ;mcl|UERj?q$@zaGVmV{hG#fTa#G5e|&Ax*BnCv`e($qe#-erP8PF!t?FU zS!b$VJnK_u*|-hxE$Pi(IaC!med5xmo+BY|A3!j6D-W ze8#B}O(e5EW|6lSHDxhPWu@i(@?t}_Pdk&|RR99d=aSt`;>p6_Rn z$VuL(ci&8Hs$59V;85Xg`!XAtM4Ej)uRPQ>$rEW3+M$|Kem7xuw}6r$kQDQhd{`s* zdBd*THoAXn@5(y}4twu7=h>im6O{n|iO7xnU3UmnBaU2Zl9wRgM|4bO zv%esPSK;f$lor%?C&QySb7bhSZtL=(&uhXY@*+%B$2%rj7kdI_slnG%3p~R4(P_m$ z*Ccg6vis(*-rKOd@094-Ry!=gZKNJaG!m3rIo9E8K4QByomRR1)oU;3NJ`1uPq@?0U+MF&yntBo%d_Yin)^J7nP zP?@i8ts4afupM#EBK_@&&0w$Ks3LYfT=OIKKPjJVicIv9lS=u$g5nEs@q#7_+=q|w zPVex1DE(TxlycnNU#-hj9s-XNSg`NQJZl2WU^x17GRr6 z@ON_`v*$957c4Cs{qE^x&(mbI#f|L+X`7Q(>`Gd}s!6b3&&yliul@_!NNs5x+1@E? z41*o2#b*@%^v?og-jgDz`a->B|B3=q(yhup?@=ZU`=O#(o4K9K;ab}GYHYJ>5o<>2 z4%g;8k^*Ua0?^R>MZe*g;E1LChOcFB8-N9Lg#})xcOB=AT1&|}SgWmz%KU~4`}EK~ z>kH-5w{P}wykMdxkNi~F4B~@4GEDC?yJ+quTVYUTB8+&EHM|I{Pn1305Y)I-gCPj@ z9ORHRT1xPew7-*%$OzYY0^v8D#yp^}Uk#0GHmIJw*#2#$q(NpW?Z&8gTc&L1^0+VB z2|HmmO}IA^WFXAGfZihR!?`Ip3WP}z3W~KH1~TepyUa@fm{~~ef+3Y}SJRB;M0MdA zL5booYLo$SB<(Eb?NtW!^K0EArsjDy>tHwZIo!=?bz*lItP1lS@RPv1u@+psh`@Tsebr z&z)m-pem2JHli!AXSNhSa(?{7GR?O)=IZtWo+ZJ(5uY1UyT?VbzmEDu7oqeYD00|$4R(&{wzhv#pbFEL0z(i!vJjZ0J1M!c~H58g9BXsJ`3k*uqo!H4uaW*NHZUn01)re0=S9K z9&jo9hz?T$sN0@`xiDbmt;~6@8FDk0{CY}O&Bzk~(nuu2Q}Dvi;9?B47pLfKsWt#{ z(1d|602I#P24Qe%KV?vdJKe!E>N|igd-cHb(|vJvrqq2!_v^KfIT!~fSagC0(e*{A zm2|fs_CsHqu?F4N#l6EZ4lwq2p&tbZQge1P&>cbsp1esSMaKEFaAc3lQeX2juIaDb z_*@7mn2%v{j)YeS@)*RCW;1`;GWPPrL`C)9)F|U_)gW&>m2a>NyaL6{K0t_1^9s9O zzfOiOo3aLVyEWvZ*F{O2KWV*bm~v*L!HAWxLHkTZR|p;$!n+Y|y_7lSVe#%@Pn!AW zbl^^|z-~NjQZ+!Ea?aumY(BnSfBT#3gJMYc(N}Z^5a#Tm=_mbb9~#gm_UwN=mFxPc zHg$Zr0*!JLgouvKX4HvqJ(V~;#*-H7h`&mV{(FNgk~of2b+%RbnC4-6f%basKC-Qg z%4fMzL!^a}fG~$}Nk3`l)YqT@?Sy#JVtJ~~RpoXm3`8a}YAcfDRxE-RE@Sl(+x26B zPsfx?sehuwMP^sR9VHkMzGP6oi9yTX=3?_TUF%{_X~lnvb9GVM&^ll3nu7o-M#xM` zU;*FaYpV0Ybz0mBvy(P3SZfVAIKYVc@tCz|o~XS&-aXAoDYO-G^C3a(6_+&6trRz| zhGCql)+Ks)K!U@6z1k@Z-yjVGo&&)*wM_leMnCkR@|LM@dHkZMVDnyMeNm{X}&<+)l*00X> zvB$ye_yd1P8k!3eX^5LKSD^+szg4PHrkz69B;&EY{YZ=kBL#!+t|m8Cut&&iTW}#; zk(7?~J{w9Xd8#Ko-AwKYA5&?E9{JS;De&@)^Y_}Udg+)U5Zi1vQh|gz;QdD@4mEtg zkg~|cJmJbWwn!k~MmvSk5KnC>6&YG$#N4~3O?m^bGM4Xqq;IRPOX9CGmA>`BgrS7i zq2JsaEIXRY{%TGucuw}*8F39qGn~nF5_X0C(`}s!53=j4%=U+^`aVMIMB}{-a|^%j z!T0G|^`sn>sZZY*Xo_H?w|hwJPxkpaB54R6uxs(TPN2b!`~~e|#?;+OcR^fOFcq$B z@T!|>-V%&Z$iQut7NVRw3b~D2JEOED^i|u#uCr1Hl0Ug`1Jih(|0<&QXcQ2o#D%(9 zMiQECx}G=Z0rMtnjl}KUgjVoJ@>nwdbHL*Y%PIYf-Ix(3=H`h@kvs_RjRG7o0SIYG^qoww>MgWe~Vkr{$QZMB-e#Hl2obFK~lML=rx zSSP6&9K0@3Zr;;|eJC!jz5>vPv?PiiVLAO+dky0btBJC{iD`#FUi%Dv* zppgq$VXFpVEh$IXLL%xmzCJcEo%Wv`5|9+5$y9LIFG#emJk=ibMo2@_>V?x9VY@cR zZ?AyPpcG9gdoWQ%eU4u0e5i*{#?-h6IkJ`%@umx zyNCX4;WtLwfBY(RK35Jr6}9=(ZQbNL)y1xSP3L%djpBJROy0b;m7?Ktv&djteJ2-N z