feat(identity): 实现统一认证运行时热切换
从 Active Revision 构造并验证 OIDC、BFF Session、Introspection 与 SSF Runtime,在数据库激活成功后原子替换内存引用。请求链路使用不可变快照,失败保留当前运行时,本地管理登录不受影响。\n\n验证:go test ./apps/api/...;go vet ./apps/api/...
This commit is contained in:
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identityruntime"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/oidcsession"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/runner"
|
||||
ssfreceiver "github.com/easyai/easyai-ai-gateway/apps/api/internal/securityevents"
|
||||
@@ -31,6 +33,8 @@ type Server struct {
|
||||
geminiUploadSessions sync.Map
|
||||
securityEventReceiver http.Handler
|
||||
securityEventManager *ssfreceiver.ConnectionManager
|
||||
identityRuntime *identityruntime.Manager
|
||||
identityPairing *identity.PairingService
|
||||
}
|
||||
|
||||
type oidcPublicClient interface {
|
||||
@@ -63,83 +67,43 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
runner: runner.New(cfg, db, logger),
|
||||
logger: logger,
|
||||
}
|
||||
server.auth.LegacyJWTEnabled = !cfg.OIDCEnabled || cfg.OIDCAcceptLegacyHS256
|
||||
server.auth.ServerMainInternalKey = cfg.ServerMainInternalKey
|
||||
server.auth.ServerMainInternalSecret = cfg.ServerMainInternalSecret
|
||||
securityEventMetrics := &ssfreceiver.Metrics{}
|
||||
if cfg.OIDCEnabled {
|
||||
secretStore, err := securityEventSecretStore(cfg)
|
||||
if err != nil {
|
||||
panic("invalid OIDC security event secret store: " + err.Error())
|
||||
}
|
||||
manager, err := ssfreceiver.NewConnectionManager(ctx, db, secretStore, ssfreceiver.ConnectionManagerConfig{
|
||||
AppEnv: cfg.AppEnv, OIDCEnabled: cfg.OIDCEnabled, OIDCIssuer: cfg.OIDCIssuer, OIDCTenantID: cfg.OIDCTenantID,
|
||||
ManagementClientID: cfg.OIDCIntrospectionClientID, ManagementClientSecret: cfg.OIDCIntrospectionClientSecret,
|
||||
PublicBaseURL: cfg.PublicBaseURL,
|
||||
HeartbeatInterval: time.Duration(cfg.OIDCSecurityEventsHeartbeatIntervalSeconds) * time.Second,
|
||||
StaleAfter: time.Duration(cfg.OIDCSecurityEventsStaleAfterSeconds) * time.Second,
|
||||
ClockSkew: time.Duration(cfg.OIDCSecurityEventsClockSkewSeconds) * time.Second,
|
||||
}, securityEventMetrics)
|
||||
if err != nil {
|
||||
panic("initialize OIDC security event connection manager: " + err.Error())
|
||||
}
|
||||
server.securityEventManager = manager
|
||||
server.securityEventReceiver = manager
|
||||
secretStore, err := securityEventSecretStore(cfg)
|
||||
if err != nil {
|
||||
panic("invalid identity SecretStore: " + err.Error())
|
||||
}
|
||||
if cfg.OIDCEnabled {
|
||||
var evaluator func(context.Context, auth.OIDCSecurityEventIdentity) (auth.OIDCSecurityEventEvaluation, error)
|
||||
if server.securityEventManager != nil {
|
||||
evaluator = server.securityEventManager.Evaluate
|
||||
runtimeBuilder := identityruntime.NewRuntimeBuilder(ctx, db, secretStore, identityruntime.RuntimeBuilderConfig{
|
||||
AppEnv: cfg.AppEnv, JWKSCacheTTL: 5 * time.Minute,
|
||||
HeartbeatInterval: time.Duration(cfg.OIDCSecurityEventsHeartbeatIntervalSeconds) * time.Second,
|
||||
StaleAfter: time.Duration(cfg.OIDCSecurityEventsStaleAfterSeconds) * time.Second,
|
||||
ClockSkew: time.Duration(cfg.OIDCSecurityEventsClockSkewSeconds) * time.Second,
|
||||
}, securityEventMetrics)
|
||||
server.identityRuntime = identityruntime.NewManager(db, runtimeBuilder)
|
||||
if err := server.identityRuntime.LoadActive(ctx); err != nil && logger != nil {
|
||||
logger.Error("load active identity runtime failed; local management login remains available", "error_category", "identity_runtime_load_failed")
|
||||
}
|
||||
server.identityPairing = identity.NewPairingService(db, secretStore, func(baseURL string) (identity.OnboardingRemote, error) {
|
||||
return identity.NewOnboardingClient(baseURL, nil)
|
||||
}, server.identityRuntime)
|
||||
server.auth.OIDCVerifierProvider = func() *auth.OIDCVerifier {
|
||||
if runtime := server.identityRuntime.Current(); runtime != nil {
|
||||
return runtime.Verifier
|
||||
}
|
||||
verifier, err := auth.NewOIDCVerifier(auth.OIDCConfig{
|
||||
Issuer: cfg.OIDCIssuer, Audience: cfg.OIDCAudience, TenantID: cfg.OIDCTenantID,
|
||||
RolePrefix: cfg.OIDCRolePrefix, RequiredScopes: cfg.OIDCRequiredScopes,
|
||||
JWKSCacheTTL: time.Duration(cfg.OIDCJWKSCacheTTLSeconds) * time.Second,
|
||||
IntrospectionEnabled: cfg.OIDCIntrospectionEnabled,
|
||||
IntrospectionClientID: cfg.OIDCIntrospectionClientID,
|
||||
IntrospectionClientSecret: cfg.OIDCIntrospectionClientSecret,
|
||||
IntrospectionCredentialProvider: server.securityEventManager.IntrospectionCredential,
|
||||
SecurityEventEvaluator: evaluator,
|
||||
IntrospectionObserver: securityEventMetrics.ObserveIntrospection,
|
||||
JWKSRefreshFailureObserver: func() { securityEventMetrics.ObserveJWKSRefreshFailure("oidc") },
|
||||
})
|
||||
if err != nil {
|
||||
panic("invalid OIDC configuration: " + err.Error())
|
||||
}
|
||||
server.auth.OIDCVerifier = verifier
|
||||
if cfg.OIDCBrowserSessionEnabled {
|
||||
key, err := cfg.OIDCSessionEncryptionKeyBytes()
|
||||
if err != nil {
|
||||
panic("invalid OIDC session configuration: " + err.Error())
|
||||
}
|
||||
cipher, err := oidcsession.NewCipher(key)
|
||||
if err != nil {
|
||||
panic("invalid OIDC session configuration: " + err.Error())
|
||||
}
|
||||
client, err := auth.NewOIDCPublicClient(auth.OIDCPublicClientConfig{
|
||||
Issuer: cfg.OIDCIssuer, ClientID: cfg.OIDCClientID, RedirectURI: cfg.OIDCRedirectURI,
|
||||
PostLogoutRedirectURI: cfg.OIDCPostLogoutRedirectURI,
|
||||
Scopes: append([]string{"openid", "profile"}, cfg.OIDCRequiredScopes...),
|
||||
})
|
||||
if err != nil {
|
||||
panic("invalid OIDC public client configuration: " + err.Error())
|
||||
}
|
||||
sessions, err := oidcsession.NewService(db, cipher, verifier, client, oidcsession.Config{
|
||||
IdleTTL: time.Duration(cfg.OIDCSessionIdleTTLSeconds) * time.Second,
|
||||
AbsoluteTTL: time.Duration(cfg.OIDCSessionAbsoluteTTLSeconds) * time.Second,
|
||||
RefreshBefore: time.Duration(cfg.OIDCSessionRefreshBeforeSeconds) * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
panic("invalid OIDC session configuration: " + err.Error())
|
||||
}
|
||||
server.oidcClient = client
|
||||
server.oidcSessions = sessions
|
||||
server.oidcSessionCipher = cipher
|
||||
server.auth.OIDCSessionResolver = func(ctx context.Context, sessionID string) (*auth.User, error) {
|
||||
user, resolveErr := sessions.Resolve(ctx, sessionID)
|
||||
return user, oidcSessionRequestError(resolveErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
server.auth.OIDCSessionResolverProvider = func(ctx context.Context, sessionID string) (*auth.User, error) {
|
||||
runtime := server.identityRuntime.Current()
|
||||
if runtime == nil || runtime.Sessions == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
user, resolveErr := runtime.Sessions.Resolve(ctx, sessionID)
|
||||
return user, oidcSessionRequestError(resolveErr)
|
||||
}
|
||||
server.auth.LegacyJWTEnabledProvider = func() bool {
|
||||
runtime := server.identityRuntime.Current()
|
||||
return runtime == nil || runtime.Revision.LegacyJWTEnabled
|
||||
}
|
||||
server.auth.LocalAPIKeyVerifier = db.VerifyLocalAPIKey
|
||||
server.runner.StartAsyncQueueWorker(ctx)
|
||||
|
||||
Reference in New Issue
Block a user