feat(captcha): 升级验证码校验流程与资源管理

- 引入MatchParam封装滑动轨迹及相关信息,提升扩展性。- 调整验证码生成与校验接口,以支持更精细的参数控制。
- 优化资源管理,提高验证码资源的加载效率。
- 更新文档与示例代码,以反映API的最新变化。BREAKING CHANGE: 验证码校验接口发生改变,现在需要传入MatchParam对象而非直接传入轨迹对象。这可能会影响直接调用验证码校验服务的客户端代码,需根据最新API文档进行适配。
This commit is contained in:
天爱有情
2024-08-19 16:26:18 +08:00
parent 9324cce657
commit 407bfe87b0
22 changed files with 118 additions and 53 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.5.0</version>
<version>1.5.1</version>
<name>tianai-captcha</name>
<description>行为验证码</description>
+4 -2
View File
@@ -37,13 +37,15 @@
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.5.0</version>
<version>1.5.1</version>
</dependency>
```
### 2. 构建 `ImageCaptchaApplication`负责生成和校验验证码
```java
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
public class ApplicationTest {
public static void main(String[] args) {
@@ -63,7 +65,7 @@ public class ApplicationTest {
// 注意: 该项目只负责生成和校验验证码数据, 至于二次验证等需要自行扩展
String id = res.getId();
ImageCaptchaTrack imageCaptchaTrack = null;
ApiResponse<?> valid = application.matching(id, imageCaptchaTrack);
ApiResponse<?> valid = application.matching(id, new MatchParam(imageCaptchaTrack));
System.out.println(valid.isSuccess());
@@ -18,6 +18,7 @@ import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
import lombok.extern.slf4j.Slf4j;
@@ -155,23 +156,28 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack imageCaptchaTrack) {
public ApiResponse<?> matching(String id, MatchParam matchParam) {
AnyMap validData = getVerification(id);
if (validData == null) {
return ApiResponse.ofMessage(ApiResponseStatusConstant.EXPIRED);
}
ApiResponse<?> response = beforeValid(id, imageCaptchaTrack, validData);
ApiResponse<?> response = beforeValid(id, matchParam, validData);
if (!response.isSuccess()) {
return response;
}
ApiResponse<?> basicValid = getImageCaptchaValidator().valid(imageCaptchaTrack, validData);
response = afterValid(id, imageCaptchaTrack, validData, basicValid);
ApiResponse<?> basicValid = getImageCaptchaValidator().valid(matchParam.getTrack(), validData);
response = afterValid(id, matchParam, validData, basicValid);
if (!response.isSuccess()) {
return response;
}
return basicValid;
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack track) {
return matching(id, new MatchParam(track, null));
}
@Override
public boolean matching(String id, Float percentage) {
@@ -295,12 +301,12 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
captchaInterceptor.afterGenerateImageCaptchaValidData(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, validData);
}
private ApiResponse<?> beforeValid(String id, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) {
return captchaInterceptor.beforeValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), imageCaptchaTrack, validData);
private ApiResponse<?> beforeValid(String id, MatchParam matchParam, AnyMap validData) {
return captchaInterceptor.beforeValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData);
}
private ApiResponse<?> afterValid(String id, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData, ApiResponse<?> basicValid) {
return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), imageCaptchaTrack, validData, basicValid);
private ApiResponse<?> afterValid(String id, MatchParam matchParam, AnyMap validData, ApiResponse<?> basicValid) {
return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData, basicValid);
}
}
@@ -10,6 +10,7 @@ import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
/**
* @Author: 天爱有情
@@ -51,8 +52,13 @@ public class FilterImageCaptchaApplication implements ImageCaptchaApplication {
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack ImageCaptchaTrack) {
return target.matching(id, ImageCaptchaTrack);
public ApiResponse<?> matching(String id, MatchParam matchParam) {
return target.matching(id, matchParam);
}
@Override
public ApiResponse<?> matching(String id, ImageCaptchaTrack track) {
return target.matching(id, track);
}
@Override
@@ -11,6 +11,7 @@ import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.resource.ImageCaptchaResourceManager;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
/**
* @Author: 天爱有情
@@ -64,13 +65,22 @@ public interface ImageCaptchaApplication {
* 匹配
*
* @param id 验证码的ID
* @param imageCaptchaTrack 滑动轨迹
* @param matchParam 匹配数据,包含鼠标轨迹,设备信息等
* @return 匹配成功返回true 否则返回false
*/
ApiResponse<?> matching(String id, ImageCaptchaTrack imageCaptchaTrack);
ApiResponse<?> matching(String id, MatchParam matchParam);
/**
* 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, ImageCaptchaTrack)}
* 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, MatchParam)}
*
* @param id 验证码的ID
* @param track 轨迹数据
* @return 匹配成功返回true 否则返回false
*/
ApiResponse<?> matching(String id, ImageCaptchaTrack track);
/**
* 兼容一下旧版本,新版本建议使用 {@link ImageCaptchaApplication#matching(String, MatchParam)}
*
* @param id id
* @param percentage 百分比数据
@@ -114,6 +124,7 @@ public interface ImageCaptchaApplication {
* @return CaptchaInterceptor
*/
CaptchaInterceptor getCaptchaInterceptor();
/**
* 设置 拦截器
*
@@ -1,5 +1,7 @@
package cloud.tianai.captcha.common;
import lombok.EqualsAndHashCode;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -8,6 +10,7 @@ import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
@EqualsAndHashCode
public class AnyMap implements Map<String, Object> {
private Map<String, Object> target;
@@ -19,6 +22,7 @@ public class AnyMap implements Map<String, Object> {
public AnyMap(Map<String, Object> map) {
this.target = map;
}
public Float getFloat(String key) {
return getFloat(key, null);
}
@@ -69,7 +73,7 @@ public class AnyMap implements Map<String, Object> {
}
public static AnyMap of(Map<String,Object> map) {
public static AnyMap of(Map<String, Object> map) {
return new AnyMap(map);
}
@@ -5,8 +5,7 @@ package cloud.tianai.captcha.common.exception;
* @date 2022/5/7 9:04
* @Description 图片验证码异常
*/
public class ImageCaptchaException extends RuntimeException {
public class ImageCaptchaException extends RuntimeException{
public ImageCaptchaException() {
}
@@ -25,5 +24,4 @@ public class ImageCaptchaException extends RuntimeException {
public ImageCaptchaException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -16,7 +16,6 @@ public class ApiResponse<T> implements Serializable {
public static final ApiResponse<?> SUCCESS;
static {
//默认
CodeDefinition definition = ApiResponseStatusConstant.SUCCESS;
SUCCESS = new ApiResponse(definition.getCode(), definition.getMessage(), null);
}
@@ -15,24 +15,12 @@ public interface ApiResponseStatusConstant {
*/
CodeDefinition SUCCESS = new CodeDefinition(200, "OK");
/**
* 无效参数
*/
CodeDefinition NOT_VALID_PARAM = new CodeDefinition(403, "无效参数");
/**
* 未知的内部错误
*/
CodeDefinition INTERNAL_SERVER_ERROR = new CodeDefinition(500, "未知的内部错误");
/**
* 已失效
*/
CodeDefinition EXPIRED = new CodeDefinition(4000, "已失效");
/**
* 基础校验失败
*/
CodeDefinition BASIC_CHECK_FAIL = new CodeDefinition(4001, "基础校验失败");
@@ -13,7 +13,8 @@ import lombok.*;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
// param作为扩展字段暂时将param从equals和toString中移除掉 以适应 CacheImageCaptchaGenerator
@EqualsAndHashCode(exclude = "param")
public class GenerateParam {
@@ -60,4 +61,5 @@ public class GenerateParam {
}
return param.getOrDefault(key, defaultValue);
}
}
@@ -36,7 +36,7 @@ public class RotateImageCaptchaInfo extends ImageCaptchaInfo {
rotateImageCaptchaInfo.setRandomX(randomX);
rotateImageCaptchaInfo.setBackgroundImage(backgroundImage);
rotateImageCaptchaInfo.setBackgroundImageTag(backgroundImageTag);
rotateImageCaptchaInfo.setTemplateImage(templateImageTag);
rotateImageCaptchaInfo.setTemplateImageTag(templateImageTag);
rotateImageCaptchaInfo.setTolerant(DEFAULT_TOLERANT);
rotateImageCaptchaInfo.setTemplateImage(templateImage);
rotateImageCaptchaInfo.setBackgroundImageWidth(bgImageWidth);
@@ -211,4 +211,5 @@ public class CacheImageCaptchaGenerator implements ImageCaptchaGenerator {
public void setInterceptor(CaptchaInterceptor interceptor) {
target.setInterceptor(interceptor);
}
}
@@ -8,7 +8,7 @@ import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
// ============================ 拦截器执行顺序 ============================
@@ -55,7 +55,7 @@ public interface CaptchaInterceptor {
default void afterGenerateCaptcha(Context context, String type, ImageCaptchaInfo imageCaptchaInfo, CaptchaResponse<ImageCaptchaVO> captchaResponse) {
}
default ApiResponse<?> beforeValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) {
default ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
Object preReturn = context.getPreReturnData();
if (preReturn != null) {
return (ApiResponse<?>) preReturn;
@@ -63,7 +63,7 @@ public interface CaptchaInterceptor {
return ApiResponse.ofSuccess();
}
default ApiResponse<?> afterValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData, ApiResponse<?> basicValid) {
default ApiResponse<?> afterValid(Context context, String type, MatchParam matchParam, AnyMap validData, ApiResponse<?> basicValid) {
Object preReturn = context.getPreReturnData();
if (preReturn != null) {
return (ApiResponse<?>) preReturn;
@@ -8,7 +8,7 @@ import cloud.tianai.captcha.generator.AbstractImageCaptchaGenerator;
import cloud.tianai.captcha.generator.common.model.dto.CaptchaExchange;
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
import lombok.Getter;
import lombok.Setter;
@@ -87,24 +87,24 @@ public class CaptchaInterceptorGroup implements CaptchaInterceptor {
}
@Override
public ApiResponse<?> beforeValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) {
public ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
context = createContextIfNecessary(context);
ApiResponse<?> beforeValid = null;
while (context.next() < context.getCount()) {
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
beforeValid = interceptor.beforeValid(context, type, imageCaptchaTrack, validData);
beforeValid = interceptor.beforeValid(context, type, matchParam, validData);
context.setPreReturnData(beforeValid);
}
return beforeValid == null ? ApiResponse.ofSuccess() : beforeValid;
}
@Override
public ApiResponse<?> afterValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData, ApiResponse<?> basicValid) {
public ApiResponse<?> afterValid(Context context, String type, MatchParam matchParam, AnyMap validData, ApiResponse<?> basicValid) {
context = createContextIfNecessary(context);
ApiResponse<?> valid = null;
while (context.next() < context.getCount()) {
CaptchaInterceptor interceptor = validators.get(context.getCurrent());
valid = interceptor.afterValid(context, type, imageCaptchaTrack, validData, basicValid);
valid = interceptor.afterValid(context, type, matchParam, validData, basicValid);
context.setPreReturnData(valid);
}
return valid == null ? ApiResponse.ofSuccess() : valid;
@@ -7,6 +7,7 @@ import cloud.tianai.captcha.common.util.CaptchaTypeClassifier;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.Context;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
import java.util.List;
@@ -24,14 +25,15 @@ public class BasicTrackCaptchaInterceptor implements CaptchaInterceptor {
}
@Override
public ApiResponse<?> afterValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData, ApiResponse<?> basicValid) {
public ApiResponse<?> afterValid(Context context, String type, MatchParam matchData, AnyMap validData, ApiResponse<?> basicValid) {
if (!basicValid.isSuccess()) {
return context.getGroup().afterValid(context, type, imageCaptchaTrack, validData, basicValid);
return context.getGroup().afterValid(context, type, matchData, validData, basicValid);
}
if (!CaptchaTypeClassifier.isSliderCaptcha(type)) {
// 不是滑动验证码的话暂时跳过,点选验证码行为轨迹还没做
return ApiResponse.ofSuccess();
}
ImageCaptchaTrack imageCaptchaTrack = matchData.getTrack();
// 进行行为轨迹检测
long startSlidingTime = imageCaptchaTrack.getStartTime().getTime();
long endSlidingTime = imageCaptchaTrack.getStopTime().getTime();
@@ -7,6 +7,7 @@ import cloud.tianai.captcha.common.util.ObjectUtils;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.Context;
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
/**
* @Author: 天爱有情
@@ -15,8 +16,8 @@ import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
*/
public class ParamCheckCaptchaInterceptor implements CaptchaInterceptor {
@Override
public ApiResponse<?> beforeValid(Context context, String type, ImageCaptchaTrack imageCaptchaTrack, AnyMap validData) {
checkParam(imageCaptchaTrack);
public ApiResponse<?> beforeValid(Context context, String type, MatchParam matchParam, AnyMap validData) {
checkParam(matchParam.getTrack());
return ApiResponse.ofSuccess();
}
@@ -10,7 +10,6 @@ import java.io.InputStream;
* @Description 抽象的ResourceProvider
*/
public abstract class AbstractResourceProvider implements ResourceProvider {
@Override
public InputStream getResourceInputStream(Resource data) {
InputStream resourceInputStream = doGetResourceInputStream(data);
@@ -27,5 +26,4 @@ public abstract class AbstractResourceProvider implements ResourceProvider {
* @return InputStream
*/
public abstract InputStream doGetResourceInputStream(Resource data);
}
@@ -16,7 +16,6 @@ import java.util.concurrent.ThreadLocalRandom;
* @Description 默认的资源存储
*/
public class LocalMemoryResourceStore implements ResourceStore {
private static final String TYPE_TAG_SPLIT_FLAG = "|";
/** 用于检索 type和tag. */
@@ -0,0 +1,18 @@
package cloud.tianai.captcha.validator.common.model.dto;
import lombok.Data;
@Data
public class Drives {
private Integer hardwareConcurrency;
private Boolean hasXhr = false;
private String href;
private String language;
private Long start;
private Long now;
private String platform;
private Integer scripts;
private String userAgent;
private Integer windowHeight;
private Integer windowWidth;
}
@@ -0,0 +1,31 @@
package cloud.tianai.captcha.validator.common.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: 天爱有情
* @date 2024/8/19 15:12
* @Description 验证码匹配的对象
*/
@Data
@NoArgsConstructor
public class MatchParam {
/** 轨迹信息. */
private ImageCaptchaTrack track;
/** 检测到的设备信息. */
private Drives drives;
/** 留一个扩展属性. */
private Object extendData;
public MatchParam(ImageCaptchaTrack track) {
this.track = track;
}
public MatchParam(ImageCaptchaTrack track, Drives drives) {
this.track = track;
this.drives = drives;
}
}
@@ -16,7 +16,6 @@ import java.util.List;
* @Description 基本的行为轨迹校验
*/
public class BasicCaptchaTrackValidator extends SimpleImageCaptchaValidator {
public static final CodeDefinition DEFINITION = new CodeDefinition(50001, "basic check fail");
public BasicCaptchaTrackValidator() {