# 第二十二章:WIMSE — 多系统环境中的工作负载身份 > "当工作负载跨越多个系统边界时,身份不应该在途中丢失。" ```{mermaid} mindmap root((WIMSE)) 背景 IETF 工作组 跨系统身份 与 SPIFFE 互补 核心概念 WIT Txn-Token Token Exchange 应用场景 多云 SaaS-to-SaaS 混合云 与现有标准 OAuth 2.0 SPIFFE OIDC ``` ## 22.1 WIMSE 是什么 WIMSE(Workload Identity in Multi-System Environments)是 IETF 正在制定的标准,解决**跨系统环境中工作负载身份传递**的问题。 ### WIMSE 架构总览 ```{mermaid} flowchart TB subgraph UserLayer["用户层"] User["👤 终端用户"] end subgraph ControlPlane["控制平面"] IdP["身份提供者
(IdP / OIDC)"] TES["Token Exchange
Service"] TTS["Txn-Token
Service"] PolicyEngine["策略引擎
(OPA / Cedar)"] end subgraph DataPlane["数据平面 — 多系统环境"] subgraph AWS["AWS"] SA["Service A
IAM Role"] end subgraph GCP["GCP"] SB["Service B
Service Account"] end subgraph Azure["Azure"] SC["Service C
Managed Identity"] end end subgraph IdentityLayer["身份基础设施"] SPIFFE["SPIFFE / SPIRE"] CertAuth["证书颁发机构
(CA)"] end User -->|"认证"| IdP IdP -->|"用户身份"| TTS SPIFFE -->|"SVID 签发"| SA & SB & SC CertAuth -->|"X.509 证书"| SPIFFE SA -->|"WIT + Txn-Token"| TES TES -->|"交换后的 Token"| SB SB -->|"WIT + Txn-Token"| SC TTS -->|"签发 Txn-Token"| SA PolicyEngine -->|"授权决策"| SB & SC style UserLayer fill:#E3F2FD,stroke:#1565C0 style ControlPlane fill:#FFF3E0,stroke:#E65100 style DataPlane fill:#E8F5E9,stroke:#2E7D32 style IdentityLayer fill:#F3E5F5,stroke:#6A1B9A ``` ### 为什么需要 WIMSE SPIFFE 解决了"工作负载如何获得身份"的问题,但在复杂的多系统调用链中,还有未解决的问题: ```{mermaid} flowchart LR User["👤 用户"] --> SA["Service A
(AWS IAM Role)"] SA --> SB["Service B
(GCP Service Account)"] SB --> SC["Service C
(Azure Managed Identity)"] SC --> DB[("Database")] SA -.-|"❓ 如何证明
代表 Service A 调用"| SB SB -.-|"❓ 如何传递
原始用户身份"| SC SC -.-|"❓ 如何防止
Confused Deputy"| DB style SA fill:#FF9800,color:#fff style SB fill:#4CAF50,color:#fff style SC fill:#2196F3,color:#fff ``` **核心问题:** 1. Service C 如何知道这个请求最初来自哪个用户? 2. Service B 如何向 Service C 证明"我是代表 Service A 调用的"? 3. 如何防止 Confused Deputy 攻击? ## 22.2 核心概念 ### Workload Identity Token(WIT) WIT 是基于 JWT 的工作负载身份令牌,标识工作负载自身的身份: ```json { "iss": "https://issuer.example.com", "sub": "spiffe://example.com/service-a", "aud": "spiffe://example.com/service-b", "exp": 1709510400, "iat": 1709506800, "jti": "unique-token-id", "cnf": { "kid": "key-binding-id" } } ``` ### Transaction Token(Txn-Token) Txn-Token 在调用链中传递,记录完整的调用上下文: ```json { "iss": "https://txn-token-service.example.com", "iat": 1709506800, "aud": "https://api.example.com", "txn": "txn-abc-123", "sub": { "format": "oidc_id_token", "iss": "https://idp.example.com", "sub": "user-alice" }, "rctx": { "req_ip": "192.168.1.100", "req_method": "POST", "req_path": "/api/transfer" }, "purp": "transfer_funds" } ``` ### Workload Identity Token 交换流程 ```{mermaid} sequenceDiagram autonumber participant User as 👤 用户 participant SA as Service A
(AWS) participant TTS as Txn-Token
Service participant TES as Token Exchange
Service participant SB as Service B
(GCP) participant SC as Service C
(Azure) User->>SA: 请求 + 用户凭证 (OIDC ID Token) SA->>SA: 获取本地 WIT (SPIFFE SVID) SA->>TTS: 请求 Txn-Token
(用户身份 + 请求上下文) TTS-->>SA: 签发 Txn-Token
(包含用户身份、调用目的) SA->>TES: Token Exchange 请求
(AWS WIT → GCP Token) TES->>TES: 验证 AWS WIT
查找信任映射 TES-->>SA: 返回 GCP 可识别的 Token SA->>SB: 请求 + 交换后的 Token + Txn-Token SB->>SB: 验证 Token 和 Txn-Token
检查调用链完整性 SB->>TES: Token Exchange 请求
(GCP WIT → Azure Token) TES-->>SB: 返回 Azure 可识别的 Token SB->>SC: 请求 + 交换后的 Token + Txn-Token SC->>SC: 验证 Token 和 Txn-Token
确认原始用户身份 SC-->>SB: 响应 SB-->>SA: 响应 SA-->>User: 响应 ``` ### 多系统环境信任链 ```{mermaid} flowchart TB subgraph TrustRoot["全局信任根"] RootCA["Root CA / Trust Anchor"] end subgraph TrustDomainAWS["信任域: aws.example.com"] SPIRE_AWS["SPIRE Server
(AWS)"] WIT_A["WIT: spiffe://aws.example.com/service-a"] WIT_A2["WIT: spiffe://aws.example.com/service-d"] end subgraph TrustDomainGCP["信任域: gcp.example.com"] SPIRE_GCP["SPIRE Server
(GCP)"] WIT_B["WIT: spiffe://gcp.example.com/service-b"] end subgraph TrustDomainAzure["信任域: azure.example.com"] SPIRE_Azure["SPIRE Server
(Azure)"] WIT_C["WIT: spiffe://azure.example.com/service-c"] end subgraph Federation["跨域信任联邦"] TES2["Token Exchange Service"] BundleEndpoint["Trust Bundle
Endpoint"] end RootCA -->|"签发中间 CA"| SPIRE_AWS & SPIRE_GCP & SPIRE_Azure SPIRE_AWS -->|"签发 SVID"| WIT_A & WIT_A2 SPIRE_GCP -->|"签发 SVID"| WIT_B SPIRE_Azure -->|"签发 SVID"| WIT_C SPIRE_AWS <-->|"Trust Bundle 交换"| BundleEndpoint SPIRE_GCP <-->|"Trust Bundle 交换"| BundleEndpoint SPIRE_Azure <-->|"Trust Bundle 交换"| BundleEndpoint WIT_A -->|"Token Exchange"| TES2 TES2 -->|"跨域 Token"| WIT_B WIT_B -->|"Token Exchange"| TES2 TES2 -->|"跨域 Token"| WIT_C style TrustRoot fill:#FFCDD2,stroke:#B71C1C style TrustDomainAWS fill:#FF9800,color:#000 style TrustDomainGCP fill:#4CAF50,color:#000 style TrustDomainAzure fill:#2196F3,color:#000 style Federation fill:#FFF9C4,stroke:#F57F17 ``` ### Token Exchange(RFC 8693) Token Exchange 允许工作负载将一种 Token 交换为另一种: ```{mermaid} flowchart LR subgraph Source["源系统 (AWS)"] SA2["Service A"] AWSToken["AWS IAM Token"] end subgraph Exchange["Token Exchange Service"] Validate["① 验证源 Token"] Map["② 身份映射"] Issue["③ 签发目标 Token"] end subgraph Target["目标系统 (GCP)"] SB2["Service B"] GCPToken["GCP SA Token"] end SA2 --> AWSToken AWSToken -->|"subject_token"| Validate Validate --> Map Map --> Issue Issue -->|"access_token"| GCPToken GCPToken --> SB2 style Source fill:#FF9800,color:#000 style Exchange fill:#FFF9C4,stroke:#F57F17 style Target fill:#4CAF50,color:#000 ``` ```python import httpx async def exchange_token( subject_token: str, subject_token_type: str, target_audience: str, ) -> str: """OAuth 2.0 Token Exchange (RFC 8693)""" async with httpx.AsyncClient() as client: response = await client.post( "https://token-exchange.example.com/token", data={ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": subject_token, "subject_token_type": subject_token_type, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "audience": target_audience, } ) return response.json()["access_token"] # 将 SPIFFE JWT-SVID 交换为目标系统的 Token new_token = await exchange_token( subject_token=spiffe_jwt_svid, subject_token_type="urn:ietf:params:oauth:token-type:jwt", target_audience="spiffe://gcp.example.com/service-b", ) ``` ## 22.3 Confused Deputy 攻击 ```{mermaid} sequenceDiagram participant Eve as 🦹 攻击者 Eve participant SA3 as Service A
(被迷惑) participant SB3 as Service B Note over Eve,SB3: ❌ 无 Txn-Token 保护 — Confused Deputy 攻击 Eve->>SA3: 伪造请求 SA3->>SB3: 用 Service A 自身权限调用
(不携带调用者身份) SB3-->>SA3: ✅ 执行成功(不应该!) SA3-->>Eve: 返回结果 Note over Eve,SB3: ✅ 有 Txn-Token 保护 — 攻击被阻止 Eve->>SA3: 伪造请求 SA3->>SA3: 创建 Txn-Token
记录 Eve 的身份 SA3->>SB3: 请求 + Txn-Token
(包含 Eve 的身份) SB3->>SB3: 检查 Txn-Token
发现请求者是 Eve SB3-->>SA3: ❌ 拒绝!Eve 无权限 SA3-->>Eve: 请求被拒绝 ``` ## 22.4 WIMSE 与现有标准的关系 | 标准 | 解决的问题 | WIMSE 的关系 | |------|-----------|-------------| | SPIFFE | 工作负载如何获得身份 | 互补:WIMSE 传递 SPIFFE 身份 | | OAuth 2.0 | 委托授权 | 扩展:Token Exchange | | OIDC | 用户身份认证 | 集成:Txn-Token 携带用户身份 | | mTLS | 传输层身份验证 | 补充:应用层身份传递 | ## 22.5 应用场景 ### 多云环境 ```{mermaid} flowchart LR AWSsvc["AWS 服务
spiffe://aws.../a"] -->|"WIT"| TES3["Token Exchange"] TES3 -->|"WIT (已转换)"| GCPsvc["GCP 服务
spiffe://gcp.../b"] style AWSsvc fill:#FF9800,color:#fff style TES3 fill:#FFF9C4,stroke:#F57F17 style GCPsvc fill:#4CAF50,color:#fff ``` ### SaaS-to-SaaS ```{mermaid} flowchart LR SaaSA["SaaS A
用户在 A 中触发操作"] -->|"Txn-Token
(携带用户身份)"| SaaSB["SaaS B
知道请求来自 A 的用户
可以做细粒度授权"] style SaaSA fill:#7E57C2,color:#fff style SaaSB fill:#26A69A,color:#fff ``` ## 22.6 WIMSE vs SPIFFE 对比 | 维度 | WIMSE | SPIFFE | |------|-------|--------| | **定位** | 跨系统身份**传递**与**上下文保持** | 工作负载身份**签发**与**验证** | | **标准组织** | IETF 工作组(草案阶段) | CNCF 毕业项目 | | **核心产物** | WIT、Txn-Token | SVID(X.509 / JWT) | | **身份格式** | JWT(兼容 SPIFFE URI) | SPIFFE ID(URI 格式) | | **信任建立** | 依赖底层身份系统(可以是 SPIFFE) | 自建 Trust Domain + CA | | **调用链追踪** | ✅ Txn-Token 原生支持 | ❌ 不涉及(需额外方案) | | **用户身份传递** | ✅ Txn-Token 携带用户上下文 | ❌ 仅关注工作负载身份 | | **Token 交换** | ✅ 基于 RFC 8693 | ❌ 不涉及(需 Federation) | | **跨云支持** | ✅ 设计目标之一 | ⚠️ 需要 Federation 配置 | | **成熟度** | 草案阶段,概念验证 | 生产就绪,广泛采用 | | **实现** | 尚无官方参考实现 | SPIRE(官方参考实现) | | **关系** | 消费 SPIFFE 身份,增加传递层 | 提供底层身份,被 WIMSE 使用 | > **总结:** SPIFFE 回答"我是谁",WIMSE 回答"我代表谁、为什么调用、经过了哪些系统"。两者是互补关系,WIMSE 建立在 SPIFFE 等身份系统之上。 ## 22.7 多云环境工作负载身份方案对比 | 维度 | WIMSE + SPIFFE | AWS IAM
Roles Anywhere | GCP Workload
Identity Federation | Azure Workload
Identity Federation | HashiCorp Vault | |------|---------------|--------------------------|--------------------------------------|---------------------------------------|-----------------| | **跨云支持** | ✅ 原生跨云 | ⚠️ 仅 AWS 入站 | ⚠️ 仅 GCP 入站 | ⚠️ 仅 Azure 入站 | ✅ 多云 | | **标准化** | IETF + CNCF | AWS 专有 | Google 专有 | Microsoft 专有 | HashiCorp 专有 | | **调用链追踪** | ✅ Txn-Token | ❌ | ❌ | ❌ | ❌ | | **用户上下文** | ✅ 原生支持 | ❌ | ⚠️ 有限 | ⚠️ 有限 | ⚠️ 有限 | | **无密钥** | ✅ SVID 自动轮转 | ✅ X.509 证书 | ✅ OIDC 联邦 | ✅ OIDC 联邦 | ⚠️ 需要初始认证 | | **身份格式** | SPIFFE ID (URI) | IAM Role ARN | Service Account | Managed Identity | Vault Token/Cert | | **部署复杂度** | 🔴 高(需 SPIRE + TES) | 🟡 中 | 🟢 低 | 🟢 低 | 🟡 中 | | **供应商锁定** | 🟢 无 | 🔴 AWS | 🔴 GCP | 🔴 Azure | 🟡 低 | | **成熟度** | 🟡 草案/早期 | 🟢 GA | 🟢 GA | 🟢 GA | 🟢 GA | | **社区生态** | 🟡 成长中 | 🟢 丰富 | 🟢 丰富 | 🟢 丰富 | 🟢 丰富 | > **选型建议:** > - **单云环境**:优先使用云厂商原生方案(IAM Roles Anywhere / Workload Identity Federation) > - **多云 + 简单场景**:HashiCorp Vault 作为统一身份中介 > - **多云 + 复杂调用链**:WIMSE + SPIFFE 提供最完整的跨系统身份传递能力(待标准成熟) ## 22.8 实现示例 ### Go 实现 #### WIMSE Token 创建和验证(使用 go-jose) ```go package wimse import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "errors" "time" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" ) // WIT (Workload Identity Token) 的 Claims type WITClaims struct { jwt.Claims // cnf (confirmation) 用于 Proof-of-Possession Confirmation *Confirmation `json:"cnf,omitempty"` } type Confirmation struct { KeyID string `json:"kid,omitempty"` JWK *jose.JSONWebKey `json:"jwk,omitempty"` } // TxnTokenClaims 表示 Transaction Token 的 Claims type TxnTokenClaims struct { jwt.Claims TransactionID string `json:"txn"` Subject *TxnSubject `json:"sub_id,omitempty"` RequestContext map[string]string `json:"rctx,omitempty"` Purpose string `json:"purp,omitempty"` } type TxnSubject struct { Format string `json:"format"` Issuer string `json:"iss,omitempty"` Subject string `json:"sub,omitempty"` } // WITIssuer 负责签发 Workload Identity Token type WITIssuer struct { issuer string signingKey *ecdsa.PrivateKey keyID string signer jose.Signer } // NewWITIssuer 创建一个新的 WIT 签发器 func NewWITIssuer(issuer string) (*WITIssuer, error) { // 生成 ECDSA P-256 密钥对 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } keyID := "wit-key-" + time.Now().Format("20060102150405") signerOpts := jose.SignerOptions{} signerOpts.WithType("wimse-id+jwt") signerOpts.WithHeader("kid", keyID) signer, err := jose.NewSigner( jose.SigningKey{Algorithm: jose.ES256, Key: privateKey}, &signerOpts, ) if err != nil { return nil, err } return &WITIssuer{ issuer: issuer, signingKey: privateKey, keyID: keyID, signer: signer, }, nil } // IssueWIT 签发一个 Workload Identity Token func (w *WITIssuer) IssueWIT(subject, audience string, ttl time.Duration) (string, error) { now := time.Now() claims := WITClaims{ Claims: jwt.Claims{ Issuer: w.issuer, Subject: subject, Audience: jwt.Audience{audience}, IssuedAt: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(ttl)), NotBefore: jwt.NewNumericDate(now), ID: generateJTI(), }, Confirmation: &Confirmation{ KeyID: w.keyID, }, } token, err := jwt.Signed(w.signer).Claims(claims).Serialize() if err != nil { return "", err } return token, nil } // IssueTxnToken 签发一个 Transaction Token func (w *WITIssuer) IssueTxnToken( txnID string, userIssuer string, userSubject string, audience string, reqCtx map[string]string, purpose string, ) (string, error) { now := time.Now() claims := TxnTokenClaims{ Claims: jwt.Claims{ Issuer: w.issuer, Audience: jwt.Audience{audience}, IssuedAt: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(30 * time.Second)), }, TransactionID: txnID, Subject: &TxnSubject{ Format: "oidc_id_token", Issuer: userIssuer, Subject: userSubject, }, RequestContext: reqCtx, Purpose: purpose, } token, err := jwt.Signed(w.signer).Claims(claims).Serialize() if err != nil { return "", err } return token, nil } // WITVerifier 负责验证 Workload Identity Token type WITVerifier struct { trustedIssuers map[string]*ecdsa.PublicKey } // NewWITVerifier 创建一个新的 WIT 验证器 func NewWITVerifier() *WITVerifier { return &WITVerifier{ trustedIssuers: make(map[string]*ecdsa.PublicKey), } } // AddTrustedIssuer 添加一个受信任的签发者 func (v *WITVerifier) AddTrustedIssuer(issuer string, publicKey *ecdsa.PublicKey) { v.trustedIssuers[issuer] = publicKey } // VerifyWIT 验证一个 Workload Identity Token func (v *WITVerifier) VerifyWIT(tokenString string, expectedAudience string) (*WITClaims, error) { token, err := jwt.ParseSigned(tokenString, []jose.SignatureAlgorithm{jose.ES256}) if err != nil { return nil, err } // 先解析未验证的 claims 以获取 issuer var unverified WITClaims if err := token.UnsafeClaimsWithoutVerification(&unverified); err != nil { return nil, err } // 查找受信任的签发者公钥 publicKey, ok := v.trustedIssuers[unverified.Issuer] if !ok { return nil, errors.New("untrusted issuer: " + unverified.Issuer) } // 使用公钥验证签名并解析 claims var claims WITClaims if err := token.Claims(publicKey, &claims); err != nil { return nil, err } // 验证标准 claims expected := jwt.Expected{ Audience: jwt.Audience{expectedAudience}, Time: time.Now(), } if err := claims.Claims.Validate(expected); err != nil { return nil, err } return &claims, nil } func generateJTI() string { b := make([]byte, 16) rand.Read(b) return fmt.Sprintf("%x", b) } ``` #### Workload Identity Federation 客户端 ```go package wimse import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" ) // TokenExchangeResponse 表示 RFC 8693 Token Exchange 的响应 type TokenExchangeResponse struct { AccessToken string `json:"access_token"` IssuedTokenType string `json:"issued_token_type"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` Scope string `json:"scope,omitempty"` } // FederationClient 实现跨系统的 Workload Identity Federation type FederationClient struct { tokenEndpoint string httpClient *http.Client mu sync.RWMutex cachedToken *TokenExchangeResponse expiresAt time.Time } // NewFederationClient 创建一个新的 Federation 客户端 func NewFederationClient(tokenEndpoint string) *FederationClient { return &FederationClient{ tokenEndpoint: tokenEndpoint, httpClient: &http.Client{ Timeout: 10 * time.Second, }, } } // ExchangeToken 执行 OAuth 2.0 Token Exchange (RFC 8693) func (fc *FederationClient) ExchangeToken( ctx context.Context, subjectToken string, subjectTokenType string, targetAudience string, ) (*TokenExchangeResponse, error) { // 检查缓存 fc.mu.RLock() if fc.cachedToken != nil && time.Now().Before(fc.expiresAt) { token := fc.cachedToken fc.mu.RUnlock() return token, nil } fc.mu.RUnlock() // 构建 Token Exchange 请求 data := url.Values{ "grant_type": {"urn:ietf:params:oauth:grant-type:token-exchange"}, "subject_token": {subjectToken}, "subject_token_type": {subjectTokenType}, "requested_token_type": {"urn:ietf:params:oauth:token-type:access_token"}, "audience": {targetAudience}, } req, err := http.NewRequestWithContext( ctx, "POST", fc.tokenEndpoint, strings.NewReader(data.Encode()), ) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := fc.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("executing token exchange: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("reading response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("token exchange failed (status %d): %s", resp.StatusCode, string(body)) } var tokenResp TokenExchangeResponse if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("parsing response: %w", err) } // 缓存 Token(提前 30 秒过期以留出余量) fc.mu.Lock() fc.cachedToken = &tokenResp fc.expiresAt = time.Now().Add( time.Duration(tokenResp.ExpiresIn-30) * time.Second, ) fc.mu.Unlock() return &tokenResp, nil } // CrossSystemCall 执行跨系统调用,自动处理 Token 交换和 Txn-Token 传递 func (fc *FederationClient) CrossSystemCall( ctx context.Context, targetURL string, localWIT string, txnToken string, targetAudience string, ) (*http.Response, error) { // 1. 将本地 WIT 交换为目标系统可识别的 Token exchanged, err := fc.ExchangeToken( ctx, localWIT, "urn:ietf:params:oauth:token-type:jwt", targetAudience, ) if err != nil { return nil, fmt.Errorf("token exchange: %w", err) } // 2. 构建请求,携带交换后的 Token 和 Txn-Token req, err := http.NewRequestWithContext(ctx, "GET", targetURL, nil) if err != nil { return nil, err } // 交换后的 Token 用于身份认证 req.Header.Set("Authorization", "Bearer "+exchanged.AccessToken) // Txn-Token 用于传递调用链上下文 req.Header.Set("Txn-Token", txnToken) return fc.httpClient.Do(req) } ``` ### Python 实现 #### WIMSE Token 解析器(使用 PyJWT) ```python """WIMSE Token 解析器 — 基于 PyJWT 实现 WIT 和 Txn-Token 的创建与验证""" from __future__ import annotations import uuid import time from dataclasses import dataclass, field from typing import Any import jwt from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.backends import default_backend @dataclass class WITClaims: """Workload Identity Token Claims""" issuer: str subject: str # 通常是 SPIFFE ID audience: str issued_at: float expires_at: float jti: str = field(default_factory=lambda: str(uuid.uuid4())) confirmation: dict | None = None @dataclass class TxnTokenClaims: """Transaction Token Claims""" issuer: str audience: str transaction_id: str subject_format: str # e.g., "oidc_id_token" subject_issuer: str subject_id: str request_context: dict[str, str] = field(default_factory=dict) purpose: str = "" class WIMSETokenIssuer: """签发 WIMSE Token(WIT 和 Txn-Token)""" def __init__(self, issuer: str): self.issuer = issuer # 生成 ECDSA P-256 密钥对 self._private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) self._public_key = self._private_key.public_key() self._key_id = f"wit-key-{uuid.uuid4().hex[:8]}" @property def public_key(self): return self._public_key @property def key_id(self) -> str: return self._key_id def issue_wit( self, subject: str, audience: str, ttl_seconds: int = 300, ) -> str: """签发 Workload Identity Token""" now = time.time() payload = { "iss": self.issuer, "sub": subject, "aud": audience, "iat": int(now), "nbf": int(now), "exp": int(now + ttl_seconds), "jti": str(uuid.uuid4()), "cnf": {"kid": self._key_id}, } return jwt.encode( payload, self._private_key, algorithm="ES256", headers={"typ": "wimse-id+jwt", "kid": self._key_id}, ) def issue_txn_token( self, transaction_id: str, user_issuer: str, user_subject: str, audience: str, request_context: dict[str, str] | None = None, purpose: str = "", ttl_seconds: int = 30, ) -> str: """签发 Transaction Token""" now = time.time() payload = { "iss": self.issuer, "aud": audience, "iat": int(now), "exp": int(now + ttl_seconds), "txn": transaction_id, "sub_id": { "format": "oidc_id_token", "iss": user_issuer, "sub": user_subject, }, "rctx": request_context or {}, "purp": purpose, } return jwt.encode( payload, self._private_key, algorithm="ES256", headers={"typ": "wimse-txn+jwt", "kid": self._key_id}, ) class WIMSETokenVerifier: """验证 WIMSE Token""" def __init__(self): self._trusted_issuers: dict[str, Any] = {} def add_trusted_issuer(self, issuer: str, public_key) -> None: """注册受信任的签发者公钥""" self._trusted_issuers[issuer] = public_key def verify_wit(self, token: str, expected_audience: str) -> dict: """验证 Workload Identity Token""" # 先解码 header 获取 kid,再解码未验证的 payload 获取 issuer unverified = jwt.decode(token, options={"verify_signature": False}) issuer = unverified.get("iss") public_key = self._trusted_issuers.get(issuer) if public_key is None: raise ValueError(f"Untrusted issuer: {issuer}") claims = jwt.decode( token, public_key, algorithms=["ES256"], audience=expected_audience, options={"require": ["iss", "sub", "aud", "exp", "iat", "jti"]}, ) return claims def verify_txn_token(self, token: str, expected_audience: str) -> dict: """验证 Transaction Token""" unverified = jwt.decode(token, options={"verify_signature": False}) issuer = unverified.get("iss") public_key = self._trusted_issuers.get(issuer) if public_key is None: raise ValueError(f"Untrusted issuer: {issuer}") claims = jwt.decode( token, public_key, algorithms=["ES256"], audience=expected_audience, options={"require": ["iss", "aud", "exp", "txn", "sub_id"]}, ) return claims # ── 使用示例 ────────────────────────────────────────────── if __name__ == "__main__": # 签发者 issuer = WIMSETokenIssuer("https://issuer.example.com") # 签发 WIT wit = issuer.issue_wit( subject="spiffe://example.com/service-a", audience="spiffe://example.com/service-b", ) print(f"WIT: {wit[:60]}...") # 签发 Txn-Token txn = issuer.issue_txn_token( transaction_id="txn-abc-123", user_issuer="https://idp.example.com", user_subject="user-alice", audience="https://api.example.com", request_context={"req_method": "POST", "req_path": "/api/transfer"}, purpose="transfer_funds", ) print(f"Txn-Token: {txn[:60]}...") # 验证者 verifier = WIMSETokenVerifier() verifier.add_trusted_issuer("https://issuer.example.com", issuer.public_key) claims = verifier.verify_wit(wit, "spiffe://example.com/service-b") print(f"Verified WIT subject: {claims['sub']}") txn_claims = verifier.verify_txn_token(txn, "https://api.example.com") print(f"Verified Txn-Token txn_id: {txn_claims['txn']}") ``` #### 跨系统身份传播中间件(FastAPI) ```python """WIMSE 跨系统身份传播中间件 — FastAPI 实现""" from __future__ import annotations import uuid import logging from dataclasses import dataclass from typing import Any, Callable import httpx import jwt from fastapi import FastAPI, Request, HTTPException, Depends from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from starlette.middleware.base import BaseHTTPMiddleware logger = logging.getLogger("wimse") # ── 数据模型 ────────────────────────────────────────────── @dataclass class WorkloadIdentity: """当前工作负载的身份信息""" spiffe_id: str issuer: str wit_token: str # 本地 WIT @dataclass class CallerContext: """从 WIT + Txn-Token 中提取的调用者上下文""" workload_id: str # 调用方工作负载 SPIFFE ID transaction_id: str | None original_user: str | None # 原始用户身份 user_issuer: str | None purpose: str | None request_context: dict[str, str] # ── Token 验证 ──────────────────────────────────────────── class WIMSETokenValidator: """验证入站请求中的 WIMSE Token""" def __init__(self, trusted_jwks: dict[str, Any]): """ trusted_jwks: issuer -> public_key 的映射 """ self._trusted_keys = trusted_jwks def validate_wit(self, token: str, expected_audience: str) -> dict: unverified = jwt.decode(token, options={"verify_signature": False}) issuer = unverified.get("iss", "") key = self._trusted_keys.get(issuer) if key is None: raise ValueError(f"Untrusted WIT issuer: {issuer}") return jwt.decode( token, key, algorithms=["ES256"], audience=expected_audience, ) def validate_txn_token(self, token: str, expected_audience: str) -> dict: unverified = jwt.decode(token, options={"verify_signature": False}) issuer = unverified.get("iss", "") key = self._trusted_keys.get(issuer) if key is None: raise ValueError(f"Untrusted Txn-Token issuer: {issuer}") return jwt.decode( token, key, algorithms=["ES256"], audience=expected_audience, ) # ── FastAPI 中间件 ──────────────────────────────────────── class WIMSEMiddleware(BaseHTTPMiddleware): """ WIMSE 身份传播中间件: 1. 从入站请求中提取并验证 WIT 和 Txn-Token 2. 将调用者上下文注入 request.state 3. 为出站请求自动附加 Token """ def __init__(self, app, validator: WIMSETokenValidator, local_audience: str): super().__init__(app) self.validator = validator self.local_audience = local_audience async def dispatch(self, request: Request, call_next): caller_ctx = self._extract_caller_context(request) request.state.caller_context = caller_ctx if caller_ctx and caller_ctx.transaction_id: logger.info( "WIMSE request: txn=%s caller=%s user=%s purpose=%s", caller_ctx.transaction_id, caller_ctx.workload_id, caller_ctx.original_user, caller_ctx.purpose, ) response = await call_next(request) return response def _extract_caller_context(self, request: Request) -> CallerContext | None: auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): return None wit_token = auth_header[7:] txn_token_str = request.headers.get("Txn-Token") try: wit_claims = self.validator.validate_wit(wit_token, self.local_audience) except Exception as e: logger.warning("WIT validation failed: %s", e) return None txn_claims = None if txn_token_str: try: txn_claims = self.validator.validate_txn_token( txn_token_str, self.local_audience, ) except Exception as e: logger.warning("Txn-Token validation failed: %s", e) return CallerContext( workload_id=wit_claims.get("sub", ""), transaction_id=txn_claims.get("txn") if txn_claims else None, original_user=( txn_claims.get("sub_id", {}).get("sub") if txn_claims else None ), user_issuer=( txn_claims.get("sub_id", {}).get("iss") if txn_claims else None ), purpose=txn_claims.get("purp") if txn_claims else None, request_context=txn_claims.get("rctx", {}) if txn_claims else {}, ) # ── 出站请求:身份传播 HTTP 客户端 ─────────────────────── class WIMSEPropagatingClient: """自动传播 WIMSE Token 的 HTTP 客户端""" def __init__( self, local_identity: WorkloadIdentity, token_exchange_url: str, ): self.local_identity = local_identity self.token_exchange_url = token_exchange_url self._client = httpx.AsyncClient(timeout=10.0) async def call( self, method: str, url: str, target_audience: str, txn_token: str | None = None, **kwargs, ) -> httpx.Response: """发起跨系统调用,自动处理 Token 交换和传播""" # 1. Token Exchange: 本地 WIT → 目标系统 Token exchanged = await self._exchange_token(target_audience) # 2. 构建请求头 headers = kwargs.pop("headers", {}) headers["Authorization"] = f"Bearer {exchanged}" if txn_token: headers["Txn-Token"] = txn_token return await self._client.request(method, url, headers=headers, **kwargs) async def _exchange_token(self, target_audience: str) -> str: resp = await self._client.post( self.token_exchange_url, data={ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": self.local_identity.wit_token, "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "audience": target_audience, }, ) resp.raise_for_status() return resp.json()["access_token"] async def close(self): await self._client.aclose() # ── FastAPI 应用示例 ────────────────────────────────────── def get_caller_context(request: Request) -> CallerContext | None: """FastAPI 依赖项:获取 WIMSE 调用者上下文""" return getattr(request.state, "caller_context", None) def require_caller_context(request: Request) -> CallerContext: """FastAPI 依赖项:要求必须有 WIMSE 调用者上下文""" ctx = getattr(request.state, "caller_context", None) if ctx is None: raise HTTPException(status_code=401, detail="Missing WIMSE identity") return ctx # 示例应用 app = FastAPI(title="WIMSE-Protected Service") # 注册中间件(实际使用时需要提供真实的 trusted_jwks) # validator = WIMSETokenValidator(trusted_jwks={...}) # app.add_middleware(WIMSEMiddleware, validator=validator, local_audience="spiffe://example.com/service-b") @app.get("/api/data") async def get_data(caller: CallerContext = Depends(require_caller_context)): """受 WIMSE 保护的 API 端点""" # 可以基于调用者上下文做细粒度授权 if caller.purpose != "read_data": raise HTTPException(status_code=403, detail="Purpose not allowed") return { "data": "sensitive-info", "caller_workload": caller.workload_id, "original_user": caller.original_user, "transaction_id": caller.transaction_id, } @app.get("/api/chain") async def chain_call(caller: CallerContext = Depends(require_caller_context)): """演示调用链传播:接收请求后继续调用下游服务""" # 此处演示如何将 Txn-Token 继续传播到下游 # propagating_client = WIMSEPropagatingClient(...) # response = await propagating_client.call( # "GET", # "https://service-c.example.com/api/resource", # target_audience="spiffe://example.com/service-c", # txn_token=request.headers.get("Txn-Token"), # ) return { "message": "Chain call would propagate Txn-Token to downstream", "transaction_id": caller.transaction_id, } ``` ### Java 实现 #### WIMSE Token 处理(使用 Nimbus JOSE+JWT) ```java package com.example.wimse; import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.*; import com.nimbusds.jose.jwk.*; import com.nimbusds.jose.jwk.gen.*; import com.nimbusds.jwt.*; import java.time.Instant; import java.util.*; /** * WIMSE Token 处理器 — 基于 Nimbus JOSE+JWT * 支持 WIT (Workload Identity Token) 和 Txn-Token 的签发与验证 */ public class WIMSETokenProcessor { private final String issuer; private final ECKey signingKey; private final JWSSigner signer; private final String keyId; public WIMSETokenProcessor(String issuer) throws JOSEException { this.issuer = issuer; this.keyId = "wit-key-" + UUID.randomUUID().toString().substring(0, 8); // 生成 ECDSA P-256 密钥对 this.signingKey = new ECKeyGenerator(Curve.P_256) .keyID(keyId) .generate(); this.signer = new ECDSASigner(signingKey); } /** * 获取公钥(用于分发给验证方) */ public ECKey getPublicKey() { return signingKey.toPublicJWK(); } // ── WIT 签发 ───────────────────────────────────────── /** * 签发 Workload Identity Token * * @param subject 工作负载 SPIFFE ID,如 "spiffe://example.com/service-a" * @param audience 目标工作负载 SPIFFE ID * @param ttlSeconds Token 有效期(秒) * @return 签名后的 JWT 字符串 */ public String issueWIT(String subject, String audience, long ttlSeconds) throws JOSEException { Instant now = Instant.now(); JWTClaimsSet claims = new JWTClaimsSet.Builder() .issuer(issuer) .subject(subject) .audience(audience) .issueTime(Date.from(now)) .notBeforeTime(Date.from(now)) .expirationTime(Date.from(now.plusSeconds(ttlSeconds))) .jwtID(UUID.randomUUID().toString()) .claim("cnf", Map.of("kid", keyId)) .build(); JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256) .type(new JOSEObjectType("wimse-id+jwt")) .keyID(keyId) .build(); SignedJWT signedJWT = new SignedJWT(header, claims); signedJWT.sign(signer); return signedJWT.serialize(); } // ── Txn-Token 签发 ─────────────────────────────────── /** * 签发 Transaction Token */ public String issueTxnToken( String transactionId, String userIssuer, String userSubject, String audience, Map requestContext, String purpose ) throws JOSEException { Instant now = Instant.now(); Map subjectId = Map.of( "format", "oidc_id_token", "iss", userIssuer, "sub", userSubject ); JWTClaimsSet claims = new JWTClaimsSet.Builder() .issuer(issuer) .audience(audience) .issueTime(Date.from(now)) .expirationTime(Date.from(now.plusSeconds(30))) .claim("txn", transactionId) .claim("sub_id", subjectId) .claim("rctx", requestContext) .claim("purp", purpose) .build(); JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256) .type(new JOSEObjectType("wimse-txn+jwt")) .keyID(keyId) .build(); SignedJWT signedJWT = new SignedJWT(header, claims); signedJWT.sign(signer); return signedJWT.serialize(); } // ── Token 验证 ─────────────────────────────────────── /** * WIMSE Token 验证器(静态内部类) */ public static class Verifier { private final Map trustedIssuers = new HashMap<>(); public void addTrustedIssuer(String issuer, ECKey publicKey) { trustedIssuers.put(issuer, publicKey); } /** * 验证 Workload Identity Token */ public JWTClaimsSet verifyWIT(String tokenString, String expectedAudience) throws Exception { SignedJWT signedJWT = SignedJWT.parse(tokenString); JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); // 查找受信任的签发者 String iss = claims.getIssuer(); ECKey publicKey = trustedIssuers.get(iss); if (publicKey == null) { throw new SecurityException("Untrusted issuer: " + iss); } // 验证签名 JWSVerifier verifier = new ECDSAVerifier(publicKey); if (!signedJWT.verify(verifier)) { throw new SecurityException("Invalid WIT signature"); } // 验证 audience if (!claims.getAudience().contains(expectedAudience)) { throw new SecurityException("Audience mismatch"); } // 验证过期时间 if (claims.getExpirationTime().before(new Date())) { throw new SecurityException("WIT has expired"); } return claims; } /** * 验证 Transaction Token */ public JWTClaimsSet verifyTxnToken(String tokenString, String expectedAudience) throws Exception { SignedJWT signedJWT = SignedJWT.parse(tokenString); JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); String iss = claims.getIssuer(); ECKey publicKey = trustedIssuers.get(iss); if (publicKey == null) { throw new SecurityException("Untrusted Txn-Token issuer: " + iss); } JWSVerifier verifier = new ECDSAVerifier(publicKey); if (!signedJWT.verify(verifier)) { throw new SecurityException("Invalid Txn-Token signature"); } if (!claims.getAudience().contains(expectedAudience)) { throw new SecurityException("Audience mismatch"); } if (claims.getExpirationTime().before(new Date())) { throw new SecurityException("Txn-Token has expired"); } // 验证必需字段 if (claims.getStringClaim("txn") == null) { throw new SecurityException("Missing txn claim"); } if (claims.getClaim("sub_id") == null) { throw new SecurityException("Missing sub_id claim"); } return claims; } } // ── 使用示例 ───────────────────────────────────────── public static void main(String[] args) throws Exception { // 创建签发器 WIMSETokenProcessor processor = new WIMSETokenProcessor("https://issuer.example.com"); // 签发 WIT String wit = processor.issueWIT( "spiffe://example.com/service-a", "spiffe://example.com/service-b", 300 ); System.out.println("WIT: " + wit.substring(0, 60) + "..."); // 签发 Txn-Token String txnToken = processor.issueTxnToken( "txn-abc-123", "https://idp.example.com", "user-alice", "https://api.example.com", Map.of("req_method", "POST", "req_path", "/api/transfer"), "transfer_funds" ); System.out.println("Txn-Token: " + txnToken.substring(0, 60) + "..."); // 验证 Verifier verifier = new Verifier(); verifier.addTrustedIssuer("https://issuer.example.com", processor.getPublicKey()); JWTClaimsSet witClaims = verifier.verifyWIT( wit, "spiffe://example.com/service-b"); System.out.println("Verified WIT subject: " + witClaims.getSubject()); JWTClaimsSet txnClaims = verifier.verifyTxnToken( txnToken, "https://api.example.com"); System.out.println("Verified Txn-Token txn: " + txnClaims.getStringClaim("txn")); } } ``` #### Spring Boot Workload Identity 集成 ```java package com.example.wimse.spring; import com.nimbusds.jose.crypto.ECDSAVerifier; import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.*; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.*; /** * Spring Boot WIMSE Workload Identity 集成 */ @SpringBootApplication public class WIMSEApplication { public static void main(String[] args) { SpringApplication.run(WIMSEApplication.class, args); } } // ── 认证 Token 模型 ────────────────────────────────────── /** * WIMSE 认证 Token — 封装 WIT 和 Txn-Token 的验证结果 */ class WIMSEAuthenticationToken extends AbstractAuthenticationToken { private final String workloadId; // 调用方 SPIFFE ID private final String transactionId; // 事务 ID private final String originalUser; // 原始用户 private final String purpose; // 调用目的 private final Map requestContext; public WIMSEAuthenticationToken( String workloadId, String transactionId, String originalUser, String purpose, Map requestContext, Collection authorities ) { super(authorities); this.workloadId = workloadId; this.transactionId = transactionId; this.originalUser = originalUser; this.purpose = purpose; this.requestContext = requestContext != null ? requestContext : Map.of(); setAuthenticated(true); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return workloadId; } public String getWorkloadId() { return workloadId; } public String getTransactionId() { return transactionId; } public String getOriginalUser() { return originalUser; } public String getPurpose() { return purpose; } public Map getRequestContext() { return requestContext; } } // ── WIMSE 认证过滤器 ───────────────────────────────────── /** * Spring Security 过滤器:验证入站请求中的 WIT 和 Txn-Token */ @Component class WIMSEAuthenticationFilter extends OncePerRequestFilter { @Value("${wimse.local-audience:}") private String localAudience; // 实际应用中应从 JWKS 端点或配置加载 private final Map trustedIssuers = new HashMap<>(); public void addTrustedIssuer(String issuer, ECKey publicKey) { trustedIssuers.put(issuer, publicKey); } @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String txnTokenHeader = request.getHeader("Txn-Token"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String witToken = authHeader.substring(7); try { WIMSEAuthenticationToken auth = validateAndBuild(witToken, txnTokenHeader); SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception e) { logger.warn("WIMSE authentication failed: " + e.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write( "{\"error\":\"invalid_token\",\"detail\":\"" + e.getMessage() + "\"}" ); return; } } filterChain.doFilter(request, response); } private WIMSEAuthenticationToken validateAndBuild( String witToken, String txnTokenStr ) throws Exception { // 验证 WIT SignedJWT witJWT = SignedJWT.parse(witToken); JWTClaimsSet witClaims = witJWT.getJWTClaimsSet(); ECKey publicKey = trustedIssuers.get(witClaims.getIssuer()); if (publicKey == null) { throw new SecurityException("Untrusted issuer: " + witClaims.getIssuer()); } if (!witJWT.verify(new ECDSAVerifier(publicKey))) { throw new SecurityException("Invalid WIT signature"); } if (witClaims.getExpirationTime().before(new Date())) { throw new SecurityException("WIT expired"); } String workloadId = witClaims.getSubject(); String transactionId = null; String originalUser = null; String purpose = null; Map rctx = Map.of(); // 验证 Txn-Token(如果存在) if (txnTokenStr != null && !txnTokenStr.isBlank()) { SignedJWT txnJWT = SignedJWT.parse(txnTokenStr); JWTClaimsSet txnClaims = txnJWT.getJWTClaimsSet(); ECKey txnKey = trustedIssuers.get(txnClaims.getIssuer()); if (txnKey != null && txnJWT.verify(new ECDSAVerifier(txnKey))) { transactionId = txnClaims.getStringClaim("txn"); purpose = txnClaims.getStringClaim("purp"); @SuppressWarnings("unchecked") Map subId = (Map) txnClaims.getClaim("sub_id"); if (subId != null) { originalUser = (String) subId.get("sub"); } @SuppressWarnings("unchecked") Map ctx = (Map) txnClaims.getClaim("rctx"); if (ctx != null) { rctx = ctx; } } } // 根据工作负载 ID 分配权限 List authorities = resolveAuthorities(workloadId, purpose); return new WIMSEAuthenticationToken( workloadId, transactionId, originalUser, purpose, rctx, authorities ); } private List resolveAuthorities(String workloadId, String purpose) { List authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_WORKLOAD")); // 基于 SPIFFE ID 和目的分配权限(示例策略) if (workloadId.contains("/service-a")) { authorities.add(new SimpleGrantedAuthority("ROLE_SERVICE_A")); } if ("read_data".equals(purpose)) { authorities.add(new SimpleGrantedAuthority("SCOPE_READ")); } if ("transfer_funds".equals(purpose)) { authorities.add(new SimpleGrantedAuthority("SCOPE_WRITE")); } return authorities; } } // ── Security 配置 ──────────────────────────────────────── @Configuration class WIMSESecurityConfig { @Bean public SecurityFilterChain filterChain( HttpSecurity http, WIMSEAuthenticationFilter wimseFilter ) throws Exception { http .csrf(csrf -> csrf.disable()) .addFilterBefore(wimseFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(auth -> auth .requestMatchers("/health", "/ready").permitAll() .requestMatchers("/api/**").hasRole("WORKLOAD") .anyRequest().denyAll() ); return http.build(); } } // ── 出站请求:Token 交换 + 身份传播 ───────────────────── /** * WIMSE 身份传播 RestTemplate 拦截器 * 自动为出站请求执行 Token Exchange 并附加 Txn-Token */ @Component class WIMSEPropagatingRestTemplate { @Value("${wimse.token-exchange-url:}") private String tokenExchangeUrl; @Value("${wimse.local-wit:}") private String localWIT; private final RestTemplate restTemplate = new RestTemplate(); /** * 执行跨系统调用,自动处理 Token 交换和 Txn-Token 传播 */ public ResponseEntity callWithIdentity( String url, HttpMethod method, String targetAudience, String txnToken, Class responseType ) { // 1. Token Exchange String exchangedToken = exchangeToken(localWIT, targetAudience); // 2. 构建请求头 HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(exchangedToken); if (txnToken != null) { headers.set("Txn-Token", txnToken); } HttpEntity entity = new HttpEntity<>(headers); return restTemplate.exchange(url, method, entity, responseType); } private String exchangeToken(String subjectToken, String targetAudience) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); String body = String.join("&", "grant_type=urn:ietf:params:oauth:grant-type:token-exchange", "subject_token=" + subjectToken, "subject_token_type=urn:ietf:params:oauth:token-type:jwt", "requested_token_type=urn:ietf:params:oauth:token-type:access_token", "audience=" + targetAudience ); HttpEntity request = new HttpEntity<>(body, headers); ResponseEntity response = restTemplate.exchange( tokenExchangeUrl, HttpMethod.POST, request, Map.class ); return (String) Objects.requireNonNull(response.getBody()).get("access_token"); } } // ── 示例 Controller ────────────────────────────────────── @RestController @RequestMapping("/api") class WIMSEExampleController { /** * 受 WIMSE 保护的 API 端点 */ @GetMapping("/data") public Map getData() { WIMSEAuthenticationToken auth = (WIMSEAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); return Map.of( "data", "sensitive-info", "caller_workload", auth.getWorkloadId(), "original_user", auth.getOriginalUser() != null ? auth.getOriginalUser() : "unknown", "transaction_id", auth.getTransactionId() != null ? auth.getTransactionId() : "none", "purpose", auth.getPurpose() != null ? auth.getPurpose() : "unspecified" ); } /** * 演示 Confused Deputy 防护 */ @PostMapping("/transfer") public Map transfer(@RequestBody Map body) { WIMSEAuthenticationToken auth = (WIMSEAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); // 检查 Txn-Token 中的目的是否匹配 if (!"transfer_funds".equals(auth.getPurpose())) { return Map.of( "error", "forbidden", "detail", "Purpose mismatch: expected transfer_funds, got " + auth.getPurpose() ); } // 检查原始用户是否有权限 String user = auth.getOriginalUser(); if (user == null) { return Map.of("error", "forbidden", "detail", "No user context in Txn-Token"); } return Map.of( "status", "success", "authorized_by", "WIMSE Txn-Token", "original_user", user, "caller_workload", auth.getWorkloadId(), "transaction_id", auth.getTransactionId() ); } } ``` ## 22.9 小结 - **WIMSE** 解决跨系统环境中的工作负载身份传递问题 - **WIT** 标识工作负载自身身份,**Txn-Token** 传递调用链上下文 - **Token Exchange** 实现跨系统的 Token 转换 - WIMSE 与 SPIFFE **互补**:SPIFFE 提供身份,WIMSE 传递身份 - **Confused Deputy** 攻击是 WIMSE 要解决的核心安全问题 - WIMSE 标准仍在 IETF 制定中,但核心概念已经清晰 - 在多云环境中,WIMSE + SPIFFE 提供了最完整的跨系统身份传递方案,但需要根据实际成熟度和复杂度权衡选型