diff --git a/README.md b/README.md
index 485034132..8d6bdecdd 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ OpenIsle 基于 Spring Boot 构建,提供社区后台常见的注册、登录
* **用户体系**:注册、登录,密码使用 BCrypt 加密
* **JWT 认证**:登录后获得 Token,接口通过 `Authorization: Bearer` 认证
+* **Google 登录**:支持使用 Google OAuth 登录
* **邮件通知**:抽象 `EmailSender`,默认实现基于 Resend
* **角色权限**:内置 `ADMIN` 与 `USER`,管理员接口以 `/api/admin/**` 提供
* **文章与评论**:支持分类、评论及多级回复
@@ -43,6 +44,7 @@ OpenIsle 基于 Spring Boot 构建,提供社区后台常见的注册、登录
- `MYSQL_PASSWORD`:数据库密码
- `RESEND_API_KEY`:Resend 邮件服务 API Key
- `COS_BASE_URL`:腾讯云 COS 访问域名
+ - `GOOGLE_CLIENT_ID`:Google OAuth 客户端 ID
- `JWT_SECRET`:JWT 签名密钥
- `JWT_EXPIRATION`:JWT 过期时间(毫秒)
- `PASSWORD_STRENGTH`:密码强度(LOW、MEDIUM、HIGH)
@@ -62,6 +64,7 @@ mvn spring-boot:run
- `POST /api/auth/register`:注册新用户
- `POST /api/auth/login`:登录并获取 Token
+- `POST /api/auth/google`:Google 登录并获取 Token
- `GET /api/config`:查看验证码开关配置
- 需要认证的接口示例:`GET /api/hello`(需 `Authorization` 头)
- 管理员接口示例:`GET /api/admin/hello`
diff --git a/pom.xml b/pom.xml
index 15ee53e6e..d63ae26f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,11 @@
lombok
true
+
+ com.google.api-client
+ google-api-client
+ 2.2.0
+
com.qcloud
cos_api
diff --git a/src/main/java/com/openisle/controller/AuthController.java b/src/main/java/com/openisle/controller/AuthController.java
index 3483d60dc..c3abd2ae8 100644
--- a/src/main/java/com/openisle/controller/AuthController.java
+++ b/src/main/java/com/openisle/controller/AuthController.java
@@ -5,6 +5,7 @@ import com.openisle.service.EmailSender;
import com.openisle.service.JwtService;
import com.openisle.service.UserService;
import com.openisle.service.CaptchaService;
+import com.openisle.service.GoogleAuthService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
@@ -21,6 +22,7 @@ public class AuthController {
private final JwtService jwtService;
private final EmailSender emailService;
private final CaptchaService captchaService;
+ private final GoogleAuthService googleAuthService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@@ -63,6 +65,15 @@ public class AuthController {
}
}
+ @PostMapping("/google")
+ public ResponseEntity> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
+ Optional user = googleAuthService.authenticate(req.getIdToken());
+ if (user.isPresent()) {
+ return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
+ }
+ return ResponseEntity.badRequest().body(Map.of("error", "Invalid google token"));
+ }
+
@GetMapping("/check")
public ResponseEntity> checkToken() {
return ResponseEntity.ok(Map.of("valid", true));
@@ -83,6 +94,11 @@ public class AuthController {
private String captcha;
}
+ @Data
+ private static class GoogleLoginRequest {
+ private String idToken;
+ }
+
@Data
private static class VerifyRequest {
private String username;
diff --git a/src/main/java/com/openisle/service/GoogleAuthService.java b/src/main/java/com/openisle/service/GoogleAuthService.java
new file mode 100644
index 000000000..2e7cffe87
--- /dev/null
+++ b/src/main/java/com/openisle/service/GoogleAuthService.java
@@ -0,0 +1,69 @@
+package com.openisle.service;
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.openisle.model.Role;
+import com.openisle.model.User;
+import com.openisle.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class GoogleAuthService {
+
+ private final UserRepository userRepository;
+
+ @Value("${google.client-id:}")
+ private String clientId;
+
+ public Optional authenticate(String idTokenString) {
+ GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
+ .setAudience(Collections.singletonList(clientId))
+ .build();
+ try {
+ GoogleIdToken idToken = verifier.verify(idTokenString);
+ if (idToken == null) {
+ return Optional.empty();
+ }
+ GoogleIdToken.Payload payload = idToken.getPayload();
+ String email = payload.getEmail();
+ String name = (String) payload.get("name");
+ return Optional.of(processUser(email, name));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ private User processUser(String email, String name) {
+ Optional existing = userRepository.findByEmail(email);
+ if (existing.isPresent()) {
+ User user = existing.get();
+ if (!user.isVerified()) {
+ user.setVerified(true);
+ user.setVerificationCode(null);
+ userRepository.save(user);
+ }
+ return user;
+ }
+ User user = new User();
+ String baseUsername = email.split("@")[0];
+ String username = baseUsername;
+ int suffix = 1;
+ while (userRepository.findByUsername(username).isPresent()) {
+ username = baseUsername + suffix++;
+ }
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setPassword("");
+ user.setRole(Role.USER);
+ user.setVerified(true);
+ return userRepository.save(user);
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 15eb1dd38..f4d76b89d 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -41,3 +41,6 @@ cos.secret-key=${COS_SECRET_KEY:}
cos.region=${COS_REGION:ap-guangzhou}
cos.bucket-name=${COS_BUCKET_NAME:}
# your image upload services: ...
+
+# Google OAuth configuration
+google.client-id=${GOOGLE_CLIENT_ID:}