mirror of
https://github.com/dromara/tianai-captcha.git
synced 2026-05-07 06:04:34 +08:00
feat(captcha): 升级验证码校验流程与资源管理
- 引入MatchParam封装滑动轨迹及相关信息,提升扩展性。- 调整验证码生成与校验接口,以支持更精细的参数控制。 - 优化资源管理,提高验证码资源的加载效率。 - 更新文档与示例代码,以反映API的最新变化。BREAKING CHANGE: 验证码校验接口发生改变,现在需要传入MatchParam对象而非直接传入轨迹对象。这可能会影响直接调用验证码校验服务的客户端代码,需根据最新API文档进行适配。
This commit is contained in:
@@ -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: 天爱有情
|
||||
@@ -63,14 +64,23 @@ public interface ImageCaptchaApplication {
|
||||
/**
|
||||
* 匹配
|
||||
*
|
||||
* @param id 验证码的ID
|
||||
* @param imageCaptchaTrack 滑动轨迹
|
||||
* @param id 验证码的ID
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface CommonConstant {
|
||||
/**
|
||||
* 默认的resource资源文件路径.
|
||||
*/
|
||||
String DEFAULT_SLIDER_IMAGE_RESOURCE_PATH = "META-INF/cut-image/resource";
|
||||
String DEFAULT_SLIDER_IMAGE_RESOURCE_PATH = "META-INF/cut-image/resource";
|
||||
/**
|
||||
* 默认的template资源文件路径.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+4
-2
@@ -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();
|
||||
|
||||
+3
-2
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user