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