Merge branch 'prod/release_1.0' into dev/dev

This commit is contained in:
2025-11-04 14:59:42 +08:00
8 changed files with 118 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
# ⭐️ 关键步骤:选择一个基于 JDK 21 的镜像
# openjdk:21-jdk-slim 是一个较小的选择
FROM openjdk:21-jdk-slim
FROM openjdk:21-ea-21-jdk-slim
# 设置工作目录
WORKDIR /app

20
pom.xml
View File

@@ -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>

View File

@@ -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")); // 排除登录接口
}
/**

View File

@@ -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直接获取credentialJWT 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));
}
}

View File

@@ -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;
}

View File

@@ -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.");
};
}
}

View File

@@ -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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

View File

@@ -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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