本文介绍如何将 Microsoft Entra 应用程序配置为信任托管标识。 然后,可以为访问令牌交换托管标识令牌,该令牌可以访问 Microsoft Entra 保护的资源,而无需使用或管理应用机密。
先决条件
- 具有活动订阅的 Azure 帐户。 
              创建帐户。
- 此 Azure 帐户必须有权 更新应用程序凭据。 以下任何 Microsoft Entra 角色都包括所需的权限:
- 了解 Azure 资源托管标识中的概念。
- 分配给托管工作负载的 Azure 计算资源(例如,虚拟机或 Azure 应用服务)的用户分配的托管标识。
- Microsoft Entra ID 中的应用注册。 此应用注册必须属于与托管标识相同的租户
- 应用注册须有权访问 Microsoft Entra 保护的资源(例如 Azure、Microsoft Graph、Microsoft 365 等)。 可以通过 API 权限或委派权限授予此访问权限。
重要注意事项和限制
若要创建、更新或删除联合标识凭据,则执行操作的帐户必须具有应用程序管理员、应用程序开发人员、云应用程序管理员或应用程序所有者角色。  更新联合标识凭据需要 microsoft.directory/applications/credentials/update 权限。
最多可以向应用程序或用户分配的托管标识添加 20 个联合标识凭据。
配置联合标识凭据时,需要提供几条重要信息:
- 
              颁发者、 使用者 是设置信任关系所需的关键信息部分。 当 Azure 工作负荷请求 Microsoft 标识平台将托管标识令牌转换为 Entra 应用访问令牌时,会验证联合标识凭据中的颁发者和使用者值是否与托管标识令牌中提供的- issuer- subject声明相匹配。 如果该验证检查通过,Microsoft 标识平台会向外部软件工作负荷发出访问令牌。
 
- issuer 是 Microsoft Entra 租户的颁发机构 URL,格式为 - https://login.partner.microsoftonline.cn/{tenant}/v2.0。 Microsoft Entra 应用和托管标识都必须属于同一租户。 如果- issuer声明具有值中的前导或尾随空格,则会阻止令牌交换。
 
- 
              - subject:这是大小写敏感的托管标识 对象(主体)ID 的 GUID,分配给 Azure 工作负荷。 托管标识必须与应用注册位于同一租户中,即使目标资源位于不同的云中也是如此。 如果联合标识凭据配置中的- subject与托管标识的主体 ID 完全不匹配,Microsoft 标识平台将拒绝令牌交换。
 
- 
              audiences 指定托管标识令牌中 - aud声明中的值(必需)。 该值必须是以下值之一,具体取决于目标云。
 - 
- 
              Microsoft Entra ID 全球服务: api://AzureADTokenExchange
- 
              中国区 Microsoft Entra(由世纪互联运营):api://AzureADTokenExchangeChina
 - 
- 重要 - 支持访问 另一租户 中的资源。
不支持访问 另一个云 中的资源。 对其他云的令牌请求将失败。 
 - 
- 重要 - 如果在 issuer、subject 或 audience 设置中意外添加了不正确的信息,则已成功创建联合标识凭据,且未出现错误。 在令牌交换失败之前,此错误不会变得明显。 
 
- name 是联合标识凭据的唯一标识符。 (必需)此字段的字符限制为 3-120 个字符,并且必须对 URL 友好。 支持字母数字、短划线或下划线字符,并且第一个字符必须是字母数字字符。  它在创建后就不可变。 
- description 是用户提供的联合标识凭据的说明(可选)。 Microsoft Entra ID 不会验证或检查说明。 此字段的上限为 600 个字符。 
任何联合标识凭据属性值都不支持通配符。
在本部分中,你将在现有应用程序上配置联合标识凭据,以信任托管标识。 使用以下选项卡来选择如何在现有应用程序中配置联合标识凭据。
- 登录 Microsoft Entra 管理中心。 检查你是否已在注册应用程序的租户中。 
- 浏览到 Entra ID>应用注册,并在主窗口中选择应用程序。 
- 在“管理”下,选择“证书和机密”。 
- 选择“联合凭据”选项卡,选择“添加凭据”。 - 
              
                
 
- 在【联合身份验证方案】下拉列表中,选择为【托管身份】,并根据下表填写值: - 
- 
| 字段 | 说明 | 示例 |  - 
| 颁发者 | 用于颁发托管标识令牌的 Microsoft Entra ID 颁发机构的 OAuth 2.0/OIDC 颁发者 URL。 该值将自动使用当前 Entra 租户颁发者填充。 | https://login.partner.microsoftonline.cn/{tenantID}/v2.0 |  - 
| 选择“托管标识” | 单击此链接以选择将充当联合标识凭据的托管标识。 只能将用户分配的托管标识用作凭据。 | msi-webapp1 |  - 
| 说明(可选) | 用户提供的联合标识凭据的说明。 | 信任工作负荷 UAMI 作为我的应用的凭据 |  - 
| 受众 | 必须在外部令牌中显示的受众值。 | 必须设置为以下值之一: • Entra ID 全局服务:api://AzureADTokenExchange
 • 由世纪互联运营的中国区 Entra ID:api://AzureADTokenExchangeChina
 
 |  
 - 
              
                
 
在首选的 IDE 中打开终端,并运行以下命令,在应用上创建联合标识凭据。
az ad app federated-credential create --id 00001111-aaaa-2222-bbbb-3333cccc4444 --parameters credential.json
该 id 参数指定应用程序 ID(对象 ID)。 该 parameters 参数以 JSON 格式指定联合标识凭据配置。
这是 credential.json内容的一个示例。 将 subject GUID 替换为托管标识的对象(主体)ID,并将 {tenantID} 替换为应用程序的租户 ID。
受众值必须设置为以下值之一:
 • Entra ID 全局服务:api://AzureADTokenExchange 
• 由世纪互联运营的中国区 Entra ID:api://AzureADTokenExchangeChina 
{
    "name": "msi-webapp1",
    "issuer": "https://login.partner.microsoftonline.cn/{tenantID}/v2.0",
    "subject": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "description": "Trust the workload's UAMI to impersonate the App",
    "audiences": [
        "api://AzureADTokenExchange"
    ]
}
在首选的 IDE 中打开 PowerShell 终端,并运行以下命令,在应用上创建联合标识凭据。 将 Subject GUID 替换为托管标识的对象(主体)ID,并将 {tenantID} 替换为应用程序的租户 ID。
受众值必须设置为以下值之一:
 • Entra ID 全局服务:api://AzureADTokenExchange 
• 由世纪互联运营的中国区 Entra ID:api://AzureADTokenExchangeChina 
New-AzADAppFederatedCredential -ApplicationObjectId $appObjectId -Audience api://AzureADTokenExchange -Issuer 'https://login.partner.microsoftonline.cn/{tenantID}/v2.0' -Name 'MyMsiFic' -Subject 'aaaabbbb-0000-cccc-1111-dddd2222eeee'
在首选的 IDE 中打开终端,并运行以下命令,在应用上创建联合标识凭据。 将 subject 值设置为托管标识的对象(主体)标识符,并将 {tenantID} 设置为应用程序的租户 ID。
受众值必须设置为以下值之一:
 • Entra ID 全局服务:api://AzureADTokenExchange 
• 由世纪互联运营的中国区 Entra ID:api://AzureADTokenExchangeChina 
az rest --method POST --uri 'https://microsoftgraph.chinacloudapi.cn/applications/{app_registration_id}/federatedIdentityCredentials' --body '{"name":"MyMsiFicTest","issuer":"https://login.partner.microsoftonline.cn/{tenantID}/v2.0","subject":"{Managed_Identity_Principal_ID}","description":"Trust the workloads UAMI to impersonate the App","audiences":["api://AzureADTokenExchange"]}'
此示例演示如何使用 Bicep 创建 FIC,以使应用信任分配的托管标识。 将占位符替换为相应的值。
extension 'br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.1.8-preview'
param myWorkloadManagedIdentity string = '[MANAGED-IDENTITY-NAME]'
param applicationDisplayName string = '[APPLICATION-DISPLAYNAME]'
param applicationName string = '[APPLICATION-UNIQUE-NAME]'
resource myManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
  name: myWorkloadManagedIdentity
}
resource myApp 'Microsoft.Graph/applications@v1.0' = {
  displayName: applicationDisplayName
  uniqueName: applicationName
  resource myMsiFic 'federatedIdentityCredentials@v1.0' = {
    name: '${myApp.uniqueName}/msiAsFic'
    description: 'Trust the workloads UAMI to impersonate the App'
    audiences: [
       'api://AzureADTokenExchange'
    ]
    issuer: '${environment().authentication.loginEndpoint}${tenant().tenantId}/v2.0'
    subject: myManagedIdentity.properties.principalId
  }
}
 
更新应用程序代码,以请求访问令牌
以下代码片段演示如何获取托管标识令牌并将其用作 Entra 应用程序的凭据。 在目标资源位于与 Entra 应用程序相同租户或不同租户的两种情况下,示例均有效。
Azure 标识客户端库
以下代码示例演示如何访问 Azure Key Vault 机密,但可以改编为访问受 Microsoft Entra 保护的任何资源。
using Azure.Core;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
// Audience value must be one of the below values depending on the target cloud:
// - Entra ID Global cloud: api://AzureADTokenExchange
// - Entra ID US Government: api://AzureADTokenExchangeUSGov
// - Entra ID China operated by 21Vianet: api://AzureADTokenExchangeChina
string miAudience = "api://AzureADTokenExchange";
// Create an assertion with the managed identity access token, so that it can be
// exchanged for an app token. Client ID is passed here. Alternatively, either
// object ID or resource ID can be passed.
ManagedIdentityCredential miCredential = new(
    ManagedIdentityId.FromUserAssignedClientId("<YOUR_MI_CLIENT_ID>"));
TokenRequestContext tokenRequestContext = new([$"{miAudience}/.default"]);
ClientAssertionCredential clientAssertionCredential = new(
    "<YOUR_RESOURCE_TENANT_ID>",
    "<YOUR_APP_CLIENT_ID>",
    async _ =>
        (await miCredential
            .GetTokenAsync(tokenRequestContext)
            .ConfigureAwait(false)).Token
);
// Create a new SecretClient using the assertion
SecretClient client = new(
    new Uri("https://testfickv.vault.azure.cn/"), 
    clientAssertionCredential);
// Retrieve the secret
KeyVaultSecret secret = client.GetSecret("<SECRET_NAME>");
package main
import (
  "context"
  "log"
  "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
  "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
  "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
)
func main() {
  // Audience value must be one of the below values depending on the target cloud:
  // - Entra ID Global cloud: api://AzureADTokenExchange
  // - Entra ID US Government: api://AzureADTokenExchangeUSGov
  // - Entra ID China operated by 21Vianet: api://AzureADTokenExchangeChina
  azScopes := []string{"api://AzureADTokenExchange/.default"}
  // Client ID is passed here. Alternatively, either object ID or resource ID can be passed.
  mic, err := azidentity.NewManagedIdentityCredential(
    &azidentity.ManagedIdentityCredentialOptions{
      ID: azidentity.ClientID("<YOUR_MI_CLIENT_ID>"),
    },
  )
  if err != nil {
    log.Fatal("error constructing managed identity credential: ", err)
  }
  getAssertion := func(ctx context.Context) (string, error) {
    tk, err := mic.GetToken(ctx, policy.TokenRequestOptions{Scopes: azScopes})
    return tk.Token, err
  }
  cred, err := azidentity.NewClientAssertionCredential("<YOUR_TENANT_ID>", "<YOUR_APP_CLIENT_ID>", getAssertion, nil)
  if err != nil {
    log.Fatal("error constructing client assertion credential: ", err)
  }
  client := azsecrets.NewClient("https://testfickv.vault.azure.cn", cred, nil)
	resp, err := client.GetSecret(context.TODO(), "<SECRET_NAME>", "", nil)
	if err != nil {
		// TODO: handle error
	}
}
import com.azure.core.credential.TokenRequestContext;
import com.azure.core.credential.*;
import com.azure.identity.*;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
import reactor.core.publisher.Mono;
public class KeyVaultFIC {
  // Audience value must be one of the below values depending on the target cloud:
  // - Entra ID Global cloud: api://AzureADTokenExchange
  // - Entra ID US Government: api://AzureADTokenExchangeUSGov
  // - Entra ID China operated by 21Vianet: api://AzureADTokenExchangeChina
  private static final String MI_AUDIENCE = "api://AzureADTokenExchange";
  public static void main(String[] args) throws Exception {
    ClientAssertionCredential clientAssertionCredential = new ClientAssertionCredentialBuilder()
        .tenantId("<YOUR_TENANT_ID>")
        .clientId("<YOUR_APP_CLIENT_ID>")
        .clientAssertion(() -> getTokenUsingManagedIdentity(MI_AUDIENCE).block())
        .build();
    SecretClient secretClient = new SecretClientBuilder()
        .vaultUrl("https://testfickv.vault.azure.cn")
        .credential(clientAssertionCredential)
        .buildClient();
    KeyVaultSecret secret = secretClient.getSecret("<SECRET_NAME>");
  }
  private static Mono<String> getTokenUsingManagedIdentity(String audience) {
    // Client ID is passed here. Alternatively, either object ID or resource ID can be passed.
    ManagedIdentityCredential managedIdentityCredential = new ManagedIdentityCredentialBuilder()
        .clientId("<YOUR_MI_CLIENT_ID>")
        .build();
    TokenRequestContext requestContext = new TokenRequestContext()
        .addScopes(audience + "/.default");
    return managedIdentityCredential
        .getToken(requestContext)
        .map(accessToken -> accessToken.getToken());
  }
}
import { ManagedIdentityCredential, ClientAssertionCredential, TokenCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
// Audience value must be one of the below values depending on the target cloud:
// - Entra ID Global cloud: api://AzureADTokenExchange
// - Entra ID US Government: api://AzureADTokenExchangeUSGov
// - Entra ID China operated by 21Vianet: api://AzureADTokenExchangeChina
const MI_AUDIENCE: string = "api://AzureADTokenExchange";
async function getAccessToken(credential: TokenCredential, audience: string[]): Promise<string> {
    const accessToken = await credential.getToken(audience);
    const token = accessToken?.token;
    if (!token)
        throw new Error(`Failed to obtain valid access token, received ${token}`);
    return token;
}
const main = async () => {
    // Client ID is passed here. Alternatively, either object ID or resource ID can be passed.
    const managedIdentityCredential = new ManagedIdentityCredential(
    {
        clientId: "<YOUR_MI_CLIENT_ID>"
    });
    const clientAssertionCredential = new ClientAssertionCredential(
        "<YOUR_TENANT_ID>",
        "<YOUR_APP_CLIENT_ID>",
        () => getAccessToken(managedIdentityCredential, [`${MI_AUDIENCE}/.default`]));
    const client = new SecretClient("https://testfickv.vault.azure.cn", clientAssertionCredential);
    try {
        const secret = await client.getSecret("<SECRET_NAME>");
        console.log("Found the secret from Key Vault");
    } catch (error) {
        console.error("Failed to retrieve secret:", error);
        throw error;
    }
};
main();
from azure.identity import ManagedIdentityCredential, ClientAssertionCredential
from azure.keyvault.secrets import SecretClient
# Audience value must be one of the below values depending on the target cloud:
# - Entra ID Global cloud: api://AzureADTokenExchange
# - Entra ID US Government: api://AzureADTokenExchangeUSGov
# - Entra ID China operated by 21Vianet: api://AzureADTokenExchangeChina
MI_AUDIENCE = "api://AzureADTokenExchange"
def get_managed_identity_token(credential, audience):
    return credential.get_token(audience).token
# Client ID is passed here. Alternatively, either object ID or resource ID can be passed.
managed_identity_credential = ManagedIdentityCredential(client_id="<YOUR_MI_CLIENT_ID>")
client_assertion_credential = ClientAssertionCredential(
    "<YOUR_RESOURCE_TENANT_ID>",
    "<YOUR_APP_CLIENT_ID>",
    lambda: get_managed_identity_token(managed_identity_credential, f"{MI_AUDIENCE}/.default"))
client = SecretClient(
    vault_url="https://testfickv.vault.azure.cn",
    credential=client_assertion_credential)
retrieved_secret = client.get_secret("<SECRET_NAME>")
 
Microsoft.Identity.Web
在 Microsoft.Identity.Web 中,可以在 ClientCredentials 中设置  部分,以便使用 SignedAssertionFromManagedIdentity 来启用代码使用配置的托管标识作为凭据:
{
  "AzureAd": {
    "Instance": "https://login.partner.microsoftonline.cn/",
    "ClientId": "YOUR_APPLICATION_ID",
    "TenantId": "YOUR_TENANT_ID",
    
    "ClientCredentials": [
      {
        "SourceType": "SignedAssertionFromManagedIdentity",
        "ManagedIdentityClientId": "YOUR_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID",
        "TokenExchangeUrl": "api://AzureADTokenExchange/.default"
      }
    ]
  }
}
MSAL (.NET)
在 MSAL 中,可以使用 ManagedClientApplication 类获取托管标识令牌。 然后,在构造机密客户端应用程序时,可将此令牌用作客户端断言。
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Azure.Storage.Blobs;
using Azure.Core;
using Azure.Storage.Blobs.Models;
internal class Program
{
  static async Task Main(string[] args)
  {
      string storageAccountName = "YOUR_STORAGE_ACCOUNT_NAME";
      string containerName = "CONTAINER_NAME";
      string appClientId = "YOUR_APP_CLIENT_ID";
      string resourceTenantId = "YOUR_RESOURCE_TENANT_ID";
      Uri authorityUri = new($"https://login.partner.microsoftonline.cn/{resourceTenantId}");
      string miClientId = "YOUR_MI_CLIENT_ID";
      string audience = "api://AzureADTokenExchange/.default";
      // Get mi token to use as assertion
      var miAssertionProvider = async (AssertionRequestOptions _) =>
      {
            var miApplication = ManagedIdentityApplicationBuilder
                .Create(ManagedIdentityId.WithUserAssignedClientId(miClientId))
                .Build();
            var miResult = await miApplication.AcquireTokenForManagedIdentity(audience)
                .ExecuteAsync()
                .ConfigureAwait(false);
            return miResult.AccessToken;
      };
      // Create a confidential client application with the assertion.
      IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(appClientId)
        .WithAuthority(authorityUri, false)
        .WithClientAssertion(miAssertionProvider)
        .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
        .Build();
        // Get the federated app token for the storage account
        string[] scopes = [$"https://{storageAccountName}.blob.core.chinacloudapi.cn/.default"];
        AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync().ConfigureAwait(false);
        TokenCredential tokenCredential = new AccessTokenCredential(result.AccessToken);
        var containerClient = new BlobContainerClient(
            new Uri($"https://{storageAccountName}.blob.core.chinacloudapi.cn/{containerName}"),
            tokenCredential);
        await foreach (BlobItem blob in containerClient.GetBlobsAsync())
        {
            // TODO: perform operations with the blobs
            BlobClient blobClient = containerClient.GetBlobClient(blob.Name);
            Console.WriteLine($"Blob name: {blobClient.Name}, URI: {blobClient.Uri}");
        }
    }
}
另请参阅