diff --git a/Dockerfile b/Dockerfile
index b3c3b3d..40258ed 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# ⭐️ 关键步骤:选择一个基于 JDK 21 的镜像
# openjdk:21-jdk-slim 是一个较小的选择
-FROM openjdk:21-jdk-slim
+FROM openjdk:21-ea-21-jdk-slim
# 设置工作目录
WORKDIR /app
diff --git a/pom.xml b/pom.xml
index e8faec2..4ee1f44 100644
--- a/pom.xml
+++ b/pom.xml
@@ -193,6 +193,26 @@
3.1.572
+
+
+ com.google.api-client
+ google-api-client
+ 2.2.0
+
+
+
+
+ com.google.http-client
+ google-http-client-jackson2
+ 1.42.3
+
+
+
+ com.nimbusds
+ nimbus-jose-jwt
+ 9.37
+
+
diff --git a/src/main/java/com/aida/lanecarford/config/WebConfig.java b/src/main/java/com/aida/lanecarford/config/WebConfig.java
index c034a95..a3e3d73 100644
--- a/src/main/java/com/aida/lanecarford/config/WebConfig.java
+++ b/src/main/java/com/aida/lanecarford/config/WebConfig.java
@@ -40,7 +40,7 @@ public class WebConfig implements WebMvcConfigurer {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**/**") // 保护这些路径
.excludePathPatterns(Arrays.asList("/api/auth/precheckEmail", "/api/auth/registerOrLogin",
- "/api/auth/forgotPwd", "/api/style/callback")); // 排除登录接口
+ "/api/auth/forgotPwd", "/api/style/callback", "/api/auth/parseGoogleCredential")); // 排除登录接口
}
/**
diff --git a/src/main/java/com/aida/lanecarford/controller/LoginController.java b/src/main/java/com/aida/lanecarford/controller/LoginController.java
index d856d41..3b06cba 100644
--- a/src/main/java/com/aida/lanecarford/controller/LoginController.java
+++ b/src/main/java/com/aida/lanecarford/controller/LoginController.java
@@ -5,6 +5,7 @@ import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.entity.User;
import com.aida.lanecarford.service.LoginService;
import com.aida.lanecarford.vo.LoginVO;
+import com.nimbusds.jose.JOSEException;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -12,6 +13,9 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
+import java.io.IOException;
+import java.text.ParseException;
+
/**
* 用户认证控制器
*/
@@ -96,4 +100,16 @@ public class LoginController {
return ApiResponse.success(loginService.getUserInfo());
}
+ /**
+ * Google One Tap/前端直接获取凭证
+ * 前端使用Google Identity Services直接获取credential(JWT token)
+ * 前端直接将credential传给服务端验证
+ * 服务端直接解析和验证JWT,无需与Google服务器交换
+ */
+ @CrossOrigin
+ @GetMapping("/parseGoogleCredential")
+ public ApiResponse parseGoogleCredential(@RequestParam("credential") String credential) throws ParseException, IOException, JOSEException {
+ return ApiResponse.success(loginService.parseGoogleCredential(credential));
+ }
+
}
diff --git a/src/main/java/com/aida/lanecarford/service/LoginService.java b/src/main/java/com/aida/lanecarford/service/LoginService.java
index 4ddefb5..8c0e07a 100644
--- a/src/main/java/com/aida/lanecarford/service/LoginService.java
+++ b/src/main/java/com/aida/lanecarford/service/LoginService.java
@@ -4,8 +4,12 @@ import com.aida.lanecarford.dto.LoginRequest;
import com.aida.lanecarford.entity.User;
import com.aida.lanecarford.vo.LoginVO;
import com.baomidou.mybatisplus.extension.service.IService;
+import com.nimbusds.jose.JOSEException;
import org.springframework.stereotype.Service;
+import java.io.IOException;
+import java.text.ParseException;
+
/**
* 登录服务接口
*
@@ -28,4 +32,6 @@ public interface LoginService extends IService {
boolean checkLoginStatus();
User getUserInfo();
+
+ LoginVO parseGoogleCredential(String credential) throws ParseException, JOSEException, IOException;
}
diff --git a/src/main/java/com/aida/lanecarford/service/impl/LoginServiceImpl.java b/src/main/java/com/aida/lanecarford/service/impl/LoginServiceImpl.java
index a79661b..7c2a055 100644
--- a/src/main/java/com/aida/lanecarford/service/impl/LoginServiceImpl.java
+++ b/src/main/java/com/aida/lanecarford/service/impl/LoginServiceImpl.java
@@ -19,10 +19,21 @@ import com.aida.lanecarford.vo.LoginVO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
import io.netty.util.internal.StringUtil;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
+import java.io.IOException;
+import java.net.URL;
+import java.text.ParseException;
import java.time.LocalDateTime;
import java.util.Objects;
@@ -34,6 +45,7 @@ import static com.aida.lanecarford.common.enums.AuthenticationOperationTypeEnum.
* @author xupei
* @since 2025-10-21
*/
+@Slf4j
@Service
@RequiredArgsConstructor
public class LoginServiceImpl extends ServiceImpl implements LoginService {
@@ -42,6 +54,12 @@ public class LoginServiceImpl extends ServiceImpl implements L
private final JwtUtil jwtUtil;
private final SendEmailUtil sendEmailUtil;
+ @Value("${google.client_id}")
+ private String googleClientId;
+ @Value("${google.client_secret}")
+ private String googleClientSecret;
+
+
@Override
public void preCheckAndSendEmail(LoginRequest loginRequest) {
// 1. 验证邮箱是否存在 boolean
@@ -243,9 +261,6 @@ public class LoginServiceImpl extends ServiceImpl implements L
return false;
}
- // todo 谷歌登录
-
-
// 获取用户信息
public User getUserInfo() {
AuthPrincipalVO userHolder = UserContext.getUserHolder();
@@ -256,5 +271,52 @@ public class LoginServiceImpl extends ServiceImpl implements L
return user;
}
+ @Override
+ public LoginVO parseGoogleCredential(String credential) throws ParseException, JOSEException, IOException {
+ // 从Google公钥URL下载JWK(如网络不通,可手动缓存)
+ URL jwkUrl = new URL("https://www.googleapis.com/oauth2/v3/certs");
+ JWKSet publicKeys = JWKSet.load(jwkUrl);
+
+ SignedJWT signedJWT = SignedJWT.parse(credential);
+ String kid = signedJWT.getHeader().getKeyID();
+
+ JWK jwk = publicKeys.getKeyByKeyId(kid);
+ RSASSAVerifier verifier = new RSASSAVerifier(jwk.toRSAKey());
+
+ if (!signedJWT.verify(verifier)) {
+ throw new IllegalArgumentException("Invalid signature");
+ }
+
+ JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
+ if (!claims.getAudience().contains(googleClientId)) {
+ throw new IllegalArgumentException("Invalid audience");
+ }
+
+ // 提取用户信息
+ String userId = claims.getSubject();
+ String email = claims.getStringClaim("email");
+ String name = claims.getStringClaim("name");
+ log.info("google userinfo, userId-{}, email-{}, name-{}", userId, email, name);
+
+ // 1. 根据用户邮箱查询是登录还是注册
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ queryWrapper.lambda().eq(User::getEmail, email);
+
+ User user = getOne(queryWrapper);
+ AuthenticationOperationTypeEnum type = Objects.isNull(user) ? REGISTER: LOGIN;
+ LoginRequest loginRequest = new LoginRequest();
+ loginRequest.setName(name);
+ loginRequest.setEmail(email);
+ loginRequest.setOperationType(type.name());
+
+ return switch (type) {
+ case REGISTER -> register(loginRequest);
+ case LOGIN -> login(loginRequest);
+ default -> throw new BusinessException("Unknown authentication operation type.");
+ };
+ }
+
+
+
}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index e429c45..667febf 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -88,3 +88,7 @@ tencent:
webhook:
domain: https://0dd6f6504aff.ngrok-free.app
+
+google:
+ client_id: 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com
+ client_secret: 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
\ No newline at end of file
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index 4d41d87..358840e 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -87,4 +87,8 @@ tencent:
sender: info@aida.com.hk
webhook:
- domain: http://18.167.251.121:10095
\ No newline at end of file
+ domain: http://18.167.251.121:10095
+
+google:
+ client_id: 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com
+ client_secret: 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
\ No newline at end of file