# 第二十二章: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 extends GrantedAuthority> 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