feat: 升级 2.0.0-SNAPSHOT (Java 21/Spring Boot 3.4.5) + 旋转验证码修复 + 新增验证码类型支持
This commit is contained in:
+3
@@ -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);
|
||||
|
||||
@@ -31,6 +31,8 @@ public class ImageCaptchaVO implements Serializable {
|
||||
private Integer templateImageWidth;
|
||||
/** 滑动图片高度.*/
|
||||
private Integer templateImageHeight;
|
||||
/** 旋转验证码初始角度(仅ROTATE类型).*/
|
||||
private Double degree;
|
||||
/** data 扩展数据.*/
|
||||
private Object data;
|
||||
}
|
||||
|
||||
+11
-9
@@ -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";
|
||||
}
|
||||
|
||||
+11
-4
@@ -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) {
|
||||
|
||||
+10
-13
@@ -60,6 +60,14 @@ public class StandardRotateImageCaptchaGenerator extends AbstractImageCaptchaGen
|
||||
Optional<BufferedImage> 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);
|
||||
|
||||
|
||||
+149
-1
@@ -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<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 |
Reference in New Issue
Block a user