OpenAI Microsoft Azure 身份联合配置

2026-11-09 15:42 更新

Azure 可通过托管身份的 Microsoft Entra ID 访问令牌,或 AKS 投射服务账户令牌,换取短期 OpenAI 访问令牌。

Azure 托管身份

Azure 托管工作负载无需长期密钥即可请求 Entra 令牌。OpenAI 将其作为外部主体令牌验证,再签发自己的令牌。

设置 Azure 托管身份

创建或使用代表可信受众的 Entra 应用注册,配置 Application ID URI。该 URI 是 IMDS 请求的 resource,也成为令牌 aud。参见创建 Entra 应用与主体。Application ID URI、IMDS resource、令牌 aud 和 OpenAI Audience 四者须一致。

创建托管身份,再分配给运行应用的 Azure 资源,如 VM;运行时须能访问 IMDS。参见托管身份概览及具体资源文档。

获取托管身份令牌

从已分配身份的资源,以 Application ID URI 为 resource 请求 IMDS:

来源示例 1(bash):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
APPLICATION_ID_URI="api://<application-client-id>"

TOKEN=$(curl -sS -G -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token" \
  --data-urlencode "api-version=2018-02-01" \
  --data-urlencode "resource=${APPLICATION_ID_URI}" \
  | jq -r .access_token)
export TOKEN

资源有多个用户分配身份时,增加 client_id、object_id 或 msi_res_id 选择目标。参数见获取访问令牌。

检查令牌

导出 Entra 令牌为 TOKEN,本地检查:

来源示例 2(javascript):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
  throw new Error("Expected a compact JWT with three segments");
}
if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {
  throw new Error("JWT payload is not valid Base64URL");
}

const bytes = Buffer.from(parts[1], "base64url");
if (bytes.toString("base64url") !== parts[1]) {
  throw new Error("JWT payload is not valid Base64URL");
}
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const claims = JSON.parse(decoded);
if (claims === null || Array.isArray(claims) || typeof claims !== "object") {
  throw new Error("JWT payload is not a JSON object");
}
console.log(decoded);

来源示例 3(python):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
import base64
import json
import os
import re


def reject_non_json_constant(value):
    raise ValueError(f"JWT payload contains non-JSON constant: {value}")


parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
    raise ValueError("Expected a compact JWT with three segments")

payload = parts[1]
if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:
    raise ValueError("JWT payload is not valid Base64URL")
padded_payload = payload + "=" * (-len(payload) % 4)
decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)
if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:
    raise ValueError("JWT payload is not valid Base64URL")
decoded_text = decoded.decode("utf-8")
claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)
if not isinstance(claims, dict):
    raise ValueError("JWT payload is not a JSON object")
print(decoded_text)

来源示例 4(go):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
package main

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "unicode/utf8"
)

func decodeSegment(segment string) (json.RawMessage, error) {
    if !isBase64URLSegment(segment) {
        return nil, fmt.Errorf("JWT segment is not valid Base64URL")
    }
    decoded, err := base64.RawURLEncoding.DecodeString(segment)
    if err != nil {
        return nil, err
    }
    if base64.RawURLEncoding.EncodeToString(decoded) != segment {
        return nil, fmt.Errorf("JWT segment is not valid Base64URL")
    }
    if !utf8.Valid(decoded) {
        return nil, fmt.Errorf("JWT segment is not valid UTF-8")
    }

    var value json.RawMessage
    if err := json.Unmarshal(decoded, &value); err != nil {
        return nil, err
    }
    if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
        return nil, fmt.Errorf("JWT segment is not a JSON object")
    }
    return value, nil
}

func isBase64URLSegment(segment string) bool {
    if segment == "" || len(segment)%4 == 1 {
        return false
    }
    for _, character := range segment {
        if !('A' <= character && character <= 'Z') &&
            !('a' <= character && character <= 'z') &&
            !('0' <= character && character <= '9') &&
            character != '-' &&
            character != '_' {
            return false
        }
    }
    return true
}

func main() {
    parts := strings.Split(os.Getenv("TOKEN"), ".")
    if len(parts) != 3 {
        panic("Expected a compact JWT with three segments")
    }

    payload, err := decodeSegment(parts[1])
    if err != nil {
        panic(err)
    }
    formatted, err := json.MarshalIndent(payload, "", "  ")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(formatted))
}

来源示例 5(java):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public final class DecodeJwtPayloadExample {
  private static final ObjectMapper JSON =
      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);

  private DecodeJwtPayloadExample() {}

  static String decodeUtf8(byte[] bytes) throws IOException {
    try {
      return StandardCharsets.UTF_8
          .newDecoder()
          .onMalformedInput(CodingErrorAction.REPORT)
          .onUnmappableCharacter(CodingErrorAction.REPORT)
          .decode(ByteBuffer.wrap(bytes))
          .toString();
    } catch (CharacterCodingException exception) {
      throw new IOException("JWT segment is not valid UTF-8", exception);
    }
  }

  static String decodeSegment(String segment) throws IOException {
    if (!isBase64UrlSegment(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    byte[] bytes = Base64.getUrlDecoder().decode(segment);
    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    String decoded = decodeUtf8(bytes);
    JsonNode value = JSON.readTree(decoded);
    if (value == null || value.isMissingNode() || !value.isObject()) {
      throw new IOException("JWT segment is not a JSON object");
    }
    return decoded;
  }

  static boolean isBase64UrlSegment(String segment) {
    if (segment.isEmpty() || segment.length() % 4 == 1) {
      return false;
    }
    return segment
        .chars()
        .allMatch(
            character ->
                character >= 'A' && character <= 'Z'
                    || character >= 'a' && character <= 'z'
                    || character >= '0' && character <= '9'
                    || character == '-'
                    || character == '_');
  }

  static String[] requireCompactJwt(String token) {
    if (token == null) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    String[] parts = token.split("\\.", -1);
    if (parts.length != 3) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    return parts;
  }

  public static void main(String[] args) throws IOException {
    String[] parts = requireCompactJwt(System.getenv("TOKEN"));
    System.out.println(decodeSegment(parts[1]));
  }
}

来源示例 6(csharp):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
using System.Text;
using System.Text.Json;

static string DecodeSegment(string segment)
{
    if (
        segment.Length % 4 == 1 ||
        segment.Any(
            character =>
                !(
                    character is >= 'A' and <= 'Z' ||
                    character is >= 'a' and <= 'z' ||
                    character is >= '0' and <= '9' ||
                    character is '-' or '_'
                )
        )
    )
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }

    byte[] decoded = Convert.FromBase64String(
        segment.Replace('-', '+').Replace('_', '/') +
        new string('=', (4 - segment.Length % 4) % 4)
    );
    string canonicalSegment = Convert
        .ToBase64String(decoded)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
    if (canonicalSegment != segment)
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }
    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
    using JsonDocument document = JsonDocument.Parse(decodedJson);
    if (document.RootElement.ValueKind is not JsonValueKind.Object)
    {
        throw new FormatException("JWT segment is not a JSON object");
    }
    return decodedJson;
}

string? token = Environment.GetEnvironmentVariable("TOKEN");
if (token is null)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}
string[] parts = token.Split('.');
if (parts.Length != 3)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}

Console.WriteLine(DecodeSegment(parts[1]));

来源示例 7(ruby):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
require "base64"
require "json"

parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3

unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1
  raise "JWT payload is not valid Base64URL"
end

begin
  payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))
rescue ArgumentError
  raise "JWT payload is not valid Base64URL"
end
unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]
  raise "JWT payload is not valid Base64URL"
end

payload.force_encoding(Encoding::UTF_8)
raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?

claims = JSON.parse(payload)
raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)

puts(payload)

仅解码,不验证签名,不向第三方工具提交生产令牌。载荷示例:

来源示例 8(json):

中文说明:来源的JSON数据或配置/输出片段,保留字段和值;令牌声明中的iss/aud/sub等须对照真实工作负载,解码本身不等于验签。

{
  "iss": "https://login.microsoftonline.com/11111111-2222-3333-4444-555555555555/v2.0",
  "aud": "api://00000000-1111-2222-3333-444444444444",
  "tid": "11111111-2222-3333-4444-555555555555",
  "appid": "22222222-3333-4444-5555-666666666666",
  "oid": "33333333-4444-5555-6666-777777777777",
  "sub": "33333333-4444-5555-6666-777777777777",
  "xms_mirid": "/subscriptions/<subscription-id>/resourcegroups/my-resource-group/providers/Microsoft.Compute/virtualMachines/openai-wif-vm",
  "iat": 1716235422,
  "exp": 1716239022
}

iss 从实际令牌取,可能为 https://login.microsoftonline.com/<tenant-id>/v2.0,但不能假定后缀;aud 须匹配上述四处;tid 为租户,存在时 appid 为托管身份应用/客户端 ID;用 exp - iat 检查完整寿命。

Codex 的 max_assertion_lifetime_seconds 应按批准范围覆盖签发者预期令牌完整寿命,不是剩余有效期,也不能假设每个 Entra 令牌只有一小时。Microsoft 的寿命说明指出寿命可变,且不支持配置托管身份令牌寿命。

令牌还可能有 azp、oid、sub、xms_mirid,以实际载荷为准,选能稳定识别具体身份与资源边界的声明。交换前重点对照 issuer、audience、tenant 和身份字段。

设置身份联合

先建 Entra issuer 提供商,再加稳定声明映射。

设置工作负载身份提供商

  1. 唯一 Name 如 azure-managed-identity-prod,Description 描述生产托管身份。
  2. 先取样本,OIDC Issuer URL 填实际 iss;Audience 填 Application ID URI,例如 api://<application-client-id>,匹配 aud。
  3. 关闭上传 JWKS,用 Entra 元数据和 JWKS 验证。
  4. 按需转换,例如 managed_identity_client_id 配 assertion.appid 生成 openai.managed_identity_client_id;自动添加前缀,原始 openai.* 声明不自动用作派生值。

设置服务账户映射

  1. 唯一名称如 vm-openai-wif,描述实际工作负载。
  2. 每个必需声明一行。若有 appid,优先精确匹配客户端 ID,一般最稳定;没有时从实际令牌选 azp、oid、sub 或 xms_mirid。限定租户时另加 tid。以真实 IMDS 样本中稳定的身份与资源字段为准。
  3. 选择目标 Project 与 Service account,例如 azure-managed-identity-prod-openai-wif。
  4. Permissions 按需用 api.model.request、api.vector_store.read 收窄;留空不增加 WIF scope 限制,不扩大服务账户权限。

在代码中使用令牌

OPENAI_WIF_AUDIENCE 为已配置的 Entra Application ID URI。主体令牌提供函数向 IMDS 请求,SDK 交换并认证 OpenAI 请求:

来源示例 9(javascript):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import OpenAI from "openai";

const imdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token";

const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;
const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;
const audience = process.env.OPENAI_WIF_AUDIENCE;

if (!identityProviderId || !serviceAccountId || !audience) {
  throw new Error(
    "Set OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID, and OPENAI_WIF_AUDIENCE"
  );
}

function azureManagedIdentityTokenProvider(resource) {
  return {
    tokenType: "jwt",
    getToken: async () => {
      const url = new URL(imdsEndpoint);
      url.searchParams.set("api-version", "2018-02-01");
      url.searchParams.set("resource", resource);

      const clientId = process.env.AZURE_CLIENT_ID;
      if (clientId) {
        url.searchParams.set("client_id", clientId);
      }

      const response = await fetch(url, {
        headers: { Metadata: "true" },
      });

      if (!response.ok) {
        throw new Error(
          `Azure IMDS token request failed with status ${response.status}.`
        );
      }

      const body = await response.json();
      if (!body.access_token) {
        throw new Error("Azure IMDS did not return an access token.");
      }

      return body.access_token;
    },
  };
}

const client = new OpenAI({
  workloadIdentity: {
    identityProviderId,
    serviceAccountId,
    provider: azureManagedIdentityTokenProvider(audience),
  },
});

const response = await client.responses.create({
  model: "gpt-5.6-terra",
  input: "Say hello from Azure managed identity workload identity federation.",
});

console.log(response.output_text);

来源示例 10(python):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import json
import os
from urllib.parse import urlencode
from urllib.request import Request, urlopen

from openai import OpenAI
from openai.auth import SubjectTokenProvider

IMDS_ENDPOINT = "http://169.254.169.254/metadata/identity/oauth2/token"


def azure_managed_identity_token_provider(resource: str) -> SubjectTokenProvider:
    def get_token() -> str:
        params = {
            "api-version": "2018-02-01",
            "resource": resource,
        }

        client_id = os.environ.get("AZURE_CLIENT_ID")
        if client_id:
            params["client_id"] = client_id

        request = Request(
            f"{IMDS_ENDPOINT}?{urlencode(params)}",
            headers={"Metadata": "true"},
        )

        with urlopen(request, timeout=10) as response:
            body = json.loads(response.read().decode("utf-8"))

        token = body.get("access_token", "")
        if not token:
            raise RuntimeError("Azure IMDS did not return an access token.")
        return token

    return {"token_type": "jwt", "get_token": get_token}


client = OpenAI(
    workload_identity={
        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
        "provider": azure_managed_identity_token_provider(
            os.environ["OPENAI_WIF_AUDIENCE"]
        ),
    },
)

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Say hello from Azure managed identity workload identity federation.",
)

print(response.output_text)

来源示例 11(go):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "os"

    "github.com/openai/openai-go/v3"
    "github.com/openai/openai-go/v3/auth"
    "github.com/openai/openai-go/v3/option"
    "github.com/openai/openai-go/v3/responses"
)

const azureIMDSEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"

type azureManagedIdentityTokenProvider struct {
    resource string
}

func (p azureManagedIdentityTokenProvider) TokenType() auth.SubjectTokenType {
    return auth.SubjectTokenTypeJWT
}

func (p azureManagedIdentityTokenProvider) GetToken(ctx context.Context, httpClient auth.HTTPDoer) (string, error) {
    values := url.Values{}
    values.Set("api-version", "2018-02-01")
    values.Set("resource", p.resource)
    if clientID := os.Getenv("AZURE_CLIENT_ID"); clientID != "" {
        values.Set("client_id", clientID)
    }

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, azureIMDSEndpoint+"?"+values.Encode(), nil)
    if err != nil {
        return "", &auth.SubjectTokenProviderError{
            Provider: "azure-managed-identity",
            Message:  "failed to build Azure IMDS token request",
            Cause:    err,
        }
    }
    req.Header.Set("Metadata", "true")

    resp, err := httpClient.Do(req)
    if err != nil {
        return "", &auth.SubjectTokenProviderError{
            Provider: "azure-managed-identity",
            Message:  "failed to request Azure managed identity token",
            Cause:    err,
        }
    }
    defer resp.Body.Close()

    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return "", &auth.SubjectTokenProviderError{
            Provider: "azure-managed-identity",
            Message:  fmt.Sprintf("Azure IMDS token request failed with status %d", resp.StatusCode),
        }
    }

    var body struct {
        AccessToken string `json:"access_token"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
        return "", &auth.SubjectTokenProviderError{
            Provider: "azure-managed-identity",
            Message:  "failed to decode Azure IMDS token response",
            Cause:    err,
        }
    }
    if body.AccessToken == "" {
        return "", &auth.SubjectTokenProviderError{
            Provider: "azure-managed-identity",
            Message:  "Azure IMDS did not return an access token",
        }
    }

    return body.AccessToken, nil
}

func main() {
    audience := os.Getenv("OPENAI_WIF_AUDIENCE")
    if audience == "" {
        log.Fatal("Set OPENAI_WIF_AUDIENCE")
    }

    client := openai.NewClient(
        option.WithWorkloadIdentity(auth.WorkloadIdentity{
            IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
            ServiceAccountID:   os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
            Provider: azureManagedIdentityTokenProvider{
                resource: audience,
            },
        }),
    )

    response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
        Model: openai.ChatModelGPT4_1Mini,
        Input: responses.ResponseNewParamsInputUnion{
            OfString: openai.String("Say hello from Azure managed identity workload identity federation."),
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(response.OutputText())
}

来源示例 12(java):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.auth.SubjectTokenProvider;
import com.openai.auth.SubjectTokenType;
import com.openai.auth.WorkloadIdentity;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.http.HttpClient;
import com.openai.errors.SubjectTokenProviderException;
import com.openai.models.responses.ResponseCreateParams;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;

public final class AzureManagedIdentityWorkloadIdentityExample {
  private static final String IMDS_ENDPOINT =
      "http://169.254.169.254/metadata/identity/oauth2/token";

  private AzureManagedIdentityWorkloadIdentityExample() {}

  static final class AzureManagedIdentityTokenProvider implements SubjectTokenProvider {
    private final String resource;

    AzureManagedIdentityTokenProvider(String resource) {
      this.resource = resource;
    }

    @Override
    public SubjectTokenType tokenType() {
      return SubjectTokenType.JWT;
    }

    @Override
    public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {
      try {
        String query =
            "api-version=2018-02-01&resource="
                + URLEncoder.encode(resource, StandardCharsets.UTF_8);
        String clientId = System.getenv("AZURE_CLIENT_ID");
        if (clientId != null && !clientId.isEmpty()) {
          query += "&client_id=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8);
        }

        HttpRequest request =
            HttpRequest.newBuilder()
                .uri(URI.create(IMDS_ENDPOINT + "?" + query))
                .header("Metadata", "true")
                .GET()
                .build();

        HttpResponse<String> response =
            java.net.http.HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
          throw new SubjectTokenProviderException(
              "azure-managed-identity",
              "Azure IMDS token request failed with status " + response.statusCode(),
              null);
        }

        JsonNode body = jsonMapper.readTree(response.body());
        String token = body.path("access_token").asText();
        if (token.isEmpty()) {
          throw new SubjectTokenProviderException(
              "azure-managed-identity", "Azure IMDS did not return an access token", null);
        }

        return token;
      } catch (SubjectTokenProviderException e) {
        throw e;
      } catch (Exception e) {
        throw new SubjectTokenProviderException(
            "azure-managed-identity", "failed to request Azure managed identity token", e);
      }
    }

    @Override
    public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {
      return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));
    }
  }

  public static void main(String[] args) {
    WorkloadIdentity workloadIdentity =
        WorkloadIdentity.builder()
            .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))
            .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))
            .provider(new AzureManagedIdentityTokenProvider(System.getenv("OPENAI_WIF_AUDIENCE")))
            .build();

    OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();

    ResponseCreateParams params =
        ResponseCreateParams.builder()
            .model("gpt-5.6-terra")
            .input("Say hello from Azure managed identity workload identity federation.")
            .build();

    client.responses().create(params).output().stream()
        .flatMap(item -> item.message().stream())
        .flatMap(message -> message.content().stream())
        .flatMap(content -> content.outputText().stream())
        .forEach(outputText -> System.out.println(outputText.text()));
  }
}

来源示例 13(ruby):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
require "json"
require "net/http"
require "openai"
require "uri"

class AzureManagedIdentityTokenProvider
  include OpenAI::Auth::SubjectTokenProvider

  IMDS_ENDPOINT = "http://169.254.169.254/metadata/identity/oauth2/token"

  def initialize(resource:)
    @resource = resource
  end

  def token_type
    OpenAI::Auth::TokenType::JWT
  end

  def get_token
    uri = URI(IMDS_ENDPOINT)
    params = {
      "api-version" => "2018-02-01",
      "resource" => @resource
    }
    params["client_id"] = ENV["AZURE_CLIENT_ID"] if ENV["AZURE_CLIENT_ID"]
    uri.query = URI.encode_www_form(params)

    request = Net::HTTP::Get.new(uri)
    request["Metadata"] = "true"

    response = Net::HTTP.start(uri.hostname, uri.port, read_timeout: 10) do |http|
      http.request(request)
    end

    unless response.is_a?(Net::HTTPSuccess)
      raise OpenAI::Errors::SubjectTokenProviderError.new(
        message: "Azure IMDS token request failed with status #{response.code}",
        provider: "azure-managed-identity"
      )
    end

    token = JSON.parse(response.body).fetch("access_token", "")
    if token.empty?
      raise OpenAI::Errors::SubjectTokenProviderError.new(
        message: "Azure IMDS did not return an access token",
        provider: "azure-managed-identity"
      )
    end
    token
  rescue JSON::ParserError, SystemCallError => e
    raise OpenAI::Errors::SubjectTokenProviderError.new(
      message: "Failed to request Azure managed identity token: #{e.message}",
      provider: "azure-managed-identity",
      cause: e
    )
  end
end

provider = AzureManagedIdentityTokenProvider.new(
  resource: ENV.fetch("OPENAI_WIF_AUDIENCE")
)

workload_identity = OpenAI::Auth::WorkloadIdentity.new(
  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
  provider: provider
)

client = OpenAI::Client.new(workload_identity: workload_identity)

response = client.responses.create(
  model: "gpt-5.6-terra",
  input: "Say hello from Azure managed identity workload identity federation."
)

puts(response.output_text)

Azure Kubernetes Service(AKS)

可直接交换 AKS 投射令牌。若采用 Azure Workload Identity 获取托管身份的 Entra 令牌,则 OpenAI 验证的是 Entra 令牌,应遵循上方托管身份配置,并按 Microsoft 文档配置 Azure WIF,而非混用下面集群令牌路径。

设置 AKS

获取集群 OIDC issuer:

来源示例 14(bash):

## [中文] 只读查询指定AKS集群的OIDC issuer URL,结果为空时再按下一块启用,不在本块交换令牌。
az aks show \
  --name <cluster-name> \
  --resource-group <resource-group> \
  --query "oidcIssuerProfile.issuerUrl" \
  --output tsv

结果为空时先启用:

来源示例 15(bash):

## [中文] 修改指定AKS集群配置以启用OIDC issuer;这是集群配置写操作,不是令牌请求。
az aks update \
    --resource-group <resource-group> \
    --name <cluster-name> \
    --enable-oidc-issuer

OpenAI issuer 与该 URL、投射令牌 iss 必须一致。创建或使用 ServiceAccount:

来源示例 16(bash):

## [中文] 在当前AKS集群default命名空间创建openai-wif服务账户;这是资源创建,不执行令牌交换。
kubectl create serviceaccount openai-wif --namespace default

配置受众和寿命,示例路径 /var/run/secrets/tokens/token,受众 https://api.openai.com/v1,3600秒;可自定义受众,但投射卷与提供商相同。OpenAI 验证 issuer、签名、受众、到期。

来源示例 17(yaml):

## [中文] 按本节配置受众、工作负载身份和令牌挂载;受众必须与OpenAI提供商一致,生产环境须限定账户及权限。
apiVersion: v1
kind: Pod
metadata:
  name: openai-wif-app
  namespace: default
spec:
  serviceAccountName: openai-wif
  containers:
    - name: app
      image: my-image
      volumeMounts:
        - name: aks-sa-token
          mountPath: /var/run/secrets/tokens
          readOnly: true
  volumes:
    - name: aks-sa-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              audience: "https://api.openai.com/v1"
              expirationSeconds: 3600

检查令牌

从运行 Pod 获取并导出:

来源示例 18(bash):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)
export TOKEN

本地解码:

来源示例 19(javascript):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
  throw new Error("Expected a compact JWT with three segments");
}
if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {
  throw new Error("JWT payload is not valid Base64URL");
}

const bytes = Buffer.from(parts[1], "base64url");
if (bytes.toString("base64url") !== parts[1]) {
  throw new Error("JWT payload is not valid Base64URL");
}
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const claims = JSON.parse(decoded);
if (claims === null || Array.isArray(claims) || typeof claims !== "object") {
  throw new Error("JWT payload is not a JSON object");
}
console.log(decoded);

来源示例 20(python):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
import base64
import json
import os
import re


def reject_non_json_constant(value):
    raise ValueError(f"JWT payload contains non-JSON constant: {value}")


parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
    raise ValueError("Expected a compact JWT with three segments")

payload = parts[1]
if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:
    raise ValueError("JWT payload is not valid Base64URL")
padded_payload = payload + "=" * (-len(payload) % 4)
decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)
if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:
    raise ValueError("JWT payload is not valid Base64URL")
decoded_text = decoded.decode("utf-8")
claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)
if not isinstance(claims, dict):
    raise ValueError("JWT payload is not a JSON object")
print(decoded_text)

来源示例 21(go):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
package main

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "unicode/utf8"
)

func decodeSegment(segment string) (json.RawMessage, error) {
    if !isBase64URLSegment(segment) {
        return nil, fmt.Errorf("JWT segment is not valid Base64URL")
    }
    decoded, err := base64.RawURLEncoding.DecodeString(segment)
    if err != nil {
        return nil, err
    }
    if base64.RawURLEncoding.EncodeToString(decoded) != segment {
        return nil, fmt.Errorf("JWT segment is not valid Base64URL")
    }
    if !utf8.Valid(decoded) {
        return nil, fmt.Errorf("JWT segment is not valid UTF-8")
    }

    var value json.RawMessage
    if err := json.Unmarshal(decoded, &value); err != nil {
        return nil, err
    }
    if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
        return nil, fmt.Errorf("JWT segment is not a JSON object")
    }
    return value, nil
}

func isBase64URLSegment(segment string) bool {
    if segment == "" || len(segment)%4 == 1 {
        return false
    }
    for _, character := range segment {
        if !('A' <= character && character <= 'Z') &&
            !('a' <= character && character <= 'z') &&
            !('0' <= character && character <= '9') &&
            character != '-' &&
            character != '_' {
            return false
        }
    }
    return true
}

func main() {
    parts := strings.Split(os.Getenv("TOKEN"), ".")
    if len(parts) != 3 {
        panic("Expected a compact JWT with three segments")
    }

    payload, err := decodeSegment(parts[1])
    if err != nil {
        panic(err)
    }
    formatted, err := json.MarshalIndent(payload, "", "  ")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(formatted))
}

来源示例 22(java):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public final class DecodeJwtPayloadExample {
  private static final ObjectMapper JSON =
      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);

  private DecodeJwtPayloadExample() {}

  static String decodeUtf8(byte[] bytes) throws IOException {
    try {
      return StandardCharsets.UTF_8
          .newDecoder()
          .onMalformedInput(CodingErrorAction.REPORT)
          .onUnmappableCharacter(CodingErrorAction.REPORT)
          .decode(ByteBuffer.wrap(bytes))
          .toString();
    } catch (CharacterCodingException exception) {
      throw new IOException("JWT segment is not valid UTF-8", exception);
    }
  }

  static String decodeSegment(String segment) throws IOException {
    if (!isBase64UrlSegment(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    byte[] bytes = Base64.getUrlDecoder().decode(segment);
    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
      throw new IllegalArgumentException("JWT segment is not valid Base64URL");
    }
    String decoded = decodeUtf8(bytes);
    JsonNode value = JSON.readTree(decoded);
    if (value == null || value.isMissingNode() || !value.isObject()) {
      throw new IOException("JWT segment is not a JSON object");
    }
    return decoded;
  }

  static boolean isBase64UrlSegment(String segment) {
    if (segment.isEmpty() || segment.length() % 4 == 1) {
      return false;
    }
    return segment
        .chars()
        .allMatch(
            character ->
                character >= 'A' && character <= 'Z'
                    || character >= 'a' && character <= 'z'
                    || character >= '0' && character <= '9'
                    || character == '-'
                    || character == '_');
  }

  static String[] requireCompactJwt(String token) {
    if (token == null) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    String[] parts = token.split("\\.", -1);
    if (parts.length != 3) {
      throw new IllegalArgumentException("Expected a compact JWT with three segments");
    }
    return parts;
  }

  public static void main(String[] args) throws IOException {
    String[] parts = requireCompactJwt(System.getenv("TOKEN"));
    System.out.println(decodeSegment(parts[1]));
  }
}

来源示例 23(csharp):

// [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
using System.Text;
using System.Text.Json;

static string DecodeSegment(string segment)
{
    if (
        segment.Length % 4 == 1 ||
        segment.Any(
            character =>
                !(
                    character is >= 'A' and <= 'Z' ||
                    character is >= 'a' and <= 'z' ||
                    character is >= '0' and <= '9' ||
                    character is '-' or '_'
                )
        )
    )
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }

    byte[] decoded = Convert.FromBase64String(
        segment.Replace('-', '+').Replace('_', '/') +
        new string('=', (4 - segment.Length % 4) % 4)
    );
    string canonicalSegment = Convert
        .ToBase64String(decoded)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');
    if (canonicalSegment != segment)
    {
        throw new FormatException("JWT segment is not valid Base64URL");
    }
    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
    using JsonDocument document = JsonDocument.Parse(decodedJson);
    if (document.RootElement.ValueKind is not JsonValueKind.Object)
    {
        throw new FormatException("JWT segment is not a JSON object");
    }
    return decodedJson;
}

string? token = Environment.GetEnvironmentVariable("TOKEN");
if (token is null)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}
string[] parts = token.Split('.');
if (parts.Length != 3)
{
    throw new InvalidOperationException(
        "Expected a compact JWT with three segments"
    );
}

Console.WriteLine(DecodeSegment(parts[1]));

来源示例 24(ruby):

## [中文] 在工作负载本地解码JWT查看头部/声明,仅解码并不验证签名;不要打印原始令牌或提交第三方解码网站。
require "base64"
require "json"

parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3

unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1
  raise "JWT payload is not valid Base64URL"
end

begin
  payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))
rescue ArgumentError
  raise "JWT payload is not valid Base64URL"
end
unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]
  raise "JWT payload is not valid Base64URL"
end

payload.force_encoding(Encoding::UTF_8)
raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?

claims = JSON.parse(payload)
raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)

puts(payload)

不验签,不向第三方粘贴生产令牌。载荷示例:

来源示例 25(json):

中文说明:来源的JSON数据或配置/输出片段,保留字段和值;令牌声明中的iss/aud/sub等须对照真实工作负载,解码本身不等于验签。

{
  "iss": "https://eastus.oic.prod-aks.azure.com/11111111-2222-3333-4444-555555555555/22222222-3333-4444-5555-666666666666/",
  "aud": ["https://api.openai.com/v1"],
  "sub": "system:serviceaccount:default:openai-wif",
  "iat": 1716235422,
  "exp": 1716239022,
  "kubernetes.io": {
    "namespace": "default",
    "serviceaccount": {
      "name": "openai-wif",
      "uid": "11111111-2222-3333-4444-555555555555"
    }
  }
}

iss 须匹配 AKS issuer,aud 匹配投射及 OpenAI audience,sub 匹配映射的 Kubernetes 服务账户主体。

设置身份联合

先建 AKS issuer 提供商,再设映射。

设置工作负载身份提供商

  1. Name 如 azure-aks-prod,Description 标明集群。
  2. Issuer 使用 az aks show --query "oidcIssuerProfile.issuerUrl" 的结果;Audience 为投射卷受众,如 https://api.openai.com/v1。
  3. 关闭上传 JWKS,使用 AKS OIDC 发现。
  4. 如需派生,名称 aks_subject 配 assertion.sub 产生 openai.aks_subject;未经转换的原始同名前缀声明不会参与该键匹配。

设置服务账户映射

  1. Name 唯一,如 default-openai-wif,Description 说明默认命名空间工作负载。
  2. sub 精确匹配 system:serviceaccount:default:openai-wif,格式为 system:serviceaccount:<namespace>:<service-account-name>。提供商限制集群 issuer,映射进一步限制具体账户。
  3. 选择目标 Project 与 Service account,例如 azure-aks-prod-openai-wif。
  4. Permissions 按需收窄,留空不额外限制 WIF,但始终受账户自身权限约束。

在代码中使用令牌

自定义主体令牌提供函数重读挂载文件,如 /var/run/secrets/tokens/token;SDK 交换后使用 OpenAI 访问令牌:

来源示例 26(javascript):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import { readFile } from "node:fs/promises";
import OpenAI from "openai";

const tokenPath = "/var/run/secrets/tokens/token";
const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;
const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;

if (!identityProviderId || !serviceAccountId) {
  throw new Error(
    "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"
  );
}

function mountedAksServiceAccountTokenProvider(path) {
  return {
    tokenType: "jwt",
    getToken: async () => {
      const token = (await readFile(path, "utf8")).trim();
      if (!token) {
        throw new Error("The mounted AKS service account token file is empty.");
      }
      return token;
    },
  };
}

const client = new OpenAI({
  workloadIdentity: {
    identityProviderId,
    serviceAccountId,
    provider: mountedAksServiceAccountTokenProvider(tokenPath),
  },
});

const response = await client.responses.create({
  model: "gpt-5.6-terra",
  input: "Say hello from AKS workload identity federation.",
});

console.log(response.output_text);

来源示例 27(python):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import os
from pathlib import Path

from openai import OpenAI
from openai.auth import SubjectTokenProvider

TOKEN_PATH = "/var/run/secrets/tokens/token"


def mounted_aks_service_account_token_provider(token_path: str) -> SubjectTokenProvider:
    def get_token() -> str:
        token = Path(token_path).read_text().strip()
        if not token:
            raise RuntimeError("The mounted AKS service account token file is empty.")
        return token

    return {"token_type": "jwt", "get_token": get_token}


client = OpenAI(
    workload_identity={
        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
        "provider": mounted_aks_service_account_token_provider(TOKEN_PATH),
    },
)

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Say hello from AKS workload identity federation.",
)

print(response.output_text)

来源示例 28(go):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "strings"

    "github.com/openai/openai-go/v3"
    "github.com/openai/openai-go/v3/auth"
    "github.com/openai/openai-go/v3/option"
    "github.com/openai/openai-go/v3/responses"
)

const tokenPath = "/var/run/secrets/tokens/token"

type mountedAksServiceAccountTokenProvider struct {
    path string
}

func (p mountedAksServiceAccountTokenProvider) TokenType() auth.SubjectTokenType {
    return auth.SubjectTokenTypeJWT
}

func (p mountedAksServiceAccountTokenProvider) GetToken(_ context.Context, _ auth.HTTPDoer) (string, error) {
    data, err := os.ReadFile(p.path)
    if err != nil {
        return "", &auth.SubjectTokenProviderError{
            Provider: "azure-aks",
            Message:  "failed to read mounted AKS service account token",
            Cause:    err,
        }
    }

    token := strings.TrimSpace(string(data))
    if token == "" {
        return "", &auth.SubjectTokenProviderError{
            Provider: "azure-aks",
            Message:  "mounted AKS service account token is empty",
        }
    }

    return token, nil
}

func main() {
    client := openai.NewClient(
        option.WithWorkloadIdentity(auth.WorkloadIdentity{
            IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
            ServiceAccountID:   os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
            Provider: mountedAksServiceAccountTokenProvider{
                path: tokenPath,
            },
        }),
    )

    response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
        Model: openai.ChatModelGPT4_1Mini,
        Input: responses.ResponseNewParamsInputUnion{
            OfString: openai.String("Say hello from AKS workload identity federation."),
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(response.OutputText())
}

来源示例 29(java):

// [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.auth.SubjectTokenProvider;
import com.openai.auth.SubjectTokenType;
import com.openai.auth.WorkloadIdentity;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.http.HttpClient;
import com.openai.errors.SubjectTokenProviderException;
import com.openai.models.responses.ResponseCreateParams;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;

public final class AzureAksWorkloadIdentityExample {
  private static final String TOKEN_PATH = "/var/run/secrets/tokens/token";

  private AzureAksWorkloadIdentityExample() {}

  static final class MountedAksServiceAccountTokenProvider implements SubjectTokenProvider {
    private final Path tokenPath;

    MountedAksServiceAccountTokenProvider(String tokenPath) {
      this.tokenPath = Path.of(tokenPath);
    }

    @Override
    public SubjectTokenType tokenType() {
      return SubjectTokenType.JWT;
    }

    @Override
    public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {
      String token;
      try {
        token = Files.readString(tokenPath).trim();
      } catch (Exception e) {
        throw new SubjectTokenProviderException(
            "azure-aks", "failed to read mounted AKS service account token", e);
      }

      if (token.isEmpty()) {
        throw new SubjectTokenProviderException(
            "azure-aks", "mounted AKS service account token is empty", null);
      }

      return token;
    }

    @Override
    public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {
      return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));
    }
  }

  public static void main(String[] args) {
    WorkloadIdentity workloadIdentity =
        WorkloadIdentity.builder()
            .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))
            .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))
            .provider(new MountedAksServiceAccountTokenProvider(TOKEN_PATH))
            .build();

    OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();

    ResponseCreateParams params =
        ResponseCreateParams.builder()
            .model("gpt-5.6-terra")
            .input("Say hello from AKS workload identity federation.")
            .build();

    client.responses().create(params).output().stream()
        .flatMap(item -> item.message().stream())
        .flatMap(message -> message.content().stream())
        .flatMap(content -> content.outputText().stream())
        .forEach(outputText -> System.out.println(outputText.text()));
  }
}

来源示例 30(ruby):

## [中文] 按本小节获取或重读当前外部身份令牌,交由SDK交换为短期OpenAI令牌;提供商与服务账户ID是配置,令牌本身不得记录或提交版本库。
require "openai"

TOKEN_PATH = "/var/run/secrets/tokens/token"

class MountedAksServiceAccountTokenProvider
  include OpenAI::Auth::SubjectTokenProvider

  def initialize(token_path:)
    @token_path = token_path
  end

  def token_type
    OpenAI::Auth::TokenType::JWT
  end

  def get_token
    token = File.read(@token_path).strip
    if token.empty?
      raise OpenAI::Errors::SubjectTokenProviderError.new(
        message: "Mounted AKS service account token is empty",
        provider: "azure-aks"
      )
    end
    token
  rescue SystemCallError => e
    raise OpenAI::Errors::SubjectTokenProviderError.new(
      message: "Failed to read mounted AKS service account token: #{e.message}",
      provider: "azure-aks",
      cause: e
    )
  end
end

provider = MountedAksServiceAccountTokenProvider.new(token_path: TOKEN_PATH)

workload_identity = OpenAI::Auth::WorkloadIdentity.new(
  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
  provider: provider
)

client = OpenAI::Client.new(workload_identity: workload_identity)

response = client.responses.create(
  model: "gpt-5.6-terra",
  input: "Say hello from AKS workload identity federation."
)

puts(response.output_text)

Microsoft Azure 最佳实践

尽量使用托管身份,避免手动分发凭证;不同应用/环境采用独立身份、Entra 应用和映射,不在开发、预发布与生产间共用;只接受所需 audience;用专用 Entra 应用明确所有权、审计和权限边界;优先工作负载特定声明而非租户广泛属性;定期清理闲置联合凭证;生产与非生产使用不同联合身份及 OpenAI 服务账户。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号