fix(identity): 支持平台用户显式登录 AI Gateway

修复多租户身份配置将所有人类登录都强制解释为租户上下文的问题。Web 现在提供平台与租户两个受控入口,API 严格绑定 context_type、tid、issuer、application 和 subject,并为平台用户建立独立本地投影与可刷新会话。\n\n风险:新增会话身份列保持旧会话可读,新会话一律使用严格约束;未改变租户数据隔离和 API Key 行为。\n\n验证:Go 全量测试与 go vet 通过;PostgreSQL 平台投影、会话和安全事件集成测试通过;前端 lint、141 项测试与生产 build 通过;OpenAPI 生成和迁移安全测试通过。
This commit is contained in:
2026-07-31 14:24:40 +08:00
parent 88c971564a
commit c0296dbf06
29 changed files with 1061 additions and 84 deletions
+28 -4
View File
@@ -36,6 +36,8 @@ const (
// @Description Gateway 生成 state、nonce 和 PKCE S256 参数,并跳转认证中心;浏览器不接触 Token。
// @Tags auth
// @Param returnTo query string false "登录后返回的站内相对路径"
// @Param contextType query string true "显式登录上下文:platform 或 tenant"
// @Param tenantHint query string false "Tenant 上下文可选的稳定 Tenant UUID"
// @Success 303
// @Failure 400 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
@@ -50,12 +52,22 @@ func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) {
if returnTo == "" {
returnTo = "/"
}
contextType := strings.TrimSpace(r.URL.Query().Get("contextType"))
tenantHint := strings.TrimSpace(r.URL.Query().Get("tenantHint"))
if tenantHint != "" && (runtime.Revision.TenantMode != "multi_tenant" || uuid.Validate(tenantHint) != nil) {
writeError(w, http.StatusBadRequest, "租户提示无效", errorCodeOIDCLoginInvalid)
validContext := contextType == "tenant" ||
contextType == "platform" &&
runtime.Revision.TenantMode == "multi_tenant"
validTenantHint := tenantHint == "" ||
contextType == "tenant" &&
runtime.Revision.TenantMode == "multi_tenant" &&
uuid.Validate(tenantHint) == nil
if !validContext || !validTenantHint {
writeError(w, http.StatusBadRequest, "登录上下文无效", errorCodeOIDCLoginInvalid)
return
}
transaction, err := oidcsession.NewLoginTransactionWithTenantHint(returnTo, tenantHint, time.Now())
transaction, err := oidcsession.NewLoginTransactionWithContext(
returnTo, contextType, tenantHint, time.Now(),
)
if err != nil {
writeError(w, http.StatusBadRequest, "登录后返回地址无效", errorCodeOIDCLoginInvalid)
return
@@ -67,7 +79,9 @@ func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) {
return
}
authorizationURL, err := runtime.PublicClient.AuthorizationURL(
r.Context(), transaction.State, transaction.Nonce, transaction.PKCEVerifier, transaction.TenantHint,
r.Context(), transaction.State, transaction.Nonce,
transaction.PKCEVerifier, transaction.ContextType,
transaction.TenantHint,
)
if err != nil {
s.logger.ErrorContext(r.Context(), "load OIDC authorization endpoint failed", "error", err)
@@ -127,6 +141,16 @@ func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
s.writeOIDCTokenFailure(w, r, "ACCESS_TOKEN_INVALID", auth.OIDCValidationCategory(err), "认证中心访问令牌校验失败")
return
}
if identity.ContextType != transaction.ContextType ||
transaction.TenantHint != "" &&
identity.TenantID != transaction.TenantHint {
s.writeOIDCTokenFailure(
w, r, "ACCESS_TOKEN_CONTEXT_MISMATCH",
"STABLE_IDENTITY_CLAIMS_INVALID",
"认证中心访问令牌上下文与登录入口不匹配",
)
return
}
idSubject, err := runtime.PublicClient.VerifyIDToken(r.Context(), tokens.IDToken, transaction.Nonce)
if err != nil || idSubject != identity.ID {
s.writeOIDCTokenFailure(w, r, "ID_TOKEN_INVALID", auth.OIDCValidationCategory(err), "认证中心身份令牌校验失败")