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
+16 -5
View File
@@ -50,6 +50,7 @@ type OIDCConfig struct {
type OIDCSecurityEventIdentity struct {
Issuer string
ApplicationID string
ContextType string
TenantID string
Subject string
IssuedAt time.Time
@@ -179,13 +180,21 @@ func (v *OIDCVerifier) Verify(ctx context.Context, raw string) (*User, error) {
return nil, oidcUnauthorized(registeredClaimsValidationCategory(err), "signature or registered claims are invalid", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
contextType := stringClaim(claims, "context_type")
tenantID := stringClaim(claims, "tid")
validTenant := tenantID == v.config.TenantID
validContext := contextType == "tenant" && tenantID == v.config.TenantID
if v.config.TenantMode == "multi_tenant" {
validTenant = uuid.Validate(tenantID) == nil
switch contextType {
case "platform":
validContext = tenantID == ""
case "tenant":
validContext = uuid.Validate(tenantID) == nil
default:
validContext = false
}
}
clientID := stringClaim(claims, "client_id")
if !ok || stringClaim(claims, "sub") == "" || !validTenant ||
if !ok || stringClaim(claims, "sub") == "" || !validContext ||
v.config.ClientID != "" && clientID != v.config.ClientID {
return nil, oidcUnauthorized("STABLE_IDENTITY_CLAIMS_INVALID", "stable identity claims are invalid", nil)
}
@@ -211,7 +220,8 @@ func (v *OIDCVerifier) Verify(ctx context.Context, raw string) (*User, error) {
if v.config.SecurityEventEvaluator != nil {
evaluation, evaluateErr := v.config.SecurityEventEvaluator(ctx, OIDCSecurityEventIdentity{
Issuer: v.config.Issuer, ApplicationID: v.config.ApplicationID,
TenantID: tenantID, Subject: stringClaim(claims, "sub"), IssuedAt: issuedAt,
ContextType: contextType, TenantID: tenantID,
Subject: stringClaim(claims, "sub"), IssuedAt: issuedAt,
})
if evaluateErr != nil {
return nil, NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SECURITY_EVENT_STATE_UNAVAILABLE", "认证撤销状态暂时不可用")
@@ -248,7 +258,8 @@ func (v *OIDCVerifier) Verify(ctx context.Context, raw string) (*User, error) {
}
return &User{
ID: stringClaim(claims, "sub"), Username: username, Roles: roles,
TenantID: tenantID, Source: "oidc", TokenExpiresAt: expiresAt, TokenIssuedAt: issuedAt, Issuer: v.config.Issuer,
ContextType: contextType, TenantID: tenantID, Source: "oidc",
TokenExpiresAt: expiresAt, TokenIssuedAt: issuedAt, Issuer: v.config.Issuer,
ApplicationID: v.config.ApplicationID, OIDCClientID: clientID,
}, nil
}