feat: 接入 SSF 实时会话撤销

This commit is contained in:
2026-07-14 17:06:41 +08:00
parent 30ad0e9f2c
commit f30aaeb2d4
28 changed files with 2597 additions and 91 deletions
+67 -15
View File
@@ -13,21 +13,23 @@ import (
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
"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"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type Server struct {
ctx context.Context
cfg config.Config
store *store.Store
oidcUserResolver oidcUserResolver
auth *auth.Authenticator
oidcClient oidcPublicClient
oidcSessions oidcSessionManager
oidcSessionCipher *oidcsession.Cipher
runner *runner.Service
logger *slog.Logger
geminiUploadSessions sync.Map
ctx context.Context
cfg config.Config
store *store.Store
oidcUserResolver oidcUserResolver
auth *auth.Authenticator
oidcClient oidcPublicClient
oidcSessions oidcSessionManager
oidcSessionCipher *oidcsession.Cipher
runner *runner.Service
logger *slog.Logger
geminiUploadSessions sync.Map
securityEventReceiver http.Handler
}
type oidcPublicClient interface {
@@ -63,14 +65,58 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
server.auth.LegacyJWTEnabled = !cfg.OIDCEnabled || cfg.OIDCAcceptLegacyHS256
server.auth.ServerMainInternalKey = cfg.ServerMainInternalKey
server.auth.ServerMainInternalSecret = cfg.ServerMainInternalSecret
securityEventMetrics := &ssfreceiver.Metrics{}
var securityEventService *ssfreceiver.Service
if cfg.OIDCSecurityEventsEnabled {
ssfVerifier, err := ssfreceiver.NewVerifier(ssfreceiver.VerifierConfig{
TransmitterIssuer: cfg.OIDCSecurityEventsTransmitterIssuer,
Audience: cfg.OIDCSecurityEventsReceiverAudience, SubjectIssuer: cfg.OIDCIssuer,
TenantID: cfg.OIDCTenantID, StreamID: cfg.OIDCSecurityEventsStreamID,
ClockSkew: time.Duration(cfg.OIDCSecurityEventsClockSkewSeconds) * time.Second,
})
if err != nil {
panic("invalid OIDC security event verifier configuration: " + err.Error())
}
ssfVerifier.SetMetrics(securityEventMetrics)
securityEventService, err = ssfreceiver.NewService(db, ssfreceiver.ServiceConfig{
Issuer: cfg.OIDCSecurityEventsTransmitterIssuer, Audience: cfg.OIDCSecurityEventsReceiverAudience,
StreamID: cfg.OIDCSecurityEventsStreamID, ManagementTokenURL: cfg.OIDCSecurityEventsManagementTokenURL,
ManagementClientID: cfg.OIDCSecurityEventsManagementClientID, ManagementSecret: cfg.OIDCSecurityEventsManagementClientSecret,
HeartbeatInterval: time.Duration(cfg.OIDCSecurityEventsHeartbeatIntervalSeconds) * time.Second,
StaleAfter: time.Duration(cfg.OIDCSecurityEventsStaleAfterSeconds) * time.Second,
})
if err != nil {
panic("invalid OIDC security event heartbeat configuration: " + err.Error())
}
securityEventService.SetMetrics(securityEventMetrics)
if err := securityEventService.Start(ctx); err != nil {
panic("OIDC security event state initialization failed")
}
receiver, err := ssfreceiver.NewHandler(
ssfVerifier, db, cfg.OIDCSecurityEventsTransmitterIssuer, cfg.OIDCSecurityEventsReceiverAudience,
cfg.OIDCSecurityEventsStreamID, cfg.OIDCSecurityEventsBearerSecret, cfg.OIDCSecurityEventsBearerSecretNext,
)
if err != nil {
panic("invalid OIDC security event receiver configuration: " + err.Error())
}
receiver.SetMetrics(securityEventMetrics)
server.securityEventReceiver = receiver
}
if cfg.OIDCEnabled {
var evaluator func(context.Context, auth.OIDCSecurityEventIdentity) (auth.OIDCSecurityEventEvaluation, error)
if securityEventService != nil {
evaluator = securityEventService.Evaluate
}
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,
JWKSCacheTTL: time.Duration(cfg.OIDCJWKSCacheTTLSeconds) * time.Second,
IntrospectionEnabled: cfg.OIDCIntrospectionEnabled,
IntrospectionClientID: cfg.OIDCIntrospectionClientID,
IntrospectionClientSecret: cfg.OIDCIntrospectionClientSecret,
SecurityEventEvaluator: evaluator,
IntrospectionObserver: securityEventMetrics.ObserveIntrospection,
JWKSRefreshFailureObserver: func() { securityEventMetrics.ObserveJWKSRefreshFailure("oidc") },
})
if err != nil {
panic("invalid OIDC configuration: " + err.Error())
@@ -118,6 +164,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", server.health)
mux.HandleFunc("GET /readyz", server.ready)
mux.Handle("GET /metrics", securityEventMetrics.Handler(db, cfg.OIDCSecurityEventsTransmitterIssuer, cfg.OIDCSecurityEventsReceiverAudience, cfg.OIDCSecurityEventsEnabled))
mux.HandleFunc("GET /static/simulation/{asset}", serveSimulationAsset)
mux.HandleFunc("GET /static/generated/{asset}", server.serveGeneratedStaticAsset)
mux.HandleFunc("GET /static/uploaded/{asset}", server.serveUploadedStaticAsset)
@@ -128,6 +175,9 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
mux.HandleFunc("GET /api/v1/auth/oidc/callback", server.completeOIDCLogin)
mux.HandleFunc("POST /api/v1/auth/oidc/logout", server.logoutOIDCSession)
mux.HandleFunc("DELETE /api/v1/auth/oidc/session", server.deleteOIDCBrowserSession)
if server.securityEventReceiver != nil {
mux.HandleFunc("POST /api/v1/security-events/ssf", server.receiveSecurityEvent)
}
mux.Handle("GET /api/v1/me", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.me)))
mux.Handle("GET /api/v1/public/catalog/providers", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listCatalogProviders)))
mux.Handle("GET /api/v1/public/catalog/base-models", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listBaseModels)))
@@ -284,6 +334,8 @@ func oidcSessionRequestError(err error) error {
return auth.NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SESSION_REFRESH_UNAVAILABLE", "认证中心暂时不可用,请稍后重试")
case errors.Is(err, oidcsession.ErrSessionStoreUnavailable):
return auth.NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SESSION_STORE_UNAVAILABLE", "登录会话存储暂时不可用")
case errors.Is(err, oidcsession.ErrSecurityStateUnavailable):
return auth.NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SECURITY_EVENT_STATE_UNAVAILABLE", "认证撤销状态暂时不可用")
case errors.Is(err, oidcsession.ErrGatewayUserDisabled):
return auth.NewRequestAuthError(http.StatusForbidden, "GATEWAY_USER_DISABLED", "该 Gateway 账号已停用,请联系管理员")
default: