Skip to content

Commit 8cac6ba

Browse files
authored
Enhance captcha functionality (#182)
- ## 算术验证码支持自定义运算范围 <img width="300" height="auto" alt="image" src="https://github.com/user-attachments/assets/e450cef0-a12a-40ec-bbe6-c600ecec2afb" /> - ## 字母数字组合验证支持是否忽略验证码大小写验证、自定义验证码长度 <img width="300" height="auto" alt="image" src="https://github.com/user-attachments/assets/ba1cc5e7-4ca2-4c11-aa2e-a4372ad7abe3" /> Fixes #179 ```release-note 增强验证码功能并优化配置 ```
1 parent d2171dc commit 8cac6ba

File tree

7 files changed

+67
-27
lines changed

7 files changed

+67
-27
lines changed

src/main/java/run/halo/comment/widget/SettingConfigGetter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ class CaptchaConfig {
5252
@Getter(onMethod_ = @NonNull)
5353
private CaptchaType type = CaptchaType.ALPHANUMERIC;
5454

55+
private boolean ignoreCase = true;
56+
57+
private int captchaLength = 4;
58+
59+
private int arithmeticRange = 90;
60+
5561
public CaptchaConfig setType(CaptchaType type) {
5662
this.type = (type == null ? CaptchaType.ALPHANUMERIC : type);
5763
return this;

src/main/java/run/halo/comment/widget/captcha/CaptchaEndpoint.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public RouterFunction<ServerResponse> endpoint() {
2828
private Mono<ServerResponse> generateCaptcha(ServerRequest request) {
2929
return settingConfigGetter.getSecurityConfig()
3030
.map(SettingConfigGetter.SecurityConfig::getCaptcha)
31-
.flatMap(captchaConfig -> captchaManager.generate(request.exchange(), captchaConfig.getType()))
31+
.flatMap(captchaConfig -> captchaManager.generate(request.exchange(), captchaConfig))
3232
.flatMap(captcha -> ServerResponse.ok().bodyValue(captcha.imageBase64()));
3333
}
3434

src/main/java/run/halo/comment/widget/captcha/CaptchaGenerator.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ public class CaptchaGenerator {
2626
customFont = loadArialFont();
2727
}
2828

29-
public static Captcha generateMathCaptcha() {
30-
return generateCaptchaImage(CaptchaGenerator::drawMathCaptchaText);
29+
public static Captcha generateMathCaptcha(int arithmeticRange) {
30+
return generateCaptchaImage((g2d) -> drawMathCaptchaText(g2d, arithmeticRange));
3131
}
3232

33-
public static Captcha generateSimpleCaptcha() {
34-
return generateCaptchaImage(CaptchaGenerator::drawSimpleText);
33+
public static Captcha generateSimpleCaptcha(int captchaLength) {
34+
return generateCaptchaImage((g2d) -> drawSimpleText(g2d, captchaLength));
3535
}
3636

3737
private static Captcha generateCaptchaImage(Function<Graphics2D, String> drawCaptchaTextFunc) {
@@ -61,10 +61,10 @@ private static Captcha generateCaptchaImage(Function<Graphics2D, String> drawCap
6161
return new Captcha(captchaText, bufferedImage);
6262
}
6363

64-
private static String drawMathCaptchaText(Graphics2D g2d) {
64+
private static String drawMathCaptchaText(Graphics2D g2d, int arithmeticRange) {
6565
Random random = new Random();
66-
int num1 = random.nextInt(90) + 1;
67-
int num2 = random.nextInt(90) + 1;
66+
int num1 = random.nextInt(arithmeticRange) + 1;
67+
int num2 = random.nextInt(arithmeticRange) + 1;
6868
char operator = getRandomOperator();
6969

7070
int result;
@@ -92,12 +92,15 @@ private static String drawMathCaptchaText(Graphics2D g2d) {
9292
public record Captcha(String code, BufferedImage image) {
9393
}
9494

95-
private static String drawSimpleText(Graphics2D g2d) {
96-
var captchaText = generateRandomText();
95+
private static String drawSimpleText(Graphics2D g2d, int captchaLength) {
96+
var captchaText = generateRandomText(captchaLength);
9797
Random random = new Random();
98+
int charSpacing = WIDTH / (captchaLength + 1);
9899
for (int i = 0; i < captchaText.length(); i++) {
99100
g2d.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
100-
g2d.drawString(String.valueOf(captchaText.charAt(i)), 20 + i * 24, 30);
101+
// 动态计算每个字符的位置
102+
int xPos = charSpacing + i * charSpacing;
103+
g2d.drawString(String.valueOf(captchaText.charAt(i)), xPos, 30);
101104
}
102105
return captchaText;
103106
}
@@ -121,10 +124,10 @@ private static char getRandomOperator() {
121124
return operators[random.nextInt(operators.length)];
122125
}
123126

124-
private static String generateRandomText() {
125-
StringBuilder sb = new StringBuilder(CHAR_LENGTH);
127+
private static String generateRandomText(int captchaLength) {
128+
StringBuilder sb = new StringBuilder(captchaLength);
126129
Random random = new Random();
127-
for (int i = 0; i < CHAR_LENGTH; i++) {
130+
for (int i = 0; i < captchaLength; i++) {
128131
sb.append(CHAR_STRING.charAt(random.nextInt(CHAR_STRING.length())));
129132
}
130133
return sb.toString();

src/main/java/run/halo/comment/widget/captcha/CaptchaManager.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import org.springframework.web.server.ServerWebExchange;
44
import reactor.core.publisher.Mono;
5+
import run.halo.comment.widget.SettingConfigGetter;
56

67
public interface CaptchaManager {
7-
Mono<Boolean> verify(String id, String captchaCode);
8+
Mono<Boolean> verify(String id, String captchaCode, boolean ignoreCase);
89

910
Mono<Void> invalidate(String id);
1011

11-
Mono<Captcha> generate(ServerWebExchange exchange, CaptchaType type);
12+
Mono<Captcha> generate(ServerWebExchange exchange, SettingConfigGetter.CaptchaConfig captchaConfig);
1213

1314
record Captcha(String id, String code, String imageBase64) {
1415
}

src/main/java/run/halo/comment/widget/captcha/CaptchaManagerImpl.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.web.server.ServerWebExchange;
1111
import reactor.core.publisher.Mono;
1212
import reactor.core.scheduler.Schedulers;
13+
import run.halo.comment.widget.SettingConfigGetter;
1314

1415
@Component
1516
@RequiredArgsConstructor
@@ -25,9 +26,9 @@ public class CaptchaManagerImpl implements CaptchaManager {
2526
private final CaptchaCookieResolver captchaCookieResolver;
2627

2728
@Override
28-
public Mono<Boolean> verify(String key, String captchaCode) {
29+
public Mono<Boolean> verify(String key, String captchaCode, boolean ignoreCase) {
2930
return Mono.justOrEmpty(captchaCache.getIfPresent(key))
30-
.filter(captcha -> captcha.code().equalsIgnoreCase(captchaCode))
31+
.filter(captcha -> ignoreCase ? captcha.code().equalsIgnoreCase(captchaCode) : captcha.code().equals(captchaCode))
3132
.hasElement();
3233
}
3334

@@ -38,16 +39,16 @@ public Mono<Void> invalidate(String id) {
3839
}
3940

4041
@Override
41-
public Mono<Captcha> generate(ServerWebExchange exchange, CaptchaType type) {
42-
return doGenerate(type)
42+
public Mono<Captcha> generate(ServerWebExchange exchange, SettingConfigGetter.CaptchaConfig captchaConfig) {
43+
return doGenerate(captchaConfig)
4344
.doOnNext(captcha -> captchaCookieResolver.setCookie(exchange, captcha.id()));
4445
}
4546

46-
private Mono<Captcha> doGenerate(CaptchaType type) {
47+
private Mono<Captcha> doGenerate(SettingConfigGetter.CaptchaConfig captchaConfig) {
4748
return Mono.fromSupplier(() -> {
48-
var captcha = switch (type) {
49-
case ALPHANUMERIC -> CaptchaGenerator.generateSimpleCaptcha();
50-
case ARITHMETIC -> CaptchaGenerator.generateMathCaptcha();
49+
var captcha = switch (captchaConfig.getType()) {
50+
case ALPHANUMERIC -> CaptchaGenerator.generateSimpleCaptcha(captchaConfig.getCaptchaLength());
51+
case ARITHMETIC -> CaptchaGenerator.generateMathCaptcha(captchaConfig.getArithmeticRange());
5152
};
5253
var imageBase64 = encodeBufferedImageToDataUri(captcha.image());
5354
var id = UUID.randomUUID().toString();

src/main/java/run/halo/comment/widget/captcha/CommentCaptchaFilter.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,9 @@ public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilter
6565
private Mono<Void> sendCaptchaRequiredResponse(ServerWebExchange exchange,
6666
SettingConfigGetter.CaptchaConfig captchaConfig,
6767
ResponseStatusException e) {
68-
var type = captchaConfig.getType();
6968
exchange.getResponse().getHeaders().addIfAbsent(CAPTCHA_REQUIRED_HEADER, "true");
7069
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
71-
return captchaManager.generate(exchange, type)
70+
return captchaManager.generate(exchange, captchaConfig)
7271
.flatMap(captcha -> {
7372
var problemDetail = toProblemDetail(e);
7473
problemDetail.setProperty("captcha", captcha.imageBase64());
@@ -94,7 +93,7 @@ private Mono<Void> validateCaptcha(ServerWebExchange exchange, WebFilterChain ch
9493
if (captchaCodeOpt.isEmpty() || cookie == null) {
9594
return sendCaptchaRequiredResponse(exchange, captchaConfig, new CaptchaCodeMissingException());
9695
}
97-
return captchaManager.verify(cookie.getValue(), captchaCodeOpt.get())
96+
return captchaManager.verify(cookie.getValue(), captchaCodeOpt.get(), captchaConfig.isIgnoreCase())
9897
.flatMap(valid -> {
9998
if (valid) {
10099
captchaCookieResolver.expireCookie(exchange);

src/main/resources/extensions/settings.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,42 @@ spec:
5151
label: 验证码类型
5252
if: "$get(anonymousCommentCaptcha).value === true"
5353
name: type
54+
id: type
5455
value: "ALPHANUMERIC"
5556
options:
5657
- label: 字母数字组合
5758
value: "ALPHANUMERIC"
5859
- label: 算术验证码
5960
value: "ARITHMETIC"
61+
- $formkit: number
62+
if: "$get(type).value === ALPHANUMERIC"
63+
name: captchaLength
64+
key: captchaLength
65+
label: 验证码长度
66+
help: 字母数字组合验证码长度,不宜过长或过短,建议4-6位
67+
max: 8
68+
value: 4
69+
validation: required
70+
- $formkit: radio
71+
if: "$get(type).value === ALPHANUMERIC"
72+
name: ignoreCase
73+
key: ignoreCase
74+
label: 是否忽略验证码大小写验证
75+
help: 忽略大小写验证码,建议开启
76+
value: true
77+
options:
78+
- label:
79+
value: true
80+
- label:
81+
value: false
82+
- $formkit: number
83+
if: "$get(type).value === ARITHMETIC"
84+
name: arithmeticRange
85+
key: arithmeticRange
86+
label: 计算范围
87+
help: 算术验证码计算范围
88+
value: 90
89+
validation: required
6090
- group: avatar
6191
label: 头像设置
6292
formSchema:

0 commit comments

Comments
 (0)