Add configurable captcha endpoints

This commit is contained in:
Tim
2025-07-01 14:36:29 +08:00
parent 27d498cb06
commit 10fa61d3ed
8 changed files with 160 additions and 0 deletions

View File

@@ -46,6 +46,12 @@ OpenIsle 基于 Spring Boot 构建,提供社区后台常见的注册、登录
- `JWT_SECRET`JWT 签名密钥
- `JWT_EXPIRATION`JWT 过期时间(毫秒)
- `PASSWORD_STRENGTH`密码强度LOW、MEDIUM、HIGH
- `CAPTCHA_ENABLED`是否启用验证码true/false
- `RECAPTCHA_SECRET_KEY`Google reCAPTCHA 密钥
- `CAPTCHA_REGISTER_ENABLED`:注册是否需要验证码
- `CAPTCHA_LOGIN_ENABLED`:登录是否需要验证码
- `CAPTCHA_POST_ENABLED`:发帖是否需要验证码
- `CAPTCHA_COMMENT_ENABLED`:评论是否需要验证码
2. 启动项目:
```bash
@@ -56,6 +62,7 @@ mvn spring-boot:run
- `POST /api/auth/register`:注册新用户
- `POST /api/auth/login`:登录并获取 Token
- `GET /api/config`:查看验证码开关配置
- 需要认证的接口示例:`GET /api/hello`(需 `Authorization` 头)
- 管理员接口示例:`GET /api/admin/hello`

View File

@@ -4,10 +4,12 @@ import com.openisle.model.User;
import com.openisle.service.EmailSender;
import com.openisle.service.JwtService;
import com.openisle.service.UserService;
import com.openisle.service.CaptchaService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import java.util.Map;
import java.util.Optional;
@@ -18,9 +20,22 @@ public class AuthController {
private final UserService userService;
private final JwtService jwtService;
private final EmailSender emailService;
private final CaptchaService captchaService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@Value("${app.captcha.register-enabled:false}")
private boolean registerCaptchaEnabled;
@Value("${app.captcha.login-enabled:false}")
private boolean loginCaptchaEnabled;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
}
User user = userService.register(req.getUsername(), req.getEmail(), req.getPassword());
emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode());
return ResponseEntity.ok(Map.of("message", "Verification code sent"));
@@ -37,6 +52,9 @@ public class AuthController {
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
}
Optional<User> user = userService.authenticate(req.getUsername(), req.getPassword());
if (user.isPresent()) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
@@ -50,12 +68,14 @@ public class AuthController {
private String username;
private String email;
private String password;
private String captcha;
}
@Data
private static class LoginRequest {
private String username;
private String password;
private String captcha;
}
@Data

View File

@@ -2,9 +2,11 @@ package com.openisle.controller;
import com.openisle.model.Comment;
import com.openisle.service.CommentService;
import com.openisle.service.CaptchaService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@@ -17,11 +19,21 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class CommentController {
private final CommentService commentService;
private final CaptchaService captchaService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@Value("${app.captcha.comment-enabled:false}")
private boolean commentCaptchaEnabled;
@PostMapping("/posts/{postId}/comments")
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
@RequestBody CommentRequest req,
Authentication auth) {
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().build();
}
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
return ResponseEntity.ok(toDto(comment));
}
@@ -30,6 +42,9 @@ public class CommentController {
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
@RequestBody CommentRequest req,
Authentication auth) {
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().build();
}
Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent());
return ResponseEntity.ok(toDto(comment));
}
@@ -62,6 +77,7 @@ public class CommentController {
@Data
private static class CommentRequest {
private String content;
private String captcha;
}
@Data

View File

@@ -0,0 +1,47 @@
package com.openisle.controller;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ConfigController {
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@Value("${app.captcha.register-enabled:false}")
private boolean registerCaptchaEnabled;
@Value("${app.captcha.login-enabled:false}")
private boolean loginCaptchaEnabled;
@Value("${app.captcha.post-enabled:false}")
private boolean postCaptchaEnabled;
@Value("${app.captcha.comment-enabled:false}")
private boolean commentCaptchaEnabled;
@GetMapping("/config")
public ConfigResponse getConfig() {
ConfigResponse resp = new ConfigResponse();
resp.setCaptchaEnabled(captchaEnabled);
resp.setRegisterCaptchaEnabled(registerCaptchaEnabled);
resp.setLoginCaptchaEnabled(loginCaptchaEnabled);
resp.setPostCaptchaEnabled(postCaptchaEnabled);
resp.setCommentCaptchaEnabled(commentCaptchaEnabled);
return resp;
}
@Data
private static class ConfigResponse {
private boolean captchaEnabled;
private boolean registerCaptchaEnabled;
private boolean loginCaptchaEnabled;
private boolean postCaptchaEnabled;
private boolean commentCaptchaEnabled;
}
}

View File

@@ -6,11 +6,13 @@ import com.openisle.model.Reaction;
import com.openisle.service.CommentService;
import com.openisle.service.PostService;
import com.openisle.service.ReactionService;
import com.openisle.service.CaptchaService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import java.time.LocalDateTime;
import java.util.List;
@@ -23,9 +25,19 @@ public class PostController {
private final PostService postService;
private final CommentService commentService;
private final ReactionService reactionService;
private final CaptchaService captchaService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@Value("${app.captcha.post-enabled:false}")
private boolean postCaptchaEnabled;
@PostMapping
public ResponseEntity<PostDto> createPost(@RequestBody PostRequest req, Authentication auth) {
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().build();
}
Post post = postService.createPost(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent());
return ResponseEntity.ok(toDto(post));
}
@@ -110,6 +122,7 @@ public class PostController {
private Long categoryId;
private String title;
private String content;
private String captcha;
}
@Data

View File

@@ -0,0 +1,14 @@
package com.openisle.service;
/**
* Abstract service for verifying CAPTCHA tokens.
*/
public abstract class CaptchaService {
/**
* Verify the CAPTCHA token sent from client.
*
* @param token CAPTCHA token
* @return true if token is valid
*/
public abstract boolean verify(String token);
}

View File

@@ -0,0 +1,35 @@
package com.openisle.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* CaptchaService implementation using Google reCAPTCHA.
*/
@Service
public class RecaptchaService extends CaptchaService {
@Value("${recaptcha.secret-key:}")
private String secretKey;
private final RestTemplate restTemplate = new RestTemplate();
@Override
public boolean verify(String token) {
if (token == null || token.isEmpty()) {
return false;
}
String url = "https://www.google.com/recaptcha/api/siteverify?secret={secret}&response={response}";
try {
ResponseEntity<Map> resp = restTemplate.postForEntity(url, null, Map.class, secretKey, token);
Map body = resp.getBody();
return body != null && Boolean.TRUE.equals(body.get("success"));
} catch (Exception e) {
return false;
}
}
}

View File

@@ -21,6 +21,14 @@ app.upload.max-size=${UPLOAD_MAX_SIZE:5242880}
app.user.posts-limit=${USER_POSTS_LIMIT:10}
app.user.replies-limit=${USER_REPLIES_LIMIT:50}
# Captcha configuration
app.captcha.enabled=${CAPTCHA_ENABLED:false}
recaptcha.secret-key=${RECAPTCHA_SECRET_KEY:}
app.captcha.register-enabled=${CAPTCHA_REGISTER_ENABLED:false}
app.captcha.login-enabled=${CAPTCHA_LOGIN_ENABLED:false}
app.captcha.post-enabled=${CAPTCHA_POST_ENABLED:false}
app.captcha.comment-enabled=${CAPTCHA_COMMENT_ENABLED:false}
# ========= Optional =========
# for resend email send service, you can improve your service by yourself
resend.api.key=${RESEND_API_KEY:}