任何能够访问实例元数据服务器的 Google Cloud 计算环境(Cloud Run、Cloud Functions、App Engine、Compute Engine (GCE) 以及启用了 Workload Identity 的 GKE)都可以为其附加的服务账号请求一个由 Google 签名的 “identity token”(身份令牌)。该令牌的颁发者为 https://accounts.google.com,Anthropic 可以通过标准的 OIDC 发现机制直接对其进行验证,无需额外的 Google Cloud 配置。
本指南介绍如何在 Anthropic 中注册 Google 颁发者、将 Google 服务账号绑定到 Anthropic 服务账号,以及让您的工作负载将其身份令牌交换为短期有效的 Claude API 访问令牌。
前提条件
- 熟悉 WIF 概念:服务账号、联合颁发者和联合规则。
- 一个 Google Cloud 项目,其中有运行在 Cloud Run、Cloud Functions、App Engine、Compute Engine 或 GKE 上的工作负载。
- 附加到该工作负载的用户管理的 Google 服务账号(而非 Compute Engine 默认服务账号)。
- 在 Claude Console 中为您的 Anthropic 组织创建服务账号、联合颁发者和联合规则的权限。
配置 Google Cloud
Google 会自动向任何附加了服务账号的工作负载颁发身份令牌。除了附加正确的服务账号之外,Google 端无需启用任何内容,但标准计算环境与 GKE 之间的步骤略有不同。
Cloud Run、Cloud Functions、App Engine、GCE
为您的服务或实例附加一个专用服务账号:
CLI
gcloud run deploy my-service \
--service-account inference-worker@my-project.iam.gserviceaccount.com
在工作负载内部,元数据服务器会按需返回一个已签名的身份令牌。请使用您打算在 Anthropic 端注册的 audience 来请求它,并包含 format=full,以便响应中携带 email 声明:
GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full
Metadata-Flavor: Google
或者,使用 gcloud CLI:
CLI
gcloud auth print-identity-token \
--audiences="https://api.anthropic.com" \
--include-email
SDK 的等效用法请参阅获取并使用令牌。
解码后的令牌负载如下所示:
{
"iss": "https://accounts.google.com",
"aud": "https://api.anthropic.com",
"sub": "104892...",
"azp": "104892...",
"email": "inference-worker@my-project.iam.gserviceaccount.com",
"email_verified": true,
"exp": 1775527120
}
sub 声明是 Google 服务账号的不透明数字唯一 ID。email 声明是人类可读的服务账号地址。请在您的联合规则中同时匹配 sub 和 email。
启用 Workload Identity 的 GKE
在您的集群上启用 GKE Workload Identity,并通过 iam.gke.io/gcp-service-account 注解将您的 Kubernetes 服务账号绑定到 Google 服务账号:
apiVersion: v1
kind: ServiceAccount
metadata:
name: inference-worker
namespace: prod
annotations:
iam.gke.io/gcp-service-account: inference-worker@my-project.iam.gserviceaccount.com
完成此绑定后,GKE 元数据服务器返回的 Google 签名令牌与 Cloud Run 和 GCE 的情况完全相同:相同的 https://accounts.google.com 颁发者、相同的 email 声明、相同的获取 URL。请完全按照下一节的说明配置 Anthropic。
来自 GKE 的 format=full 令牌还额外包含 google.compute_engine.project_id、google.compute_engine.zone 和 google.compute_engine.instance_name 声明,您可以在联合规则的 condition 匹配器中引用这些声明(使用诸如 claims.google.compute_engine.project_id == "my-project" 的 CEL 表达式),从而将访问范围限定到特定集群或节点池。
注意
如果您不想将 Kubernetes 服务账号绑定到 Google 服务账号,GKE Pod 也可以改用集群自身的 OIDC 颁发者(
https://container.googleapis.com/v1/projects/PROJECT/locations/REGION/clusters/CLUSTER)配合投射的serviceAccountToken卷。该路径使用的是每个集群各自的颁发者,而非accounts.google.com。有关该模式,请参阅在 Kubernetes 中使用 WIF。
配置 Anthropic
在 Claude Console 中,前往 Workload identity 页面(platform.claude.com/settings/workload-identity-federation),点击 Connect workload,然后选择 Google Cloud 磁贴。向导会引导您完成注册颁发者、创建服务账号和创建联合规则的步骤。
向导会为您创建这些资源。无论您是在向导中输入这些值,还是将它们发送到 Admin API,都请使用以下值:
联合颁发者: Google 公开发布其 OIDC 发现文档,因此请使用发现模式。这一个颁发者即可覆盖所有 Google Cloud 平台(Cloud Run、GCE、Cloud Functions、App Engine 以及启用 Workload Identity 的 GKE)。请使用规则而非颁发者来区分工作负载。
{
"name": "gcp",
"issuer_url": "https://accounts.google.com",
"jwks": { "type": "discovery" }
}
联合规则: 同时匹配 sub 和 email 声明。email 是可读的服务账号地址;sub 是服务账号的数字唯一 ID,Google 永远不会重复使用该 ID,因此固定该值可以在服务账号被删除、之后又以相同 email 创建新账号时保护该规则。使用 gcloud iam service-accounts describe inference-worker@my-project.iam.gserviceaccount.com --format='value(uniqueId)' 查找唯一 ID。
{
"name": "gcp-inference-worker",
"issuer_id": "fdis_...",
"match": {
"audience": "https://api.anthropic.com",
"claims": {
"sub": "104892101234567890123",
"email": "inference-worker@my-project.iam.gserviceaccount.com"
}
},
"target": {
"type": "service_account",
"service_account_id": "svac_..."
},
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}
获取并使用令牌
在您的 Google Cloud 工作负载内部,从元数据服务器获取身份令牌,在 POST /v1/oauth/token 处进行交换,然后使用返回的 bearer 令牌调用 Claude API。当您提供 identity_token_provider (typescript: identityTokenProvider; php: identityProvider; csharp: IdentityTokenProvider; go: option.WithFederationTokenProvider(); java: IdentityTokenConfig) 时,Claude SDK 会为您处理交换和刷新循环,如以下示例所示。除 Java 外(请参阅该示例中的注释),该选项接受一个从元数据服务器返回新身份令牌的令牌提供程序。
cURL
# 从元数据服务器获取由 Google 签名的身份令牌
JWT=$(curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full")
# 将其交换为 Anthropic 访问令牌
RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" \
-d @- <<JSON
{
"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
)
ACCESS_TOKEN=$(jq -r .access_token <<<"$RESPONSE")
# 调用 Claude API
curl -sS 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-opus-5-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello from Cloud Run"}]
}'
CLI
# 将 Google 签名的身份令牌写入 CLI 可读取的文件
export ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp)
curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full" \
> "$ANTHROPIC_IDENTITY_TOKEN_FILE"
# ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
# ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID 从环境变量中读取。
ant messages create \
--model claude-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "Hello from Cloud Run"}'
Python
import os
import anthropic
import google.auth.transport.requests
import google.oauth2.id_token
from anthropic import WorkloadIdentityCredentials
AUDIENCE = "https://api.anthropic.com"
def fetch_google_identity_token() -> str:
request = google.auth.transport.requests.Request()
return google.oauth2.id_token.fetch_id_token(request, AUDIENCE)
client = anthropic.Anthropic(
credentials=WorkloadIdentityCredentials(
identity_token_provider=fetch_google_identity_token,
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-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello from Cloud Run"}],
)
print(next(block.text for block in message.content if block.type == "text"))
TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
const METADATA_URL =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full";
async function fetchGoogleIdentityToken(): Promise<string> {
const response = await fetch(METADATA_URL, {
headers: { "Metadata-Flavor": "Google" }
});
return response.text();
}
const client = new Anthropic({
credentials: oidcFederationProvider({
identityTokenProvider: fetchGoogleIdentityToken,
federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
baseURL: "https://api.anthropic.com",
fetch
})
});
const message = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello from Cloud Run" }]
});
for (const block of message.content) {
if (block.type === "text") {
console.log(block.text);
}
}
C#
using Anthropic.Credentials;
// ...
var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
{
FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!,
OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"),
ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"),
WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"),
IdentityTokenProvider = new MetadataTokenProvider(),
});
using var client = new AnthropicClient(new ClientOptions { Credentials = credentials });
var message = await client.Messages.Create(new()
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [new UserMessageParam { Content = "Hello from Cloud Run" }],
});
foreach (var block in message.Content)
{
if (block.Value is TextBlock textBlock)
{
Console.WriteLine(textBlock.Text);
}
}
class MetadataTokenProvider : IIdentityTokenProvider
{
private const string METADATA_URL =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full";
private static readonly HttpClient httpClient = new()
{
DefaultRequestHeaders = { { "Metadata-Flavor", "Google" } },
};
public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
{
return await httpClient.GetStringAsync(METADATA_URL, ct);
}
}
Go
const audience = "https://api.anthropic.com"
googleIDToken := func(ctx context.Context) (string, error) {
creds, err := idtoken.NewCredentials(&idtoken.Options{Audience: audience})
if err != nil {
return "", err
}
tok, err := creds.Token(ctx)
if err != nil {
return "", err
}
return tok.Value, nil
}
client := anthropic.NewClient(
option.WithFederationTokenProvider(googleIDToken, 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(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Cloud Run")),
},
})
if err != nil {
panic(err)
}
for _, block := range message.Content {
if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
fmt.Println(textBlock.Text)
break
}
}
Java
import com.anthropic.config.AuthenticationConfig;
import com.anthropic.config.AuthenticationType;
import com.anthropic.config.IdentityTokenConfig;
import com.anthropic.config.InMemoryProfileConfigProvider;
import com.anthropic.config.ProfileConfig;
// ...
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
void main() throws Exception {
HttpClient http = HttpClient.newHttpClient();
HttpRequest metadataRequest = HttpRequest.newBuilder()
.uri(URI.create("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full"))
.header("Metadata-Flavor", "Google")
.build();
// Java SDK 接受令牌本身或文件路径,而非函数;使用
// source("value") 时,path(...) 保存该令牌。SDK 会在每次刷新
// Anthropic 访问令牌时发送同一令牌,一旦该令牌过期,或(若含 jti
// 声明)已被使用,刷新即会失败。对于长时间运行的
// 进程,请传入 source("file"),并在每次刷新前将新令牌
// 写入该文件。
String googleIdentityToken = http.send(metadataRequest, HttpResponse.BodyHandlers.ofString()).body();
AnthropicClient client = AnthropicOkHttpClient.builder()
.configurationProvider(InMemoryProfileConfigProvider.of(ProfileConfig.builder()
.organizationId(System.getenv("ANTHROPIC_ORGANIZATION_ID"))
.workspaceId(System.getenv("ANTHROPIC_WORKSPACE_ID"))
.authentication(AuthenticationConfig.builder()
.type(AuthenticationType.OIDC_FEDERATION)
.federationRuleId(System.getenv("ANTHROPIC_FEDERATION_RULE_ID"))
.serviceAccountId(System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
.identityToken(IdentityTokenConfig.builder()
.source("value")
.path(googleIdentityToken)
.build())
.build())
.build()))
.build();
var message = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024)
.addUserMessage("Hello from Cloud Run")
.build());
IO.println(message.content());
}
PHP
use Anthropic\Client;
use Anthropic\Lib\Credentials\CredentialResult;
use Anthropic\Lib\Credentials\IdentityTokenProvider;
use Anthropic\Lib\Credentials\TokenCache;
use Anthropic\Lib\Credentials\WorkloadIdentityCredentials;
const METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full';
class GoogleIdentityToken implements IdentityTokenProvider
{
public function getIdentityToken(): string
{
$context = stream_context_create([
'http' => ['header' => "Metadata-Flavor: Google\r\n"],
]);
return file_get_contents(METADATA_URL, false, $context);
}
}
$credentials = new CredentialResult(
provider: new TokenCache(new WorkloadIdentityCredentials(
identityProvider: new GoogleIdentityToken(),
federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'),
organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'),
serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'),
workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null,
)),
);
$client = new Client(credentials: $credentials);
$message = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello from Cloud Run']],
);
$textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
echo $textBlock->text, PHP_EOL;
Ruby
require "anthropic"
require "net/http"
METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full"
credentials = Anthropic::WorkloadIdentityCredentials.new(
identity_token_provider: -> { Net::HTTP.get(URI(METADATA_URL), {"Metadata-Flavor" => "Google"}) },
federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"),
organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"),
service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"),
workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"]
)
client = Anthropic::Client.new(credentials: credentials)
message = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{role: "user", content: "Hello from Cloud Run"}]
)
puts message.content.find { it.type == :text }.text
Google 身份令牌大约在 1 小时后过期。除 Java 外(请参阅该示例中的注释),SDK 会在过期前自动再次调用令牌提供程序并重新交换。对于运行时间超过访问令牌 expires_in 的 shell 脚本,请按定时器刷新并重复交换。
验证设置
在您的工作负载内部,解码身份令牌并确认声明与您的规则匹配:
cURL
curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full" \
| jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'
检查 iss 是否为 https://accounts.google.com、aud 是否为 https://api.anthropic.com,以及 email 是否与联合规则中的值匹配。然后运行上一节中的交换。成功的交换会返回以 sk-ant-oat01- 开头的 access_token 以及以秒为单位的 expires_in 值。如果交换失败并返回不透明的 401 authentication_error 响应(消息为 Authentication failed),请在 Claude Console 中查看身份验证历史记录页面以了解拒绝原因,并参阅排查交换失败问题。Google Cloud 端最常见的原因是缺少 email 声明(请使用 format=full 请求令牌,以便包含该声明)。
限定规则范围
警告
Google 的
sub声明是服务账号的不透明数字唯一 ID, 没有稳定的前缀。带有尾随*的subject_prefix会匹配 所有 Google Cloud 项目中的任意服务账号,其中任何一个 都可能获得联合的 Anthropic 令牌。
将规则的 match 块锁定到符合您用例的最小范围:
- 精确匹配
sub: 在claims.sub中设置完整的数字唯一 ID,切勿对 Google 令牌使用subject_prefix。 - 固定
email声明: 在sub之外添加claims.email,使稳定 ID 和可读地址都必须匹配。 - 固定受众: 将
audience设置为您从元数据服务器请求的确切值,以便拒绝为其他使用方签发的令牌。 - 在 GKE 上固定项目: 对于
format=full令牌,添加诸如claims.google.compute_engine.project_id == "my-project"的condition,将规则限制为某一个项目的节点。
后续步骤
- 阅读 Workload Identity Federation 页面,了解完整的资源模型和 SDK 凭据优先级。
- 为每个环境(生产、预发布)添加单独的联合规则,以便您可以撤销其中一个而不影响其他环境。