教程:从通用 Windows 平台 (UWP) 应用程序调用 Microsoft Graph API

在本教程中,你将生成一个本机通用 Windows 平台 (UWP) 应用,用户可登录该应用并获取访问令牌来调用 Microsoft Graph API。

应用程序会调用任何具有 Microsoft Entra ID 的公司或组织提供的工作和学校帐户。

本教程的内容:

  • 在 Visual Studio 中创建“通用 Windows 平台(UWP)”项目
  • 在 Azure 门户中注册应用程序
  • 添加代码以支持用户登录和注销
  • 添加代码以调用 Microsoft Graph API
  • 测试应用程序

先决条件

本指南的工作原理

显示本教程生成的示例应用的工作原理

本指南创建的示例 UWP 应用程序用于查询 Microsoft Graph API。 在此方案中,通过 Authorization 标头向 HTTP 请求添加令牌。 Microsoft 身份验证库处理令牌获取和续订。

MSAL.NET 版本 4.61.0 及更高版本不支持通用 Windows 平台 (UWP)、Xamarin Android 和 Xamarin iOS。 建议将 UWP 应用程序迁移到 WINUI 等新式框架。 阅读宣布即将弃用 MSAL.NET for Xamarin 和 UWP 中有关弃用的详细信息。

NuGet 包

本指南使用以下 NuGet 包:

说明
Microsoft.Identity.Client Microsoft 身份验证库
Microsoft.Graph Microsoft Graph 客户端库

设置项目

本部分逐步说明如何将 Windows 桌面 .NET 应用程序 (XAML) 与 Microsoft 登录集成。 然后,该应用程序可以查询需要令牌的 Web API,例如 Microsoft Graph API。

本指南创建的应用程序显示用来查询 Microsoft Graph API 的按钮和用来注销的按钮。它还显示包含调用结果的文本框。

提示

若要查看在本教程中生成的项目的完整版本,可以从 GitHub 下载。

创建应用程序

  1. 打开 Visual Studio,选择“新建项目”。

  2. 在“创建新项目”中,为 C# 选择“空白应用(通用 Windows)”,然后选择“下一步”。

  3. 在“配置新项目”中为应用命名,然后选择“创建”。

  4. 如果出现提示,请在“新建通用 Windows 平台项目”中选择任意版本作为“目标”版本和“最低”版本,然后选择“确定”。

    最低版本和目标版本

向项目添加 Microsoft 身份验证库

  1. 在 Visual Studio 中,选择“工具”>“NuGet 包管理器”>“包管理器控制台” 。

  2. 在“包管理器控制台”窗口中复制并粘贴以下命令:

    Install-Package Microsoft.Identity.Client
    Install-Package Microsoft.Graph
    

    注意

    首个命令将安装 Microsoft 身份验证库 (MSAL.NET)。 MSAL.NET 获取、缓存和刷新用于访问受 Microsoft 标识平台保护的 API 的用户令牌。 第二个命令安装 Microsoft Graph .NET 客户端库,用于验证对 Microsoft Graph 的请求并调用该服务。

创建应用程序的 UI

Visual Studio 创建 MainPage.xaml 作为项目模板的一部分。 打开此文件,然后将应用程序的“Grid”节点替换为以下代码:

<Grid>
    <StackPanel Background="Azure">
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
            <Button x:Name="CallGraphButton" Content="Call Microsoft Graph API" HorizontalAlignment="Right" Padding="5" Click="CallGraphButton_Click" Margin="5" FontFamily="Segoe Ui"/>
            <Button x:Name="SignOutButton" Content="Sign-Out" HorizontalAlignment="Right" Padding="5" Click="SignOutButton_Click" Margin="5" Visibility="Collapsed" FontFamily="Segoe Ui"/>
        </StackPanel>
        <TextBlock Text="API Call Results" Margin="2,0,0,-5" FontFamily="Segoe Ui" />
        <TextBox x:Name="ResultText" TextWrapping="Wrap" MinHeight="120" Margin="5" FontFamily="Segoe Ui"/>
        <TextBlock Text="Token Info" Margin="2,0,0,-5" FontFamily="Segoe Ui" />
        <TextBox x:Name="TokenInfoText" TextWrapping="Wrap" MinHeight="70" Margin="5" FontFamily="Segoe Ui"/>
    </StackPanel>
</Grid>

使用 Microsoft 身份验证库获取用于 Microsoft Graph API 的令牌

本部分介绍如何使用 Microsoft 身份验证库获取用于 Microsoft Graph API 的令牌。 更改 MainPage.xaml.cs 文件。

  1. MainPage.xaml.cs 中,添加以下引用:

    using Microsoft.Identity.Client;
    using Microsoft.Graph;
    using Microsoft.Graph.Models;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using System.Net.Http.Headers;
    
  2. 使用以下代码替换 MainPage 类:

    public sealed partial class MainPage : Page
    {
    
        //Set the scope for API call to user.read
        private string[] scopes = new string[] { "https://microsoftgraph.chinacloudapi.cn/user.read" };
    
        // Below are the clientId (Application Id) of your app registration and the tenant information.
        // You have to replace:
        // - the content of ClientID with the Application Id for your app registration
        private const string ClientId = "[Application Id pasted from the application registration portal]";
    
        private const string Tenant = "common"; // Alternatively "[Enter your tenant, as obtained from the Azure portal, e.g. kko365.partner.onmschina.cn]"
        private const string Authority = "https://login.partner.microsoftonline.cn/" + Tenant;
    
        // The MSAL Public client app
        private static IPublicClientApplication PublicClientApp;
    
        private static string MSGraphURL = "https://microsoftgraph.chinacloudapi.cn/v1.0/";
        private static AuthenticationResult authResult;
    
        public MainPage()
        {
            this.InitializeComponent();
        }
    
        /// <summary>
        /// Call AcquireTokenAsync - to acquire a token requiring user to sign in
        /// </summary>
        private async void CallGraphButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                // Sign in user using MSAL and obtain an access token for Microsoft Graph
                GraphServiceClient graphClient = await SignInAndInitializeGraphServiceClient(scopes);
    
                // Call the /me endpoint of Graph
                User graphUser = await graphClient.Me.GetAsync();
    
                // Go back to the UI thread to make changes to the UI
                await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    ResultText.Text = "Display Name: " + graphUser.DisplayName + "\nBusiness Phone: " + graphUser.BusinessPhones.FirstOrDefault()
                                      + "\nGiven Name: " + graphUser.GivenName + "\nid: " + graphUser.Id
                                      + "\nUser Principal Name: " + graphUser.UserPrincipalName;
                    DisplayBasicTokenInfo(authResult);
                    this.SignOutButton.Visibility = Visibility.Visible;
                });
            }
            catch (MsalException msalEx)
            {
                await DisplayMessageAsync($"Error Acquiring Token:{System.Environment.NewLine}{msalEx}");
            }
            catch (Exception ex)
            {
                await DisplayMessageAsync($"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}");
                return;
            }
        }
                /// <summary>
        /// Signs in the user and obtains an access token for Microsoft Graph
        /// </summary>
        /// <param name="scopes"></param>
        /// <returns> Access Token</returns>
        private static async Task<string> SignInUserAndGetTokenUsingMSAL(string[] scopes)
        {
            // Initialize the MSAL library by building a public client application
            PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
                .WithAuthority(Authority)
                .WithUseCorporateNetwork(false)
                .WithRedirectUri("https://login.partner.microsoftonline.cn/common/oauth2/nativeclient")
                 .WithLogging((level, message, containsPii) =>
                 {
                     Debug.WriteLine($"MSAL: {level} {message} ");
                 }, LogLevel.Warning, enablePiiLogging: false, enableDefaultPlatformLogging: true)
                .Build();
    
            // It's good practice to not do work on the UI thread, so use ConfigureAwait(false) whenever possible.
            IEnumerable<IAccount> accounts = await PublicClientApp.GetAccountsAsync().ConfigureAwait(false);
            IAccount firstAccount = accounts.FirstOrDefault();
    
            try
            {
                authResult = await PublicClientApp.AcquireTokenSilent(scopes, firstAccount)
                                                  .ExecuteAsync();
            }
            catch (MsalUiRequiredException ex)
            {
                // A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token
                Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
    
                authResult = await PublicClientApp.AcquireTokenInteractive(scopes)
                                                  .ExecuteAsync()
                                                  .ConfigureAwait(false);
    
            }
            return authResult.AccessToken;
        }
    }
    

以交互方式获取用户令牌

AcquireTokenInteractive 方法会生成提示用户登录的窗口。 应用程序通常要求用户首次登录访问受保护的资源时采用交互方式。 当用于获取令牌的无提示操作失败时,用户也可能需要登录。 例如,当用户的密码过期时。

以无提示方式获取用户令牌

AcquireTokenSilent 方法处理令牌获取和续订,无需进行任何用户交互。 在 AcquireTokenInteractive 首次运行并提示用户输入凭据后,请使用 AcquireTokenSilent 方法请求后续调用的令牌。 该方法以无提示方式获取令牌。 Microsoft 身份验证库处理令牌缓存和续订。

最终,AcquireTokenSilent 方法会失败。 失败的原因包括用户已注销,或者已在另一设备上更改了密码。 Microsoft 身份验证库在检测到此问题需要交互式操作时,会引发 MsalUiRequiredException 异常。 应用程序可以通过两种方式处理此异常:

  • 应用程序会立即调用 AcquireTokenInteractive。 此调用会导致系统提示用户进行登录。 通常将这个适合联机应用程序的方法用于没有脱机内容供用户使用的情况。 此引导式设置生成的示例遵循此模式。 首次运行示例时可以看到其正在运行。

    由于没有用户使用过该应用程序,因此 accounts.FirstOrDefault() 包含一个 null 值,并且引发 MsalUiRequiredException 异常。

    此示例中的代码随后通过调用 AcquireTokenInteractive 来处理此异常。 此调用会导致系统提示用户进行登录。

  • 应用程序会向用户提供视觉指示,要求用户登录。 然后,用户可以选择适当的时间进行登录。 应用程序可以稍后重试 AcquireTokenSilent。 当用户可以使用其他应用程序功能而不会出现中断时,请使用此方法。 例如,当脱机内容在应用程序中可用时。 在这种情况下,用户可以决定何时需要登录。 在出现网络暂时不可用的情况后,应用程序可以重试 AcquireTokenSilent

通过从 SignInUserAndGetTokenUsingMSAL 方法获取令牌,来实例化 Microsoft Graph Service 客户端

在项目中,创建一个名为 TokenProvider.cs 的新文件:右键单击该项目,选择“添加”>“新建项”>“空白页”。

将以下代码添加到新创建的文件中:

using Microsoft.Kiota.Abstractions.Authentication;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace UWP_app_MSGraph {
    public class TokenProvider : IAccessTokenProvider {
        private Func<string[], Task<string>> getTokenDelegate;
        private string[] scopes;

        public TokenProvider(Func<string[], Task<string>> getTokenDelegate, string[] scopes) {
            this.getTokenDelegate = getTokenDelegate;
            this.scopes = scopes;
        }

        public Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = default,
            CancellationToken cancellationToken = default) {
            return getTokenDelegate(scopes);
        }

        public AllowedHostsValidator AllowedHostsValidator { get; }
    }
}

提示

粘贴代码后,请确保 TokenProvider.cs 文件中的命名空间与项目的命名空间匹配。 这可让你更轻松地引用项目中的 TokenProvider 类。

TokenProvider 类定义了一个自定义访问令牌提供程序,该提供程序执行指定的委托方法来获取和返回访问令牌。

将以下新方法添加到 MainPage.xaml.cs

      /// <summary>
     /// Sign in user using MSAL and obtain a token for Microsoft Graph
     /// </summary>
     /// <returns>GraphServiceClient</returns>
     private async static Task<GraphServiceClient> SignInAndInitializeGraphServiceClient(string[] scopes)
     {
         var tokenProvider = new TokenProvider(SignInUserAndGetTokenUsingMSAL, scopes);
         var authProvider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
         var graphClient = new GraphServiceClient(authProvider, MSGraphURL);

         return await Task.FromResult(graphClient);
     }

在此方法中,你将使用自定义访问令牌提供程序 TokenProviderSignInUserAndGetTokenUsingMSAL 方法连接到 Microsoft Graph .NET SDK 并创建经过身份验证的客户端。

若要在 MainPage.xaml.cs 文件中使用 BaseBearerTokenAuthenticationProvider,请添加以下引用:

using Microsoft.Kiota.Abstractions.Authentication;

对受保护 API 进行 REST 调用的详细信息

在此示例应用程序中,GetGraphServiceClient 方法通过使用访问令牌来实例化 GraphServiceClient。 然后,GraphServiceClient 用于从“me”终结点获取用户配置文件信息。

添加方法以注销用户

若要注销用户,请将以下方法添加到 MainPage.xaml.cs

/// <summary>
/// Sign out the current user
/// </summary>
private async void SignOutButton_Click(object sender, RoutedEventArgs e)
{
    IEnumerable<IAccount> accounts = await PublicClientApp.GetAccountsAsync().ConfigureAwait(false);
    IAccount firstAccount = accounts.FirstOrDefault();

    try
    {
        await PublicClientApp.RemoveAsync(firstAccount).ConfigureAwait(false);
        await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
        {
            ResultText.Text = "User has signed out";
            this.CallGraphButton.Visibility = Visibility.Visible;
                this.SignOutButton.Visibility = Visibility.Collapsed;
            });
        }
        catch (MsalException ex)
        {
            ResultText.Text = $"Error signing out user: {ex.Message}";
        }
    }

MSAL.NET 使用异步方法来获取令牌或操作帐户。 同样,需要在 UI 线程中支持 UI 操作。 因此,需要进行 Dispatcher.RunAsync 调用,并在调用 ConfigureAwait(false) 之前采取预防措施。

有关注销的详细信息

SignOutButton_Click 方法从 Microsoft 身份验证库用户缓存中删除用户。 此方法有效地告知 Microsoft 身份验证库忘记当前用户。 在未来发出获取令牌的请求时,必须采用交互方式才能成功。

此示例中的应用程序支持单个用户。 Microsoft 身份验证库支持用户通过多个帐户登录的方案。 用户在其中具有多个帐户的电子邮件应用程序就是一个示例。

显示基本令牌信息

将以下方法添加到 MainPage.xaml.cs,以显示有关令牌的基本信息:

/// <summary>
/// Display basic information contained in the token. Needs to be called from the UI thread.
/// </summary>
private void DisplayBasicTokenInfo(AuthenticationResult authResult)
{
    TokenInfoText.Text = "";
    if (authResult != null)
    {
        TokenInfoText.Text += $"User Name: {authResult.Account.Username}" + Environment.NewLine;
        TokenInfoText.Text += $"Token Expires: {authResult.ExpiresOn.ToLocalTime()}" + Environment.NewLine;
    }
}

详细信息

通过 OpenID Connect 获取的 ID 令牌还包含与用户相关的一小部分信息。 DisplayBasicTokenInfo 显示令牌中包含的基本信息。 该信息包含用户的显示名称和 ID。 它还包含令牌到期日期,以及表示访问令牌本身的字符串。 如果多次选择“调用 Microsoft Graph API”按钮,将会发现后续请求重复使用了同一令牌。 而且还会发现,在 Microsoft 身份验证库决定续订令牌时,过期日期也延长了。

显示消息

将以下新方法添加到 MainPage.xaml.cs

/// <summary>
/// Displays a message in the ResultText. Can be called from any thread.
/// </summary>
private async Task DisplayMessageAsync(string message)
{
     await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
         () =>
         {
             ResultText.Text = message;
         });
     }

注册应用程序

提示

本文中的步骤可能因开始使用的门户而略有不同。

现在,注册应用程序:

  1. 至少以应用程序开发人员的身份登录到 Microsoft Entra 管理中心
  2. 如果你有权访问多个租户,请使用顶部菜单中的“设置”图标 ,通过“目录 + 订阅”菜单切换到你希望在其中注册应用程序的租户。
  3. 浏览到“标识”>“应用程序”>“应用注册”。
  4. 选择“新注册”。
  5. 输入应用程序的名称(例如 UWP-App-calling-MSGraph)。 应用的用户可能会看到此名称,你稍后可对其进行更改。
  6. 在“支持的帐户类型”下,请选择“任何组织目录(任何 Microsoft Entra 目录 - 多租户)中的帐户”
  7. 选择注册
  8. 在概览页上,找到并复制“应用程序(客户端) ID”值。 返回到 Visual Studio,打开 MainPage.xaml.cs,将 ClientId 的值替换为该值。

为应用程序配置身份验证:

  1. 在 Microsoft Entra 管理中心,选择“身份验证”>“添加平台”,然后选择“移动和桌面应用程序”。
  2. 在“重定向 URI”部分中,输入 https://login.partner.microsoftonline.cn/common/oauth2/nativeclient
  3. 选择“配置” 。

为应用程序配置 API 权限:

  1. 选择 API 权限>添加权限
  2. 选择“Microsoft Graph”。
  3. 选择“委托的权限”,搜索 User.Read 并验证是否已选择“User.Read”。
  4. 如果进行了更改,请选择“添加权限”以保存所做的更改。

在联盟域中启用集成身份验证(可选)

若要在为 Azure AD 联合域使用集成 Windows 身份验证的情况下启用该身份验证方法,应用程序清单必须启用其他功能。 回到 Visual Studio 中的应用程序。

  1. 打开 Package.appxmanifest

  2. 选择“功能”并启用以下设置:

    • 企业身份验证
    • 专用网络(客户端和服务器)
    • 共享用户证书

重要

默认情况下,未为此示例配置集成 Windows 身份验证。 请求Enterprise AuthenticationShared User Certificates功能的应用程序需要由 Windows 应用商店进行的更高级别的验证。 此外,并非所有开发人员都希望执行更高级别的验证。 仅当需要将集成 Windows 身份验证用于 Microsoft Entra 联合域时,才启用此设置。

使用 WithDefaultRedirectURI() 的替代方法

当前示例中使用了 WithRedirectUri("https://login.partner.microsoftonline.cn/common/oauth2/nativeclient") 方法。 若要使用 WithDefaultRedirectURI(),请完成以下步骤:

  1. 在 MainPage.XAML.cs 中,将 WithRedirectUri 替换为 WithDefaultRedirectUri

    当前代码

    
    PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
        .WithAuthority(Authority)
        .WithUseCorporateNetwork(false)
        .WithRedirectUri("https://login.partner.microsoftonline.cn/common/oauth2/nativeclient")
        .WithLogging((level, message, containsPii) =>
         {
             Debug.WriteLine($"MSAL: {level} {message} ");
         }, LogLevel.Warning, enablePiiLogging: false, enableDefaultPlatformLogging: true)
        .Build();
    
    

    更新的代码

    
    PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
        .WithAuthority("https://login.partner.microsoftonline.cn/common")
        .WithUseCorporateNetwork(false)
        .WithDefaultRedirectUri()
        .WithLogging((level, message, containsPii) =>
         {
             Debug.WriteLine($"MSAL: {level} {message} ");
         }, LogLevel.Warning, enablePiiLogging: false, enableDefaultPlatformLogging: true)
        .Build();
    
  2. 通过在 MainPage.xaml.cs 中添加 redirectURI 字段并在其上设置断点,来查找应用的回调 URI:

    
    public sealed partial class MainPage : Page
    {
            ...
    
            string redirectURI = Windows.Security.Authentication.Web.WebAuthenticationBroker
                                .GetCurrentApplicationCallbackUri().ToString();
            public MainPage()
            {
                ...
            }
           ...
    }
    
    

    运行应用,然后在到达断点时,复制 redirectUri 的值。 该值应该类似于以下值:ms-app://s-1-15-2-1352796503-54529114-405753024-3540103335-3203256200-511895534-1429095407/

    然后,可以删除该代码行,因为只需要使用一次即可提取该值。

  3. 在 Microsoft Entra 管理中心,在“身份验证”窗格的“RedirectUri”中添加返回值。

测试代码

若要测试应用程序,请按“F5”键,在 Visual Studio 中运行项目。 将显示主窗口:

应用程序的用户界面

准备好测试后,选择“调用 Microsoft Graph API”。 然后使用 Microsoft Entra 组织帐户登录。 用户首次运行此测试时,应用程序会显示一个要求用户登录的窗口。

首次登录应用程序时,会显示与下图类似的许可屏幕。 选择“是”显式许可访问:

访问许可屏幕

预期结果

“API 调用结果”屏幕上会显示 Microsoft 图形 API 调用返回的用户个人资料信息:

“API 调用结果”屏幕

“令牌信息”框中还会显示通过 AcquireTokenInteractiveAcquireTokenSilent 获得的令牌的相关基本信息:

属性 格式 说明
Username user@domain.com 用于标识用户的用户名。
Token Expires DateTime 令牌的过期时间。 Microsoft 身份验证库根据需要通过续订令牌来延长到期日期。

有关作用域和委派权限的详细信息

Microsoft Graph API 需要 user.read 作用域来读取用户的配置文件。 默认情况下,在应用程序注册门户中注册的每个应用程序中,都会添加此作用域。 Microsoft Graph 的其他 API 以及后端服务器的自定义 API 可能需要其他作用域。 例如,Microsoft Graph API 需要 Calendars.Read 作用域来列出用户的日历。

若要在应用程序上下文中访问用户的日历,请将 Calendars.Read 委托权限添加到应用程序注册信息。 然后,将 Calendars.Read 作用域添加到 acquireTokenSilent 调用。

增加作用域数量时,用户可能会收到接受其他许可的提示。

已知问题

问题 1

在 Microsoft Entra 联合域上登录应用程序时,可能会收到以下错误消息之一:

  • “在请求中未找到有效的客户端证书。”
  • “在用户的证书存储中未找到有效的证书。”
  • “请重试选择不同的身份验证方法。”

原因: 未启用企业功能和证书功能。

解决方案: 按照在联盟域中启用集成身份验证(可选)中的步骤操作。

问题 2

在联合域中启用集成身份验证,并尝试在 Windows 10 计算机上使用 Windows Hello 登录到配置了多重身份验证的环境。 此时将显示证书列表。 如果选择使用 PIN,则不会显示 PIN 窗口。

原因: 此问题是在 Windows 10 桌面版上运行的 UWP 应用程序中 Web 身份验证代理的已知限制。 该代理在 Windows 10 手机版上可正常工作。

解决方法: 选择“使用其他选项登录”。 然后选择“使用用户名和密码登录”。 选择“提供密码”。 然后完成手机身份验证过程。

帮助和支持

如果需要帮助、需要报告问题,或者需要详细了解支持选项,请参阅面向开发人员的帮助和支持

后续步骤

详细了解如何使用 Microsoft 身份验证库 (MSAL) 在 .NET 应用程序中进行授权和身份验证: