From 0b331c793d2efcf6e88ec8d2924d200e198ac1df Mon Sep 17 00:00:00 2001 From: liuyiwuqing <1520431201@qq.com> Date: Thu, 24 Jul 2025 18:42:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(captcha):=20=E5=A2=9E=E5=BC=BA=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 算术验证码支持自定义运算范围 - 字母数字组合验证支持是否忽略验证码大小写验证、自定义验证码长度 --- .../comment/widget/SettingConfigGetter.java | 6 ++++ .../widget/captcha/CaptchaEndpoint.java | 2 +- .../widget/captcha/CaptchaGenerator.java | 29 ++++++++++-------- .../widget/captcha/CaptchaManager.java | 5 ++-- .../widget/captcha/CaptchaManagerImpl.java | 17 ++++++----- .../widget/captcha/CommentCaptchaFilter.java | 5 ++-- src/main/resources/extensions/settings.yaml | 30 +++++++++++++++++++ 7 files changed, 67 insertions(+), 27 deletions(-) diff --git a/src/main/java/run/halo/comment/widget/SettingConfigGetter.java b/src/main/java/run/halo/comment/widget/SettingConfigGetter.java index 257bcc6..7262892 100644 --- a/src/main/java/run/halo/comment/widget/SettingConfigGetter.java +++ b/src/main/java/run/halo/comment/widget/SettingConfigGetter.java @@ -52,6 +52,12 @@ class CaptchaConfig { @Getter(onMethod_ = @NonNull) private CaptchaType type = CaptchaType.ALPHANUMERIC; + private boolean ignoreCase = true; + + private int captchaLength = 4; + + private int arithmeticRange = 90; + public CaptchaConfig setType(CaptchaType type) { this.type = (type == null ? CaptchaType.ALPHANUMERIC : type); return this; diff --git a/src/main/java/run/halo/comment/widget/captcha/CaptchaEndpoint.java b/src/main/java/run/halo/comment/widget/captcha/CaptchaEndpoint.java index 18edb95..35ce325 100644 --- a/src/main/java/run/halo/comment/widget/captcha/CaptchaEndpoint.java +++ b/src/main/java/run/halo/comment/widget/captcha/CaptchaEndpoint.java @@ -28,7 +28,7 @@ public RouterFunction endpoint() { private Mono generateCaptcha(ServerRequest request) { return settingConfigGetter.getSecurityConfig() .map(SettingConfigGetter.SecurityConfig::getCaptcha) - .flatMap(captchaConfig -> captchaManager.generate(request.exchange(), captchaConfig.getType())) + .flatMap(captchaConfig -> captchaManager.generate(request.exchange(), captchaConfig)) .flatMap(captcha -> ServerResponse.ok().bodyValue(captcha.imageBase64())); } diff --git a/src/main/java/run/halo/comment/widget/captcha/CaptchaGenerator.java b/src/main/java/run/halo/comment/widget/captcha/CaptchaGenerator.java index ea5d651..cf626c8 100644 --- a/src/main/java/run/halo/comment/widget/captcha/CaptchaGenerator.java +++ b/src/main/java/run/halo/comment/widget/captcha/CaptchaGenerator.java @@ -26,12 +26,12 @@ public class CaptchaGenerator { customFont = loadArialFont(); } - public static Captcha generateMathCaptcha() { - return generateCaptchaImage(CaptchaGenerator::drawMathCaptchaText); + public static Captcha generateMathCaptcha(int arithmeticRange) { + return generateCaptchaImage((g2d) -> drawMathCaptchaText(g2d, arithmeticRange)); } - public static Captcha generateSimpleCaptcha() { - return generateCaptchaImage(CaptchaGenerator::drawSimpleText); + public static Captcha generateSimpleCaptcha(int captchaLength) { + return generateCaptchaImage((g2d) -> drawSimpleText(g2d, captchaLength)); } private static Captcha generateCaptchaImage(Function drawCaptchaTextFunc) { @@ -61,10 +61,10 @@ private static Captcha generateCaptchaImage(Function drawCap return new Captcha(captchaText, bufferedImage); } - private static String drawMathCaptchaText(Graphics2D g2d) { + private static String drawMathCaptchaText(Graphics2D g2d, int arithmeticRange) { Random random = new Random(); - int num1 = random.nextInt(90) + 1; - int num2 = random.nextInt(90) + 1; + int num1 = random.nextInt(arithmeticRange) + 1; + int num2 = random.nextInt(arithmeticRange) + 1; char operator = getRandomOperator(); int result; @@ -92,12 +92,15 @@ private static String drawMathCaptchaText(Graphics2D g2d) { public record Captcha(String code, BufferedImage image) { } - private static String drawSimpleText(Graphics2D g2d) { - var captchaText = generateRandomText(); + private static String drawSimpleText(Graphics2D g2d, int captchaLength) { + var captchaText = generateRandomText(captchaLength); Random random = new Random(); + int charSpacing = WIDTH / (captchaLength + 1); for (int i = 0; i < captchaText.length(); i++) { g2d.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256))); - g2d.drawString(String.valueOf(captchaText.charAt(i)), 20 + i * 24, 30); + // 动态计算每个字符的位置 + int xPos = charSpacing + i * charSpacing; + g2d.drawString(String.valueOf(captchaText.charAt(i)), xPos, 30); } return captchaText; } @@ -121,10 +124,10 @@ private static char getRandomOperator() { return operators[random.nextInt(operators.length)]; } - private static String generateRandomText() { - StringBuilder sb = new StringBuilder(CHAR_LENGTH); + private static String generateRandomText(int captchaLength) { + StringBuilder sb = new StringBuilder(captchaLength); Random random = new Random(); - for (int i = 0; i < CHAR_LENGTH; i++) { + for (int i = 0; i < captchaLength; i++) { sb.append(CHAR_STRING.charAt(random.nextInt(CHAR_STRING.length()))); } return sb.toString(); diff --git a/src/main/java/run/halo/comment/widget/captcha/CaptchaManager.java b/src/main/java/run/halo/comment/widget/captcha/CaptchaManager.java index 6a6ab10..93216ad 100644 --- a/src/main/java/run/halo/comment/widget/captcha/CaptchaManager.java +++ b/src/main/java/run/halo/comment/widget/captcha/CaptchaManager.java @@ -2,13 +2,14 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import run.halo.comment.widget.SettingConfigGetter; public interface CaptchaManager { - Mono verify(String id, String captchaCode); + Mono verify(String id, String captchaCode, boolean ignoreCase); Mono invalidate(String id); - Mono generate(ServerWebExchange exchange, CaptchaType type); + Mono generate(ServerWebExchange exchange, SettingConfigGetter.CaptchaConfig captchaConfig); record Captcha(String id, String code, String imageBase64) { } diff --git a/src/main/java/run/halo/comment/widget/captcha/CaptchaManagerImpl.java b/src/main/java/run/halo/comment/widget/captcha/CaptchaManagerImpl.java index 7c20373..b4060cc 100644 --- a/src/main/java/run/halo/comment/widget/captcha/CaptchaManagerImpl.java +++ b/src/main/java/run/halo/comment/widget/captcha/CaptchaManagerImpl.java @@ -10,6 +10,7 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import run.halo.comment.widget.SettingConfigGetter; @Component @RequiredArgsConstructor @@ -25,9 +26,9 @@ public class CaptchaManagerImpl implements CaptchaManager { private final CaptchaCookieResolver captchaCookieResolver; @Override - public Mono verify(String key, String captchaCode) { + public Mono verify(String key, String captchaCode, boolean ignoreCase) { return Mono.justOrEmpty(captchaCache.getIfPresent(key)) - .filter(captcha -> captcha.code().equalsIgnoreCase(captchaCode)) + .filter(captcha -> ignoreCase ? captcha.code().equalsIgnoreCase(captchaCode) : captcha.code().equals(captchaCode)) .hasElement(); } @@ -38,16 +39,16 @@ public Mono invalidate(String id) { } @Override - public Mono generate(ServerWebExchange exchange, CaptchaType type) { - return doGenerate(type) + public Mono generate(ServerWebExchange exchange, SettingConfigGetter.CaptchaConfig captchaConfig) { + return doGenerate(captchaConfig) .doOnNext(captcha -> captchaCookieResolver.setCookie(exchange, captcha.id())); } - private Mono doGenerate(CaptchaType type) { + private Mono doGenerate(SettingConfigGetter.CaptchaConfig captchaConfig) { return Mono.fromSupplier(() -> { - var captcha = switch (type) { - case ALPHANUMERIC -> CaptchaGenerator.generateSimpleCaptcha(); - case ARITHMETIC -> CaptchaGenerator.generateMathCaptcha(); + var captcha = switch (captchaConfig.getType()) { + case ALPHANUMERIC -> CaptchaGenerator.generateSimpleCaptcha(captchaConfig.getCaptchaLength()); + case ARITHMETIC -> CaptchaGenerator.generateMathCaptcha(captchaConfig.getArithmeticRange()); }; var imageBase64 = encodeBufferedImageToDataUri(captcha.image()); var id = UUID.randomUUID().toString(); diff --git a/src/main/java/run/halo/comment/widget/captcha/CommentCaptchaFilter.java b/src/main/java/run/halo/comment/widget/captcha/CommentCaptchaFilter.java index 84e5217..62020e1 100644 --- a/src/main/java/run/halo/comment/widget/captcha/CommentCaptchaFilter.java +++ b/src/main/java/run/halo/comment/widget/captcha/CommentCaptchaFilter.java @@ -65,10 +65,9 @@ public Mono filter(@NonNull ServerWebExchange exchange, @NonNull WebFilter private Mono sendCaptchaRequiredResponse(ServerWebExchange exchange, SettingConfigGetter.CaptchaConfig captchaConfig, ResponseStatusException e) { - var type = captchaConfig.getType(); exchange.getResponse().getHeaders().addIfAbsent(CAPTCHA_REQUIRED_HEADER, "true"); exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); - return captchaManager.generate(exchange, type) + return captchaManager.generate(exchange, captchaConfig) .flatMap(captcha -> { var problemDetail = toProblemDetail(e); problemDetail.setProperty("captcha", captcha.imageBase64()); @@ -94,7 +93,7 @@ private Mono validateCaptcha(ServerWebExchange exchange, WebFilterChain ch if (captchaCodeOpt.isEmpty() || cookie == null) { return sendCaptchaRequiredResponse(exchange, captchaConfig, new CaptchaCodeMissingException()); } - return captchaManager.verify(cookie.getValue(), captchaCodeOpt.get()) + return captchaManager.verify(cookie.getValue(), captchaCodeOpt.get(), captchaConfig.isIgnoreCase()) .flatMap(valid -> { if (valid) { captchaCookieResolver.expireCookie(exchange); diff --git a/src/main/resources/extensions/settings.yaml b/src/main/resources/extensions/settings.yaml index b812553..d2dbf66 100644 --- a/src/main/resources/extensions/settings.yaml +++ b/src/main/resources/extensions/settings.yaml @@ -51,12 +51,42 @@ spec: label: 验证码类型 if: "$get(anonymousCommentCaptcha).value === true" name: type + id: type value: "ALPHANUMERIC" options: - label: 字母数字组合 value: "ALPHANUMERIC" - label: 算术验证码 value: "ARITHMETIC" + - $formkit: number + if: "$get(type).value === ALPHANUMERIC" + name: captchaLength + key: captchaLength + label: 验证码长度 + help: 字母数字组合验证码长度,不宜过长或过短,建议4-6位 + max: 8 + value: 4 + validation: required + - $formkit: radio + if: "$get(type).value === ALPHANUMERIC" + name: ignoreCase + key: ignoreCase + label: 是否忽略验证码大小写验证 + help: 忽略大小写验证码,建议开启 + value: true + options: + - label: 是 + value: true + - label: 否 + value: false + - $formkit: number + if: "$get(type).value === ARITHMETIC" + name: arithmeticRange + key: arithmeticRange + label: 计算范围 + help: 算术验证码计算范围 + value: 90 + validation: required - group: avatar label: 头像设置 formSchema: