feat(identity): 接入认证中心多租户登录

支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
This commit is contained in:
2026-07-28 17:28:35 +08:00
parent 0b02e62c72
commit 5c679ff13f
45 changed files with 2986 additions and 139 deletions
@@ -0,0 +1,76 @@
package identityruntime
import (
"context"
"errors"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type tenantContextSyncRepository interface {
DueOIDCTenantBindingSyncs(context.Context, string, string, int) ([]store.OIDCTenantBindingSyncTarget, error)
ApplyOIDCTenantBindingSync(context.Context, string, identity.TenantContext, bool, time.Time) error
RejectOIDCTenantBinding(context.Context, string, string, time.Time) error
FailOIDCTenantBindingSync(context.Context, string, string, time.Time) error
}
type tenantContextSyncReader interface {
Get(context.Context, string, string) (identity.TenantContext, bool, error)
}
func runTenantContextSynchronizer(ctx context.Context, repository tenantContextSyncRepository, reader tenantContextSyncReader, issuer, applicationID string) {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case now := <-ticker.C:
_ = synchronizeDueTenantContexts(ctx, repository, reader, issuer, applicationID, now.UTC())
}
}
}
func synchronizeDueTenantContexts(ctx context.Context, repository tenantContextSyncRepository, reader tenantContextSyncReader, issuer, applicationID string, now time.Time) error {
targets, err := repository.DueOIDCTenantBindingSyncs(ctx, issuer, applicationID, 50)
if err != nil {
return err
}
for _, target := range targets {
tenant, unchanged, readErr := reader.Get(ctx, target.ExternalTenantID, target.ETag)
switch {
case readErr == nil && unchanged:
err = repository.ApplyOIDCTenantBindingSync(ctx, target.ID, identity.TenantContext{}, true, now)
case readErr == nil && tenant.Active():
err = repository.ApplyOIDCTenantBindingSync(ctx, target.ID, tenant, false, now)
case readErr == nil:
err = repository.RejectOIDCTenantBinding(ctx, target.ID, "tenant_inactive", now)
case errors.Is(readErr, identity.ErrTenantContextNotFound):
err = repository.RejectOIDCTenantBinding(ctx, target.ID, "tenant_not_found", now)
default:
err = repository.FailOIDCTenantBindingSync(
ctx, target.ID, "tenant_context_unavailable", now.Add(tenantContextRetryDelay(target.FailureCount)),
)
}
if err != nil && !errors.Is(err, store.ErrOIDCTenantUnavailable) {
return err
}
}
return nil
}
func tenantContextRetryDelay(failureCount int) time.Duration {
if failureCount < 0 {
failureCount = 0
}
delay := 30 * time.Second
for index := 0; index < failureCount && delay < 5*time.Minute; index++ {
delay *= 2
}
if delay > 5*time.Minute {
return 5 * time.Minute
}
return delay
}