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
+21 -5
View File
@@ -36,6 +36,8 @@ type Event struct {
EventType string
EventTimestamp time.Time
SubjectIssuer string
ApplicationID string
SubjectType string
Subject string
TenantID string
InitiatingEntity string
@@ -50,6 +52,8 @@ type VerifierConfig struct {
Audience string
SubjectIssuer string
TenantID string
ApplicationID string
TenantMode string
StreamID string
ClockSkew time.Duration
HTTPClient *http.Client
@@ -93,8 +97,11 @@ func (e *protocolError) Error() string { return e.Code }
func NewVerifier(config VerifierConfig) (*Verifier, error) {
config.TransmitterIssuer = strings.TrimRight(strings.TrimSpace(config.TransmitterIssuer), "/")
config.SubjectIssuer = strings.TrimRight(strings.TrimSpace(config.SubjectIssuer), "/")
if !absoluteHTTPURL(config.TransmitterIssuer) || !absoluteHTTPURL(config.SubjectIssuer) || config.Audience == "" || config.TenantID == "" {
return nil, errors.New("SSF transmitter, audience, subject issuer, and tenant are required")
multiTenant := config.TenantMode == "multi_tenant"
validBinding := multiTenant && uuid.Validate(config.ApplicationID) == nil ||
!multiTenant && uuid.Validate(config.TenantID) == nil
if !absoluteHTTPURL(config.TransmitterIssuer) || !absoluteHTTPURL(config.SubjectIssuer) || config.Audience == "" || !validBinding {
return nil, errors.New("SSF transmitter, audience, subject issuer, and identity binding are required")
}
if _, err := uuid.Parse(config.StreamID); err != nil {
return nil, errors.New("SSF stream id must be a UUID")
@@ -179,6 +186,7 @@ func (v *Verifier) Verify(ctx context.Context, raw string) (Event, error) {
return Event{}, &protocolError{Code: "invalid_request"}
}
event.EventType, event.EventTimestamp, event.InitiatingEntity = SessionRevokedEventType, unixClaim(value["event_timestamp"]), stringValue(value["initiating_entity"])
event.ApplicationID, event.SubjectType = stringValue(value["application_id"]), stringValue(value["subject_type"])
if event.EventTimestamp.IsZero() || event.EventTimestamp.After(v.now().Add(v.config.ClockSkew)) ||
!validInitiator(event.InitiatingEntity) || !v.readSessionSubject(claims["sub_id"], &event) {
return Event{}, &protocolError{Code: "invalid_request"}
@@ -259,11 +267,19 @@ func (v *Verifier) readSessionSubject(raw any, event *Event) bool {
return false
}
event.SubjectIssuer, event.Subject, event.TenantID = strings.TrimRight(stringValue(user["iss"]), "/"), stringValue(user["sub"]), stringValue(tenant["id"])
if event.SubjectIssuer != v.config.SubjectIssuer || event.TenantID != v.config.TenantID {
if event.SubjectIssuer != v.config.SubjectIssuer || uuid.Validate(event.TenantID) != nil {
return false
}
_, err := uuid.Parse(event.Subject)
return err == nil
if v.config.TenantMode == "multi_tenant" {
if event.ApplicationID != v.config.ApplicationID ||
event.SubjectType != "principal" && event.SubjectType != "tenant" ||
event.SubjectType == "tenant" && event.Subject != event.TenantID {
return false
}
} else if event.ApplicationID != "" || event.SubjectType != "" || event.TenantID != v.config.TenantID {
return false
}
return uuid.Validate(event.Subject) == nil
}
func (v *Verifier) readStreamSubject(raw any, event *Event) bool {