# 在 SPIFFE 中使用 WIF

使用来自 SPIRE 或任何其他 SPIFFE 兼容签发方的 JWT-SVID 将 SPIFFE 工作负载认证到 Claude API。

---

[SPIFFE](https://spiffe.io/) 是 CNCF 为工作负载签发身份的标准。[SPIRE](https://spiffe.io/docs/latest/spire-about/) 是其开源参考实现，多个商业产品也签发 SPIFFE 兼容的身份。Anthropic 与任何发出 OIDC 兼容 JWT-SVID 的 SPIFFE 实现联合。联合可以通过公共 HTTPS URL 的 OIDC 发现文档（`discovery` 模式；参阅 [URL 约束](/docs/en/manage-claude/wif-reference#url-fields)）或直接注册 JWKS（`inline` 模式）来实现。JWT-SVID 规范将 `sub` 定义为工作负载的 SPIFFE ID，SPIFFE Workload API 要求调用者在获取时提供 `aud`，因此这些声明在各实现中是相同的。Anthropic 还要求 `iss` 和 `iat`，而 JWT-SVID 规范并未强制要求这两个字段，因此请配置你的实现来填充它们（在 SPIRE 中，`iss` 是 `jwt_issuer` 服务器设置，`iat` 会自动设置）。有了这些，本指南的[配置 Anthropic](#configure-anthropic)、[获取并使用令牌](#acquire-and-use-the-token)和[限定规则范围](#scope-your-rule)部分适用于任何 SPIFFE 实现。有关当前列表，请参阅 SPIFFE 项目网站上的[实现 SPIFFE 的商业软件](https://spiffe.io/docs/latest/spiffe-about/overview/#commercial-software-that-implements-spiffe)。

SPIFFE 为每个工作负载分配一个格式为 `spiffe://<trust-domain>/` 的稳定身份 URI，SPIRE 通过 Workload API 按需将该身份签发为 JWT-SVID。JWT-SVID 是普通的签名 JWT，其 `sub` 声明是工作负载的 SPIFFE ID，`aud` 声明由工作负载在获取时提供。

从 SPIRE 信任域到标准 OIDC 的桥梁是 [SPIRE OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md)，一个独立的辅助程序，为信任域的 JWT 签名密钥发布 `/.well-known/openid-configuration` 和 JWKS 端点。有了发现提供者，JWT-SVID 的验证方式与其他任何 OIDC 令牌相同：将发现 URL 注册为联合签发方，编写匹配工作负载 SPIFFE ID 的联合规则，然后让工作负载将其 JWT-SVID 出示给 Anthropic 的令牌交换端点。

本页的示例使用 SPIRE，适用于 SPIRE Agent 运行的任何地方：Kubernetes Pod、虚拟机和裸金属主机。

<Note>
如果你的 Kubernetes 集群没有运行 SPIRE，且你想改用集群原生的投影服务账号令牌进行认证，请参阅[在 Kubernetes 中使用 WIF](/docs/en/manage-claude/wif-providers/kubernetes)。
</Note>

## 前置条件

- 熟悉 [WIF 概念](/docs/en/manage-claude/workload-identity-federation#concepts)：服务账号、联合签发方和联合规则。
- 一个已签发工作负载身份的 SPIFFE 部署（本页示例使用 SPIRE Server 和 Agent），以及需要调用 Claude API 的工作负载的注册条目。
- 信任域的 OIDC 发现端点（在 SPIRE 中为 [OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md)），运行在可公开访问的 HTTPS 端点上，或者已导出 JWKS 用于 `inline` 注册。
- 你的 SPIFFE 签发方配置为在 JWT-SVID 上将 `iss` 声明设置为你将注册为联合签发方 `issuer_url` 的值。对于 `discovery` 模式，这是发现端点的公共 URL（在 SPIRE 中为 `jwt_issuer` 服务器设置）。
- JWT-SVID 可供你的工作负载使用。WIF 仅接受 JWT-SVID；X.509-SVID 不被使用。
- 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。

获取 JWT-SVID 时请求的受众值始终为 `https://api.anthropic.com`。在 spiffe-helper 的 `jwt_audience`、Workload API `FetchJWTSVID` 调用和联合规则的 `audience` 匹配器中使用此值。

## 配置 SPIRE

本节的说明是 SPIRE 特定的。如果你使用不同的 SPIFFE 签发方，请按照其文档配置其 OIDC 发现端点和 JWT-SVID 检索，然后继续[配置 Anthropic](#configure-anthropic)。

如果你已经在运行带有 OIDC Discovery Provider 的 SPIRE，与 Anthropic 联合需要在 SPIRE 侧完成三件事：一个与发现 URL 匹配的 `jwt_issuer`、一个将调用 Claude API 的工作负载的注册条目，以及让该工作负载使用 Anthropic 受众获取 JWT-SVID 的方式。以下小节逐一介绍。配置片段仅显示与 Anthropic 联合相关的设置；它们不是完整的 SPIRE 部署配置。

<Tip>
首次设置 SPIRE？按照 [SPIRE 快速入门](https://spiffe.io/docs/latest/try/)部署 SPIRE Server 和 Agent，然后将 [OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md) 作为独立服务与 SPIRE Server 一起部署。发现模式联合依赖于提供者的部署和公开可达性；它不是默认 SPIRE 安装的一部分。
</Tip>

### 验证 JWT 签发方

Anthropic 通过将 JWT-SVID 的 `iss` 声明与注册的联合签发方匹配，并从该签发方的发现文档获取 JWKS 来验证 JWT-SVID。两个 SPIRE 设置必须在同一 URL 上达成一致：SPIRE Server 的 `jwt_issuer`（成为每个铸造的 JWT-SVID 中的 `iss` 声明）和 OIDC Discovery Provider 的 `domains` 列表（决定发现文档和 JWKS 的服务主机）。该共享 URL 就是你向 Anthropic 注册的 URL。

信任域和签发方 URL 是独立的。信任域（`spiffe://prod.example.com`）限定 `sub` 声明的范围；签发方 URL（`https://oidc-discovery.prod.example.com`）是 Anthropic 获取签名密钥的地方。它们不需要共享主机名。

确认 `jwt_issuer` 在 SPIRE Server 的配置中已设置并指向发现提供者的公共 URL。以下示例还展示了默认的 JWT-SVID 生命周期；SPIRE 的内置默认值为 5 分钟，足够短以至于需要持续轮换（参阅[运行 spiffe-helper](#run-spiffe-helper)）。Anthropic 的令牌交换端点会拒绝任何生命周期超过联合签发方配置的最大值的标识令牌（默认为 1 小时；参阅[验证规则](/docs/en/manage-claude/wif-reference#validation-rules)）。此检查适用于每个 SPIFFE 实现，不仅仅是 SPIRE，因此请将 `default_jwt_svid_ttl`（或任何每个条目的覆盖值）保持在该最大值或以下。

```text server.conf
server {
    trust_domain         = "prod.example.com"
    jwt_issuer           = "https://oidc-discovery.prod.example.com"
    default_jwt_svid_ttl = "5m"
    # ...
}
```

在 OIDC Discovery Provider 的配置中，相同的主机名必须出现在 `domains` 下，且提供者必须能够访问 SPIRE Server 的 API 套接字。提供者通过 HTTPS 提供发现文档和 JWKS；使用其内置的 ACME 支持终止 TLS，或在其前面放置一个执行 TLS 终止的负载均衡器。

```text oidc-discovery-provider.conf
domains = ["oidc-discovery.prod.example.com"]

server_api {
    address = "unix:///run/spire/sockets/private/api.sock"
}

acme {
    email        = "platform@example.com"
    tos_accepted = true
}
```

<Note>
示例使用 `server_api`，它将发现提供者连接到 SPIRE Server 的特权 API 套接字。提供者还接受 `workload_api` 块（带 `socket_path` 和 `trust_domain`），通过 SPIRE Agent 的 Workload API 获取捆绑包；当发现提供者不应访问 Server API 或运行在无法访问 Server 的节点上时使用它。在 Windows 上，`address` 字段仅支持 Unix；请改用 `server_api { experimental { named_pipe_name = "\\spire-server\\private\\api" } }` 提供 Server API 管道名称。
</Note>

### 注册工作负载

每个调用 Claude API 的工作负载都需要一个 SPIRE 注册条目，将其运行时选择器映射到 SPIFFE ID。如果工作负载已注册，请记下其 SPIFFE ID；你将在联合规则的 `subject_prefix` 中使用它。如果没有，请注册它。对于 Kubernetes Pod，选择器通常是命名空间和 Kubernetes 服务账号：

```bash CLI nocheck
spire-server entry create \
    -spiffeID spiffe://prod.example.com/ns/inference/sa/worker \
    -parentID spiffe://prod.example.com/spire/agent/k8s_psat/prod-cluster/NODE_UID \
    -selector k8s:ns:inference \
    -selector k8s:sa:worker
```

<Note>
所示的 `parentID` 是单个节点的自动生成的代理 ID。对于集群范围的注册，将条目父级设为[节点别名](https://spiffe.io/docs/latest/deploying/registering/#mapping-workloads-to-multiple-nodes)，以便匹配每个节点上的工作负载，如 [SPIRE Kubernetes 快速入门](https://spiffe.io/docs/latest/try/getting-started-k8s/)所示。
</Note>

Kubernetes 外部的工作负载使用主机级选择器，如 `unix:uid:1000`（`unix:path` 也可用，但需要在代理的 unix workload attestor 配置中设置 `discover_workload_path = true`）。运行 [spire-controller-manager](https://github.com/spiffe/spire-controller-manager) 的集群可以使用 `ClusterSPIFFEID` 自定义资源声明条目，而不是直接调用 `spire-server entry create`。

### 运行 spiffe-helper

[spiffe-helper](https://github.com/spiffe/spiffe-helper) 是一个 sidecar 工具，连接到 SPIRE Agent 套接字，为给定受众获取 JWT-SVID，将其写入文件，并在过期前重新获取。该辅助程序默认以守护进程模式运行；下面的示例显式设置了 `daemon_mode = true`。

```text helper.conf
agent_address = "/run/spire/sockets/agent.sock"
cert_dir      = "/var/run/secrets/anthropic.com"
daemon_mode   = true

jwt_svids = [{
    jwt_audience       = "https://api.anthropic.com"
    jwt_svid_file_name = "token"
}]
```

在 Kubernetes 中，将 spiffe-helper 作为 sidecar 容器运行，与你的应用容器共享基于内存的 `emptyDir` 卷（`medium: Memory`），这样 Bearer SVID 永远不会落盘。将 SPIRE Agent 套接字从主机挂载到 sidecar 中，在两个容器中将共享卷挂载到 `/var/run/secrets/anthropic.com`，并在应用容器上设置 `ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic.com/token`。在 VM 和裸金属上，将 spiffe-helper 作为系统服务与工作负载一起运行，并将两者指向共享目录。

## 配置 Anthropic

按照[设置指南](/docs/en/manage-claude/workload-identity-federation#set-up-federation)在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 SPIFFE 特定的值。

**联合签发方：** 以 `discovery` 模式注册 OIDC Discovery Provider 的公共 URL。Anthropic 从此 URL 获取 `/.well-known/openid-configuration`，并按照返回的 `jwks_uri` 检索信任域的签名密钥。

```json
{
  "name": "spire-prod",
  "issuer_url": "https://oidc-discovery.prod.example.com",
  "jwks": { "type": "discovery" }
}
```

如果发现提供者无法从公共互联网访问，请自行获取 JWKS（`curl https://oidc-discovery.prod.example.com/keys`），并使用返回的 `keys` 数组的内容以 `"jwks": {"type": "inline", "keys": [...]}` 注册签发方。在 `inline` 模式下，`issuer_url` 仅与 JWT-SVID 的 `iss` 声明进行比较；Anthropic 永远不会尝试访问它。

<Warning>
SPIRE 频繁轮换 JWT 签名密钥，默认与 CA 相同的节奏（`ca_ttl`，24 小时）。如果你使用内联 JWKS 而非发现 URL 注册签发方，你必须在每次 SPIRE 轮换时更新 JWKS：在工作负载开始出示新密钥之前添加新密钥，并在使用旧密钥签名的令牌过期后**删除已取代的密钥**。留在内联 JWKS 中的过期密钥将无限期受信任。
</Warning>

要在不暴露公共发现端点的情况下自动化 JWKS 更新，请配置 SPIRE Server [BundlePublisher](https://spiffe.io/docs/latest/deploying/spire_server/#built-in-plugins) 插件（`aws_s3`、`gcp_cloudstorage` 或 `k8s_configmap`），设置 `format = "jwks"` 以在每次轮换时将 JWT 签名密钥推送到外部存储，然后将其同步到签发方的内联密钥中。

**联合规则：** 匹配 JWT-SVID 的 `sub`（SPIFFE ID）和你配置 spiffe-helper 请求的 `aud`。SPIFFE ID 是 URI 字符串，`subject_prefix` 将其作为不透明文本匹配，因此精确值或末尾带 `*` 的前缀匹配都适用。对于更复杂的模式，使用 CEL `condition`。

```json
{
  "name": "spire-inference-worker",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "spiffe://prod.example.com/ns/inference/sa/worker",
    "audience": "https://api.anthropic.com"
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

尽可能精确地匹配工作负载。只有当该路径下注册的每个工作负载都需要映射到同一个 Anthropic 服务账号时，才将 `subject_prefix` 放宽为 `spiffe://prod.example.com/ns/inference/*`。将规则的 `fdrl_...` ID 添加到工作负载的 `ANTHROPIC_FEDERATION_RULE_ID` 环境变量中。

## 获取并使用令牌

Anthropic SDK 可以从 spiffe-helper 维护的文件中读取 JWT-SVID，也可以通过令牌提供者可调用对象直接调用 SPIFFE Workload API。文件路径是最简单的集成方式，适用于每种 SDK 语言；可调用路径去除了 sidecar，但需要在你的应用程序语言中使用 SPIFFE Workload API 客户端。

<Tabs>
  <Tab title="基于文件的方式（使用 spiffe-helper）">
    当 spiffe-helper 将新的 JWT-SVID 写入 `/var/run/secrets/anthropic.com/token` 时，将 `ANTHROPIC_IDENTITY_TOKEN_FILE` 设置为该路径，同时设置 `ANTHROPIC_FEDERATION_RULE_ID`、`ANTHROPIC_ORGANIZATION_ID`、`ANTHROPIC_SERVICE_ACCOUNT_ID` 和 `ANTHROPIC_WORKSPACE_ID`。SDK 在每次令牌交换时读取文件，因此它总是获取最近轮换的 SVID，并在过期前自动刷新 Anthropic 访问令牌。

    <CodeGroup>

    

    ```bash cURL nocheck
    JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")

    ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \
      -H "content-type: application/json" \
      --data @- <<JSON | jq -r .access_token
    {
      "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
      "assertion": "$JWT",
      "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
      "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
      "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
      "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
    }
    JSON
    )

    curl https://api.anthropic.com/v1/messages \
      -H "authorization: Bearer $ACCESS_TOKEN" \
      -H "anthropic-version: 2023-06-01" \
      -H "content-type: application/json" \
      -d '{
        "model": "claude-sonnet-4-6",
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": "Hello, Claude"}]
      }' | jq -r '.content[0].text'
    ```

    

    ```python Python nocheck
    import anthropic

    # 读取 spiffe-helper 写入 ANTHROPIC_IDENTITY_TOKEN_FILE 的 JWT-SVID，
    # 以及 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
    # ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
    client = anthropic.Anthropic()

    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hello, Claude"}],
    )
    print(message.content[0].text)
    ```

    

    ```typescript TypeScript nocheck
    import Anthropic from "@anthropic-ai/sdk";

    // 读取 spiffe-helper 写入 ANTHROPIC_IDENTITY_TOKEN_FILE 的 JWT-SVID，
    // 以及 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
    // ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
    const client = new Anthropic();

    const message = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      messages: [{ role: "user", content: "Hello, Claude" }]
    });
    for (const block of message.content) {
      if (block.type === "text") {
        console.log(block.text);
      }
    }
    ```

    

    ```go Go nocheck hidelines={1..10,-1}
    package main

    import (
    	"context"
    	"fmt"

    	"github.com/anthropics/anthropic-sdk-go"
    )

    func main() {
    	// 读取 spiffe-helper 写入 ANTHROPIC_IDENTITY_TOKEN_FILE 的 JWT-SVID，
    	// 以及 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
    	// ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
    	client := anthropic.NewClient()

    	message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
    		Model:     anthropic.ModelClaudeSonnet4_6,
    		MaxTokens: 1024,
    		Messages: []anthropic.MessageParam{
    			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
    		},
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(message.Content[0].Text)
    }
    ```

    

    ```java Java nocheck hidelines={1..6,-1}
    import com.anthropic.client.AnthropicClient;
    import com.anthropic.client.okhttp.AnthropicOkHttpClient;
    import com.anthropic.models.messages.MessageCreateParams;
    import com.anthropic.models.messages.Model;

    void main() {
        AnthropicClient client = AnthropicOkHttpClient.fromEnv();

        var message = client.messages().create(MessageCreateParams.builder()
                .model(Model.CLAUDE_SONNET_4_6)
                .maxTokens(1024)
                .addUserMessage("Hello, Claude")
                .build());

        IO.println(message.content());
    }
    ```

    

    ```csharp C# nocheck hidelines={1..3}
    using Anthropic.Models.Messages;
    using Anthropic.Oidc;

    var result = AnthropicCredentials.Resolve()
        ?? throw new InvalidOperationException("No federation credentials found in environment");
    using var client = new AnthropicOidcClient(result);

    var message = await client.Messages.Create(new()
    {
        Model = Model.ClaudeSonnet4_6,
        MaxTokens = 1024,
        Messages = [new() { Role = Role.User, Content = "Hello, Claude" }],
    });
    foreach (var block in message.Content)
    {
        if (block.Value is TextBlock textBlock)
        {
            Console.WriteLine(textBlock.Text);
        }
    }
    ```

    

    ```bash CLI nocheck
    # 读取 spiffe-helper 写入 ANTHROPIC_IDENTITY_TOKEN_FILE 的 JWT-SVID，
    # 以及 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
    # ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
    ant messages create \
      --model claude-sonnet-4-6 \
      --max-tokens 1024 \
      --message '{role: user, content: "Hello, Claude"}'
    ```

    

    ```php PHP nocheck hidelines={1..3}
    <?php
    require 'vendor/autoload.php';

    use Anthropic\Client;

    // 读取 spiffe-helper 写入 ANTHROPIC_IDENTITY_TOKEN_FILE 的 JWT-SVID，
    // 以及 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
    // ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
    $client = new Client();

    $message = $client->messages->create(
        model: 'claude-sonnet-4-6',
        maxTokens: 1024,
        messages: [['role' => 'user', 'content' => 'Hello, Claude']],
    );
    echo $message->content[0]->text, PHP_EOL;
    ```

    

    ```ruby Ruby nocheck
    require "anthropic"

    # 读取 spiffe-helper 写入 ANTHROPIC_IDENTITY_TOKEN_FILE 的 JWT-SVID，
    # 以及 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
    # ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
    client = Anthropic::Client.new

    message = client.messages.create(
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      messages: [{role: "user", content: "Hello, Claude"}]
    )
    puts message.content.first.text
    ```

    </CodeGroup>
  </Tab>

  <Tab title="通过 SPIFFE Workload API 的可调用方式">
    直接链接 SPIFFE Workload API 客户端的工作负载可以跳过 spiffe-helper，向 SDK 传递一个从代理套接字获取新 JWT-SVID 的可调用对象。SDK 在每次令牌交换前调用该可调用对象，因此工作负载总是出示未过期的 SVID。Go（[go-spiffe](https://github.com/spiffe/go-spiffe)）和 Python（[py-spiffe](https://github.com/HewlettPackard/py-spiffe)）有成熟的 Workload API 客户端。

    
    <CodeGroup>

    

    ```go Go nocheck hidelines={1..14,-1}
    package main

    import (
    	"context"
    	"fmt"
    	"os"

    	"github.com/anthropics/anthropic-sdk-go"
    	"github.com/anthropics/anthropic-sdk-go/option"
    	"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
    	"github.com/spiffe/go-spiffe/v2/workloadapi"
    )

    func main() {
    	const audience = "https://api.anthropic.com"

    	ctx := context.Background()
    	source, err := workloadapi.NewJWTSource(ctx)
    	if err != nil {
    		panic(err)
    	}
    	defer source.Close()

    	fetchJWTSVID := func(ctx context.Context) (string, error) {
    		svid, err := source.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience})
    		if err != nil {
    			return "", err
    		}
    		return svid.Marshal(), nil
    	}

    	client := anthropic.NewClient(
    		option.WithFederationTokenProvider(fetchJWTSVID, option.FederationOptions{
    			FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
    			OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
    			ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
    			WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
    		}),
    	)

    	message, err := client.Messages.New(ctx, anthropic.MessageNewParams{
    		Model:     anthropic.ModelClaudeSonnet4_6,
    		MaxTokens: 1024,
    		Messages: []anthropic.MessageParam{
    			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
    		},
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(message.Content[0].Text)
    }
    ```

    

    ```python Python nocheck
    import os
    import anthropic
    from anthropic import WorkloadIdentityCredentials
    from spiffe import JwtSource

    AUDIENCE = "https://api.anthropic.com"

    # 连接到 SPIFFE_ENDPOINT_SOCKET 处的 SPIRE Agent 套接字。
    jwt_source = JwtSource()


    def fetch_jwt_svid() -> str:
        svid = jwt_source.fetch_svid(audience={AUDIENCE})
        return svid.token


    client = anthropic.Anthropic(
        credentials=WorkloadIdentityCredentials(
            identity_token_provider=fetch_jwt_svid,
            federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
            organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
            service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
            workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
        ),
    )

    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hello, Claude"}],
    )
    print(message.content[0].text)
    ```

    </CodeGroup>

    <Note>
    对于其他语言，请使用运行时的 SPIFFE Workload API 客户端获取 JWT-SVID（或调用 `spire-agent api fetch jwt`），将其写入文件，并按照基于文件的标签设置 `ANTHROPIC_IDENTITY_TOKEN_FILE` 为该路径。
    </Note>
  </Tab>
</Tabs>

## 验证设置

在接入 SDK 之前，直接从 SPIRE Agent 获取 JWT-SVID 并确认声明与你的联合规则期望的匹配。如果你使用不同的 SPIFFE 实现，请使用其 CLI 或 Workload API 客户端获取 JWT-SVID，并以相同方式解码载荷。

<Note>
Workload API 会验证调用进程的身份。对于 Kubernetes 注册条目，在满足条目选择器且挂载了代理套接字的 Pod 内运行此命令（例如使用 `kubectl exec`）。在 VM 和裸金属上，以匹配条目 `unix:` 选择器的用户或进程运行它。从未验证的主机 Shell 运行会返回 `no identity issued`，这是验证步骤最常见的失败原因。
</Note>

```bash CLI nocheck
spire-agent api fetch jwt \
    -audience https://api.anthropic.com \
    -socketPath /run/spire/sockets/agent.sock \
    -output json \
  | jq -r '.[0].svids[0].svid' \
  | jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'
```

`-output json` 标志将 SVID 响应和捆绑包响应作为两元素 JSON 数组返回，因此 `jq -r '.[0].svids[0].svid'` 提取裸令牌。在没有 `-output` 的旧版 SPIRE 上，命令打印带标签的块；在这种情况下，通过 `awk '/^[[:space:]]*eyJ/{print $1; exit}'` 管道默认输出以提取令牌行。检查 `iss` 是否为你注册的 OIDC Discovery Provider URL，`sub` 是否为工作负载的 SPIFFE ID，`aud` 是否包含 `https://api.anthropic.com`。然后运行[获取并使用令牌](#acquire-and-use-the-token)中的 cURL 示例；成功交换会返回一个以 `sk-ant-oat01-` 开头的 `access_token`。如果出现 `400 invalid_grant`，请参阅[排查交换失败](/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange)；最常见的 SPIRE 侧原因是 SPIRE Server 的 `jwt_issuer` 与注册为联合签发方的 URL 不匹配。

## 限定规则范围

SPIFFE ID 路径约定由运维人员定义，因此联合规则的 `subject_prefix` 匹配器应反映你的注册条目使用的路径方案。常见方案包括 `spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>`（spire-controller-manager 中 `ClusterSPIFFEID` 资源发出的默认值）和 `spiffe://<trust-domain>/host/<hostname>/<service>`（用于 VM 和裸金属工作负载）。

<Warning>
`subject_prefix` 为 `spiffe://prod.example.com/*` 会匹配信任域中的每个工作负载。如果没有 `audience` 匹配器，规则还会接受为任何受众铸造的 JWT-SVID，包括工作负载为无关依赖方请求的那些。
</Warning>

将规则的 `match` 块锁定到适合你用例的最窄范围：

- **固定到一个工作负载：** 将 `subject_prefix` 设置为完整的 SPIFFE ID，不带末尾的 `*`。
- **始终设置受众：** 在规则上要求 `audience`，并配置 spiffe-helper（或 Workload API 调用）使用相同的值，这样为其他依赖方铸造的 SVID 会被拒绝。
- **按路径段限定范围：** 使用 `spiffe://prod.example.com/ns/inference/*` 授予命名空间下注册的每个工作负载访问权限，并为每个命名空间创建单独的规则和 Anthropic 服务账号，而不是放宽一个规则。
- **每个信任域一个签发方：** 每个 SPIRE 信任域都有自己的签名密钥和 OIDC Discovery Provider。将每个注册为单独的联合签发方，并将规则绑定到拥有它们匹配的 SPIFFE ID 的签发方。

## 后续步骤

- [Workload Identity Federation](/docs/en/manage-claude/workload-identity-federation)：概念、令牌交换流程和 SDK 配置选项。
- [WIF 参考文档](/docs/en/manage-claude/wif-reference)：环境变量、JWKS 源模式和规则匹配模式。
- [在 Kubernetes 中使用 WIF](/docs/en/manage-claude/wif-providers/kubernetes)：适用于使用原生投影服务账号令牌而非 SPIRE 的集群。
