feat(auth): Google 登录支持多客户端 audience 校验
- GoogleAuthProperties 的 clientId 改为 clientIds 列表 - GoogleIdTokenVerifier 命中任一 clientId 即通过,兼容 iOS 与 Android 不同的 aud - app-api 注入三个客户端 id,生产可用 GOOGLE_CLIENT_IDS 覆盖
This commit is contained in:
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 :全部先审后发(生产建议)
|
||||
|
||||
+9
-4
@@ -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<>();
|
||||
}
|
||||
+7
-3
@@ -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(""),
|
||||
|
||||
Reference in new issue
Block a user