feat(auth): Google 登录支持多客户端 audience 校验

- GoogleAuthProperties 的 clientId 改为 clientIds 列表
- GoogleIdTokenVerifier 命中任一 clientId 即通过,兼容 iOS 与 Android 不同的 aud
- app-api 注入三个客户端 id,生产可用 GOOGLE_CLIENT_IDS 覆盖
This commit is contained in:
jianghaifeng committed 2026-11-13 12:30:55 +08:00
1 parent a706621786
commit 32cac38a0e
3 files changed
+21 -7

No files matched your search

@@ -65,6 +65,11 @@ fruitshorts:
# 仅作 sys_config 缺失/非法时的兜底;运行时优先读取后台「系统配置」
register-max-per-ip-per-day: ${REGISTER_MAX_PER_IP_PER_DAY:5}
register-max-per-device-per-day: ${REGISTER_MAX_PER_DEVICE_PER_DAY:3}
google:
# Google 登录允许的客户端 id(逗号分隔)。idToken 的 aud 随平台不同:
# iOS 为 iOS 客户端 id,Android 为 serverClientId 对应的 Web 客户端 id。
# 客户端 id 属公开信息,不是密钥;生产可用 GOOGLE_CLIENT_IDS 覆盖。
client-ids: ${GOOGLE_CLIENT_IDS:11115008580-51gv4sj2c78rud6n127g3shu4d2n96un.apps.googleusercontent.com,11115008580-dbqe47fhp5560nhrocrppiq7hcnlvl1s.apps.googleusercontent.com,11115008580-1v5pt5mfgbbuba74ebia6hngj618ne3a.apps.googleusercontent.com}
interaction:
# MACHINE_FIRST:机审通过即发布,命中 REVIEW 级敏感词才转人工
# PRE_REVIEW :全部先审后发(生产建议)
@@ -6,20 +6,25 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Google 登录配置。
*
* <p>凭据只从配置/环境变量读,不入库、不提交到仓库:
* <pre>
* fruitshorts.auth.google.client-id: 1234567890-xxx.apps.googleusercontent.com
* fruitshorts.auth.google.client-ids: 1234-ios.apps.googleusercontent.com,1234-web.apps.googleusercontent.com
* </pre>
* 未配置 clientId 时,Google 登录接口明确返回"未配置"错误,不会静默放行。
* 客户端各平台拿到的 idToken,其 audience 并不相同:iOS 是 iOS 客户端 id,
* Android 是 serverClientId 对应的 Web 客户端 id,因此这里必须允许配置多个。
* 未配置时,Google 登录接口明确返回"未配置"错误,不会静默放行。
*/
@Data
@Component
@ConfigurationProperties(prefix = "fruitshorts.auth.google")
public class GoogleAuthProperties {
/** Google OAuth 客户端 id,用于校验 idToken 的 audience。 */
private String clientId = "";
/** 允许的 Google OAuth 客户端 id 列表,用于校验 idToken 的 audience。 */
private List<String> clientIds = new ArrayList<>();
}
@@ -19,6 +19,7 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
/**
* Google idToken 校验器。
@@ -45,11 +46,14 @@ public class GoogleIdTokenVerifier {
/**
* 校验 idToken 并返回 Google 用户信息。
*
* <p>audience 按平台不同而不同(iOS 用 iOS 客户端 id,Android 用 Web 客户端 id),
* 因此只要命中配置列表里的任意一个即视为合法,同时拦截其他应用盗用令牌。
*
* @param idToken 客户端从 Google 拿到的 idToken
*/
public GoogleUser verify(String idToken) {
String clientId = properties.getClientId();
if (!StringUtils.hasText(clientId)) {
List<String> clientIds = properties.getClientIds();
if (clientIds == null || clientIds.isEmpty()) {
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "Google 登录未配置");
}
if (!StringUtils.hasText(idToken)) {
@@ -58,7 +62,7 @@ public class GoogleIdTokenVerifier {
JsonNode body = fetchTokenInfo(idToken);
String aud = body.path("aud").asText("");
String sub = body.path("sub").asText("");
if (!clientId.equals(aud) || !StringUtils.hasText(sub)) {
if (!clientIds.contains(aud) || !StringUtils.hasText(sub)) {
throw new ServiceException(ErrorCode.UNAUTHORIZED, "Google 令牌无效");
}
return new GoogleUser(sub, body.path("email").asText(""),