教學課程:在 Windows Presentation Foundation (WPF) 傳統型應用程式中登入使用者並呼叫 Microsoft Graph

在本教學課程中,您會建置原生 Windows Desktop .NET (XAML) 應用程式,以登入使用者並取得存取令牌來呼叫 Microsoft Graph API。

完成本指南之後,您的應用程式就可以呼叫受保護的 API (使用個人帳戶,包括 outlook.com、live.com 等等)。 應用程式也可以使用任何公司或組織 (使用 Microsoft Entra ID) 的公司和學校帳戶。

在本教學課程中:

  • 在 Visual Studio 中建立 Windows Presentation Foundation (WPF) 專案
  • 安裝適用於 .NET 的 Microsoft Authentication Library (MSAL)
  • 註冊應用程式
  • 新增程式碼以支援使用者登入和登出
  • 新增程式碼以呼叫 Microsoft Graph API
  • 測試應用程式

必要條件

本指南產生之範例應用程式的運作方式

這個教學課程所產生範例應用程式的螢幕擷取畫面。

本指南建立的應用程式範例,可讓 Windows 傳統型應用程式查詢 Microsoft Graph API,或查詢從 Microsoft 身分識別平台端點接受權杖的 Web API。 針對這個案例,您會透過授權標頭將一個權杖新增到 HTTP 要求。 Microsoft Authentication Library (MSAL) 會處理權杖取得和更新作業。

處理權杖取得以存取受保護的 Web API

使用者經過驗證之後,應用程式範例就會收到一個權杖,其可用來查詢 Microsoft Graph API 或由 Microsoft 身分識別平台保護的 Web API。

例如,Microsoft Graph 需要可允許存取特定資源的權杖。 例如,需要權杖才能讀取使用者的設定檔、存取使用者的行事曆,或傳送電子郵件。 您的應用程式可以使用 MSAL 要求存取權杖,以透過指定 API 範圍來存取這些資源。 此存取權杖接著會新增到 HTTP 授權標頭,這是針對受保護資源進行之每個呼叫的授權標頭。

MSAL 會為您管理快取和重新整理存取權杖,因此您的應用程式不需要執行這些作業。

NuGet 套件

本指南會使用以下 NuGet 套件:

程式庫 描述
Microsoft.Identity.Client Microsoft Authentication Library (MSAL.NET)

設定您的專案

在本節中,您會建立新的專案,以示範如何將 Windows Desktop .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 目錄 - 多租用戶) 和個人 Microsoft 帳戶 (例如 Skype、Xbox)]
  7. 選取註冊
  8. 在 [管理] 底下,選取 [驗證]>[新增平台]
  9. 選取 [行動應用程式與傳統型應用程式]
  10. 在 [重新導向 URI] 區段中選取 https://login.microsoftonline.com/common/oauth2/nativeclient
  11. 選取設定

新增程式碼以初始化 MSAL

在這個步驟中,您建立類別來處理和 MSAL 的互動,例如處理權杖。

  1. 開啟 App.xaml.cs 檔案,然後將 MSAL 的參考新增至類別:

    using Microsoft.Identity.Client;
    
  2. 將應用程式類別更新至下面這樣:

    public partial class App : Application
    {
        static App()
        {
            _clientApp = PublicClientApplicationBuilder.Create(ClientId)
                .WithAuthority(AzureCloudInstance.AzurePublic, 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`
        //   - for any Work or School accounts, or Microsoft personal account, use `common`
        //   - for Microsoft Personal account, use consumers
        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://graph.microsoft.com/v1.0/me";
    
        //Set the scope for API call to user.read
        string[] scopes = new string[] { "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。 此方法是用來使用授權標頭針對圖形 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 帳戶 (live.com、outlook.com) 來進行測試。

登入應用程式。

您第一次登入應用程式時,系統還會提示您同意允許應用程式存取您的設定檔,並將您登入,如下所示:

同意應用程式存取。

檢視應用程式結果

登入之後,您應該會看到 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: