谷歌登录token验证
This commit is contained in:
20
pom.xml
20
pom.xml
@@ -193,6 +193,26 @@
|
||||
<version>3.1.572</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Google API 客户端核心库 -->
|
||||
<dependency>
|
||||
<groupId>com.google.api-client</groupId>
|
||||
<artifactId>google-api-client</artifactId>
|
||||
<version>2.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Google HTTP 客户端 Jackson2 -->
|
||||
<dependency>
|
||||
<groupId>com.google.http-client</groupId>
|
||||
<artifactId>google-http-client-jackson2</artifactId>
|
||||
<version>1.42.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.nimbusds</groupId>
|
||||
<artifactId>nimbus-jose-jwt</artifactId>
|
||||
<version>9.37</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -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")); // 排除登录接口
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<LoginVO> parseGoogleCredential(@RequestParam("credential") String credential) throws ParseException, IOException, JOSEException {
|
||||
return ApiResponse.success(loginService.parseGoogleCredential(credential));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<User> {
|
||||
boolean checkLoginStatus();
|
||||
|
||||
User getUserInfo();
|
||||
|
||||
LoginVO parseGoogleCredential(String credential) throws ParseException, JOSEException, IOException;
|
||||
}
|
||||
|
||||
@@ -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<UserMapper, User> implements LoginService {
|
||||
@@ -42,6 +54,12 @@ public class LoginServiceImpl extends ServiceImpl<UserMapper, User> 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<UserMapper, User> implements L
|
||||
return false;
|
||||
}
|
||||
|
||||
// todo 谷歌登录
|
||||
|
||||
|
||||
// 获取用户信息
|
||||
public User getUserInfo() {
|
||||
AuthPrincipalVO userHolder = UserContext.getUserHolder();
|
||||
@@ -256,5 +271,52 @@ public class LoginServiceImpl extends ServiceImpl<UserMapper, User> 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<User> 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.");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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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
|
||||
@@ -87,4 +87,8 @@ tencent:
|
||||
sender: info@aida.com.hk
|
||||
|
||||
webhook:
|
||||
domain: http://18.167.251.121:10095
|
||||
domain: http://18.167.251.121:10095
|
||||
|
||||
google:
|
||||
client_id: 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com
|
||||
client_secret: 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
|
||||
Reference in New Issue
Block a user