OpenAI Microsoft Azure 身份联合配置
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 提供商,再加稳定声明映射。
设置工作负载身份提供商
- 唯一 Name 如
azure-managed-identity-prod,Description 描述生产托管身份。 - 先取样本,OIDC Issuer URL 填实际
iss;Audience 填 Application ID URI,例如api://<application-client-id>,匹配aud。 - 关闭上传 JWKS,用 Entra 元数据和 JWKS 验证。
- 按需转换,例如
managed_identity_client_id配assertion.appid生成openai.managed_identity_client_id;自动添加前缀,原始openai.*声明不自动用作派生值。
设置服务账户映射
- 唯一名称如
vm-openai-wif,描述实际工作负载。 - 每个必需声明一行。若有
appid,优先精确匹配客户端 ID,一般最稳定;没有时从实际令牌选azp、oid、sub或xms_mirid。限定租户时另加tid。以真实 IMDS 样本中稳定的身份与资源字段为准。 - 选择目标 Project 与 Service account,例如
azure-managed-identity-prod-openai-wif。 - 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 提供商,再设映射。
设置工作负载身份提供商
- Name 如
azure-aks-prod,Description 标明集群。 - Issuer 使用
az aks show --query "oidcIssuerProfile.issuerUrl"的结果;Audience 为投射卷受众,如https://api.openai.com/v1。 - 关闭上传 JWKS,使用 AKS OIDC 发现。
- 如需派生,名称
aks_subject配assertion.sub产生openai.aks_subject;未经转换的原始同名前缀声明不会参与该键匹配。
设置服务账户映射
- Name 唯一,如
default-openai-wif,Description 说明默认命名空间工作负载。 sub精确匹配system:serviceaccount:default:openai-wif,格式为system:serviceaccount:<namespace>:<service-account-name>。提供商限制集群 issuer,映射进一步限制具体账户。- 选择目标 Project 与 Service account,例如
azure-aks-prod-openai-wif。 - 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 服务账户。

TRAE-AI编程


更多建议: