教程:在 Windows Presentation Foundation (WPF) 桌面应用中登录用户并调用 Microsoft Graph

在本教程中,将生成一个本机 Windows 桌面 .NET (XAML) 应用,用户可登录该应用并获取访问令牌来调用 Microsoft 图形 API。

该应用程序将使用任何使用 Microsoft Entra ID 的公司或组织提供的工作和学校帐户。

本教程的内容:

  • 在 Visual Studio 中创建 Windows Presentation Foundation (WPF) 项目
  • 安装适用于 .NET 的 Microsoft 身份验证库 (MSAL)
  • 注册应用程序
  • 添加代码以支持用户登录和注销
  • 添加代码以调用 Microsoft Graph API
  • 测试应用程序

先决条件

本指南生成的示例应用的工作原理

屏幕截图显示本教程生成的示例应用的工作原理。

本指南中创建的示例应用程序是一个 Windows 桌面应用程序,它能够查询 Microsoft Graph API 或 Web API,而该 API 接受来自 Microsoft 标识平台终结点的令牌。 对于此方案,通过 Authorization 标头将令牌添加到 HTTP 请求。 Microsoft 身份验证库 (MSAL) 处理令牌获取和续订。

负责获得用于访问受保护 Web API 的令牌

对用户进行身份验证后,示例应用程序会收到一个令牌,该令牌可用于查询受 Microsoft 标识平台保护的 Microsoft Graph API 或 Web API。

API(如 Microsoft Graph)需要令牌以允许访问特定资源。 例如,需要使用令牌才能读取用户的配置文件、访问用户日历或发送电子邮件。 应用程序可通过指定 API 作用域来使用 MSAL 请求访问令牌,从而访问这些资源。 然后对于针对受保护资源发出的每个调用,将此访问令牌添加到 HTTP 授权标头。

MSAL 负责管理缓存和刷新访问令牌,因此应用程序无需执行这些任务。

NuGet 包

本指南使用以下 NuGet 包:

说明
Microsoft.Identity.Client Microsoft 身份验证库 (MSAL.NET)

设置项目

在本部分中,你将创建一个新项目,用于演示如何将 Windows 桌面 .NET 应用程序 (XAML) 与“使用 Microsoft 登录”集成,使该应用程序能够查询需要令牌的 Web API

要创建的应用程序会显示一个用于调用 Microsoft Graph API 的按钮、一个用于显示结果的区域和一个注销按钮。

注意

想要改为下载此示例的 Visual Studio 项目? 下载一个项目,并且在执行该项目之前跳到配置步骤来配置代码示例。

使用以下步骤创建应用程序:

  1. 打开 Visual Studio
  2. 在“开始”窗口中,选择“创建新项目”。
  3. 在“所有语言”下拉列表中,选择“C#”。
  4. 搜索并选择“WPF 应用 (.NET Framework)”模板,然后选择“下一步”。
  5. 在“项目名称”框中,输入一个名称(如“Win-App-calling-MsGraph”)。
  6. 为项目选择“位置”或接受默认选项。
  7. 在“框架”中,选择“.NET Framework 4.8”。
  8. 选择“创建”。

将 MSAL 添加到项目

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

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

    Install-Package Microsoft.Identity.Client -Pre
    

注册应用程序

提示

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

若要注册并配置应用程序,请执行以下步骤:

  1. 至少以应用程序开发人员的身份登录到 Microsoft Entra 管理中心
  2. 如果你有权访问多个租户,请使用顶部菜单中的“设置”图标 ,通过“目录 + 订阅”菜单切换到你希望在其中注册应用程序的租户。
  3. 浏览到“标识”>“应用程序”>“应用注册”。
  4. 选择“新注册”。
  5. 输入应用程序的名称(例如 Win-App-calling-MsGraph)。 应用的用户可能会看到此名称,你稍后可对其进行更改。
  6. 在“支持的帐户类型”部分中,请选择“任何组织目录(任何 Microsoft Entra 目录 - 多租户)中的帐户”
  7. 选择“注册” 。
  8. 在“管理”下,选择“身份验证”>“添加平台” 。
  9. 选择“移动和桌面应用程序”。
  10. 在“重定向 URI”部分中,选择 https://login.partner.microsoftonline.cn/common/oauth2/nativeclient
  11. 选择“配置” 。

添加代码以初始化 MSAL

此步骤将创建用于处理与 MSAL 的交互(例如令牌处理)的类。

  1. 打开 App.xaml.cs 文件,将 MSAL 的引用添加到类:

    using Microsoft.Identity.Client;
    
  2. 将 App 类更新为:

    public partial class App : Application
    {
        static App()
        {
            _clientApp = PublicClientApplicationBuilder.Create(ClientId)
                .WithAuthority(AzureCloudInstance.AzureChina, Tenant)
                .WithDefaultRedirectUri()
                .Build();
        }
    
        // 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
        // - the content of Tenant by the information about the accounts allowed to sign-in in your application:
        //   - For Work or School account in your org, use your tenant ID, or domain
        //   - for any Work or School accounts, use `organizations` or `common`
        private static string ClientId = "Enter_the_Application_Id_here";
    
        private static string Tenant = "common";
    
        private static IPublicClientApplication _clientApp ;
    
        public static IPublicClientApplication PublicClientApp { get { return _clientApp; } }
    }
    

创建应用程序 UI

本部分说明应用程序如何查询受保护的后端服务器,例如 Microsoft Graph。

系统将自动创建 MainWindow.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>
        <Label Content="API Call Results" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
        <TextBox x:Name="ResultText" TextWrapping="Wrap" MinHeight="120" Margin="5" FontFamily="Segoe Ui"/>
        <Label Content="Token Info" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
        <TextBox x:Name="TokenInfoText" TextWrapping="Wrap" MinHeight="70" Margin="5" FontFamily="Segoe Ui"/>
    </StackPanel>
</Grid>

使用 MSAL 获取 Microsoft Graph API 的令牌

本部分使用 MSAL 获取 Microsoft Graph API 的令牌。

  1. MainWindow.xaml.cs 文件中,将 MSAL 的引用添加到类:

    using Microsoft.Identity.Client;
    
  2. MainWindow 类代码替换为以下代码:

    public partial class MainWindow : Window
    {
        //Set the API Endpoint to Graph 'me' endpoint
        string graphAPIEndpoint = "https://microsoftgraph.chinacloudapi.cn/v1.0/me";
    
         //Set the scope for API call to https://microsoftgraph.chinacloudapi.cn/user.read
        string[] scopes = new string[] { "https://microsoftgraph.chinacloudapi.cn/user.read" };
    
    
        public MainWindow()
        {
            InitializeComponent();
        }
    
      /// <summary>
        /// Call AcquireToken - to acquire a token requiring user to sign-in
        /// </summary>
        private async void CallGraphButton_Click(object sender, RoutedEventArgs e)
        {
            AuthenticationResult authResult = null;
            var app = App.PublicClientApp;
            ResultText.Text = string.Empty;
            TokenInfoText.Text = string.Empty;
    
            var accounts = await app.GetAccountsAsync();
            var firstAccount = accounts.FirstOrDefault();
    
            try
            {
                authResult = await app.AcquireTokenSilent(scopes, firstAccount)
                    .ExecuteAsync();
            }
            catch (MsalUiRequiredException ex)
            {
                // A MsalUiRequiredException happened on AcquireTokenSilent.
                // This indicates you need to call AcquireTokenInteractive to acquire a token
                System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
    
                try
                {
                    authResult = await app.AcquireTokenInteractive(scopes)
                        .WithAccount(accounts.FirstOrDefault())
                        .WithPrompt(Prompt.SelectAccount)
                        .ExecuteAsync();
                }
                catch (MsalException msalex)
                {
                    ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
                }
            }
            catch (Exception ex)
            {
                ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
                return;
            }
    
            if (authResult != null)
            {
                ResultText.Text = await GetHttpContentWithToken(graphAPIEndpoint, authResult.AccessToken);
                DisplayBasicTokenInfo(authResult);
                this.SignOutButton.Visibility = Visibility.Visible;
            }
        }
        }
    

详细信息

以交互方式获取用户令牌

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

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

AcquireTokenSilent 方法处理令牌获取和续订,无需进行任何用户交互。 首次执行 AcquireTokenInteractive 后,通常使用 AcquireTokenSilent 方法获得用于访问受保护资源的令牌,以便进行后续调用,因为调用请求或续订令牌都以无提示方式进行。

最终,AcquireTokenSilent 方法可能会失败。 失败可能是因为用户已注销,或者在另一设备上更改了密码。 MSAL 检测到可以通过请求交互式操作解决问题时,它将引发 MsalUiRequiredException 异常。 应用程序可以通过两种方式处理此异常:

  • 它可以立即调用 AcquireTokenInteractive。 此调用会导致系统提示用户进行登录。 此模式用于联机应用程序,这类应用程序中没有可供用户使用的脱机内容。 此设置生成的示例将遵循此模式,第一次执行示例时便可看到该模式。

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

  • 此示例中的代码随后处理此异常,方法是通过调用 AcquireTokenInteractive 使其显示用户登录提示。

  • 它可以改为向用户呈现视觉指示,要求用户进行交互式登录,以便他们可以选择适当的时间进行登录。 也可以让应用程序稍后重试 AcquireTokenSilent。 当用户可以使用其他应用程序功能而不导致中断时,会频繁使用此模式。 例如,当应用程序中存在脱机内容时。 在这种情况下,用户可以决定何时需要登录来访问受保护的资源或刷新过期信息。 也可让应用程序决定在网络临时不可用又还原后,是否重试 AcquireTokenSilent

使用刚获得的令牌调用 Microsoft Graph API

将以下新方法添加到 MainWindow.xaml.cs。 该方法用于通过 Authorize 标头对图形 API 执行 GET 请求:

/// <summary>
/// Perform an HTTP GET request to a URL using an HTTP Authorization header
/// </summary>
/// <param name="url">The URL</param>
/// <param name="token">The token</param>
/// <returns>String containing the results of the GET operation</returns>
public async Task<string> GetHttpContentWithToken(string url, string token)
{
    var httpClient = new System.Net.Http.HttpClient();
    System.Net.Http.HttpResponseMessage response;
    try
    {
        var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url);
        //Add the token in Authorization header
        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        response = await httpClient.SendAsync(request);
        var content = await response.Content.ReadAsStringAsync();
        return content;
    }
    catch (Exception ex)
    {
        return ex.ToString();
    }
}

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

在此示例应用程序中,请使用 GetHttpContentWithToken 方法对需要令牌的受保护资源发出 HTTP GET 请求,然后将内容返回给调用方。 此方法在 HTTP 授权标头中添加获取的令牌。 此示例中的资源是 Microsoft Graph API me 终结点,用于显示用户的个人资料信息。

添加注销用户的方法

将以下方法添加到 MainWindow.xaml.cs 文件可注销用户:

/// <summary>
/// Sign out the current user
/// </summary>
private async void SignOutButton_Click(object sender, RoutedEventArgs e)
{
    var accounts = await App.PublicClientApp.GetAccountsAsync();

    if (accounts.Any())
    {
        try
        {
            await App.PublicClientApp.RemoveAsync(accounts.FirstOrDefault());
            this.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}";
        }
    }
}

有关用户注销的详细信息

SignOutButton_Click 方法将移除 MSAL 用户缓存中的用户,这样可有效地告知 MSAL 忘记当前用户,使将来获取令牌的请求只能在交互模式下取得成功。

虽然此示例中的应用程序支持单个用户,但 MSAL 也支持多个帐户可能同时登录的情况。 用户在其中具有多个帐户的电子邮件应用程序就是一个示例。

显示基本令牌信息

若要显示有关令牌的基本信息,请将以下方法添加到 MainWindow.xaml.cs 文件:

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

详细信息

除了用于调用 Microsoft Graph API 的访问令牌,MSAL 还可以在用户登录后获取 ID 令牌。 此令牌包含一小部分与用户相关的信息。 DisplayBasicTokenInfo 方法显示包含在令牌中的基本信息。 例如,它显示用户的显示名称和 ID,以及令牌到期日期和表示访问令牌本身的字符串。 多次选择“调用 Microsoft Graph API”按钮,便会发现后续请求使用了同一令牌。 你还会看到,在 MSAL 确定需要续订令牌后,到期日期延长了。

测试代码

要在 Visual Studio 中运行项目,请选择 F5. 将显示应用程序的 MainWindow

首次运行该应用程序并选择“调用 Microsoft Graph API”按钮时,系统会提示登录。 使用 Microsoft Entra 帐户(工作或学校帐户)对其进行测试。

登录到应用程序。

首次登录到应用程序时,还会提示你同意允许应用程序访问你的个人资料并使你登录,如下所示:

许可应用程序访问。

查看应用程序结果

在登录后,应当会显示由 Microsoft Graph API 调用返回的用户配置文件信息。 结果会显示在“API 调用结果”框中。 有关通过调用 AcquireTokenInteractiveAcquireTokenSilent 获取的令牌的基本信息,应当会在“令牌信息”框中显示。 结果包含以下属性:

属性 格式 说明
用户名 user@domain.com 用于标识用户的用户名。
令牌到期 DateTime 令牌到期的时间。 MSAL 根据需要通过续订令牌来延长到期日期。

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

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

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

注意

当你增加作用域数量时,可能会提示用户另外进行许可。

帮助和支持

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

下一步

在我们的多部分场景系列中,详细了解如何构建可调用受保护 Web API 的桌面应用: