共用方式為


教學課程:向 WPF 傳統型應用程式驗證使用者

本教學課程是系列的最後一個部分,示範如何使用 Microsoft Entra 系統管理中心來建置 Windows Presentation Form (WPF) 傳統型應用程式並準備進行驗證。 在此系列的第 1 部分中,您已在外部租用戶中註冊應用程式並設定使用者流程。 本教學課程示範如何建置 .NET WPF 傳統型應用程式,並使用 Microsoft Entra 外部 ID 來登入和登出使用者。

在本教學課程中,您將會:

  • 設定 WPF 傳統型應用程式以使用其應用程式註冊詳細資料。
  • 建置傳統型應用程式,以登入使用者並代表使用者來取得權杖。

必要條件

建立 WPF 傳統型應用程式

  1. 開啟您的終端機,然後導覽至您要讓專案位於其中的資料夾。

  2. 初始化 WPF 傳統型應用程式,並導覽至其根資料夾。

    dotnet new wpf --language "C#" --name sign-in-dotnet-wpf
    cd sign-in-dotnet-wpf
    

安裝套件

安裝設定提供者,以協助應用程式從應用程式設定檔案的索引鍵/值組中讀取設定資料。 這些設定抽象概念可讓您將設定值繫結至 .NET 物件的執行個體。

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder

安裝 Microsoft 驗證連結庫 (MSAL),其中包含您取得權杖所需的所有重要元件。 您也會安裝 MSAL Broker 程式庫,以處理與傳統型驗證 Broker 的互動。

dotnet add package Microsoft.Identity.Client
dotnet add package Microsoft.Identity.Client.Broker

建立 appsettings.json 檔案並新增註冊設定

  1. 在應用程式的根資料夾中,建立 appsettings.json 檔案。

  2. 將應用程式註冊詳細資料新增至 appsettings.json 檔案。

    {
        "AzureAd": {
            "Authority": "https://<Enter_the_Tenant_Subdomain_Here>.ciamlogin.com/",
            "ClientId": "<Enter_the_Application_Id_Here>"
        }
    }
    
    • Enter_the_Tenant_Subdomain_Here 取代為目錄 (租用戶) 子網域。
    • Enter_the_Application_Id_Here 取代為您稍早所註冊應用程式的應用程式 (用戶端) 識別碼。
  3. 建立應用程式設定檔案之後,我們將會建立另一個稱為 AzureAdConfig.cs 的檔案,以協助您從應用程式設定檔案中讀取設定。 在應用程式的根資料夾中,建立 AzureAdConfig.cs 檔案。

  4. AzureAdConfig.js 檔案中,定義 ClientIdAuthority 屬性的 getter 和 setter。 新增下列程式碼:

    namespace sign_in_dotnet_wpf
    {
        public class AzureAdConfig
        {
            public string Authority { get; set; }
            public string ClientId { get; set; }
        }
    }
    

使用自訂 URL 網域 (選用)

使用自訂網域對驗證 URL 進行完整品牌化。 就使用者而言,使用者在驗證過程中一直停留在您的網域中,而不會重新導向至 ciamlogin.com 網域名稱。

遵循下列步驟來使用 自訂網域:

  1. 使用針對外部租用戶中的應用程式啟用自訂 URL 網域中的步驟,為外部租用戶啟用自訂 URL 網域。

  2. 開啟 appsettings.json 檔案:

    1. Authority 屬性的值更新為 https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here。 以您的自訂 URL 網域取代 Enter_the_Custom_Domain_Here,並以您的租用戶識別碼取代 Enter_the_Tenant_ID_Here。 如果您沒有租用戶識別碼,請了解如何讀取租用戶詳細資料
    2. 新增具有值 [Enter_the_Custom_Domain_Here]knownAuthorities 屬性。

appsettings.json 檔案進行變更之後,如果您的自訂 URL 網域為 login.contoso.com,且您的租用戶識別碼為 aaaabbbb-0000-cccc-1111-dddd2222eeee,則您的檔案看起來應該類似以下程式碼片段:

{
    "AzureAd": {
        "Authority": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
        "ClientId": "Enter_the_Application_Id_Here",
        "KnownAuthorities": ["login.contoso.com"]
    }
}

修改專案檔

  1. 導覽至應用程式根資料夾中的 sign-in-dotnet-wpf.csproj 檔案。

  2. 在此檔案中,採取下列兩個步驟:

    1. 修改 sign-in-dotnet-wpf.csproj 檔案,以指示您的應用程式在編譯專案時,將 appsettings.json 檔案複製至輸出目錄。 將下列程式碼片段新增至 sign-in-dotnet-wpf.csproj 檔案:
    2. 將目標架構設定為以 windows10.0.19041.0 組建為目標,以協助您從權杖快取中讀取已快取的權杖,如您將在權杖快取協助程式類別中所看到的一樣。
    <Project Sdk="Microsoft.NET.Sdk">
    
        ...
    
        <!-- Set target framework to target windows10.0.19041.0 build -->
        <PropertyGroup>
            <OutputType>WinExe</OutputType>
            <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework> <!-- target framework -->
            <RootNamespace>sign_in_dotnet_wpf</RootNamespace>
            <Nullable>enable</Nullable>
            <UseWPF>true</UseWPF>
        </PropertyGroup>
    
        <!-- Copy appsettings.json file to output folder. -->
        <ItemGroup>
            <None Remove="appsettings.json" />
        </ItemGroup>
    
        <ItemGroup>
            <EmbeddedResource Include="appsettings.json">
                <CopyToOutputDirectory>Always</CopyToOutputDirectory>
            </EmbeddedResource>
        </ItemGroup>
    </Project>
    

建立權杖快取協助程式類別

建立可初始化權杖快取的權杖快取協助程式類別。 應用程式嘗試先從快取中讀取權杖,再嘗試取得新的權杖。 如果在快取中找不到權杖,則應用程式會取得新的權杖。 登出時,會清除所有帳戶和所有對應存取權杖的快取。

  1. 在應用程式的根資料夾中,建立 TokenCacheHelper.cs 檔案。

  2. 開啟 TokenCacheHelper.cs 檔案。 將套件和命名空間新增至檔案。 在下列步驟中,您會將相關的邏輯新增至 TokenCacheHelper 類別,以使用程式碼邏輯來填入此檔案。

    using System.IO;
    using System.Security.Cryptography;
    using Microsoft.Identity.Client;
    
    namespace sign_in_dotnet_wpf
    {
        static class TokenCacheHelper{}
    }
    
  3. 將建構函式新增至可定義快取檔案路徑的 TokenCacheHelper 類別。 對於已封裝的傳統型應用程式 (MSIX 套件,也稱為傳統型橋接器),執行中組件資料夾的狀態為「唯讀」。 在此情況下,我們需要為已封裝的應用程式使用 Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path + "\msalcache.bin",而這是個別應用程式讀取/寫入資料夾。

    namespace sign_in_dotnet_wpf
    {
        static class TokenCacheHelper
        {
            static TokenCacheHelper()
            {
                try
                {
                    CacheFilePath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, ".msalcache.bin3");
                }
                catch (System.InvalidOperationException)
                {
                    CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";
                }
            }
            public static string CacheFilePath { get; private set; }
            private static readonly object FileLock = new object();
        }
    }
    
    
  4. 新增程式碼以處理權杖快取序列化。 ITokenCache 介面會實作快取作業的公用存取權。 ITokenCache 介面包含訂閱快取序列化事件的方法,而 ITokenCacheSerializer 介面會公開您需要在快取序列化事件中使用的方法,以序列化/還原序列化快取。 TokenCacheNotificationArgs 包含可存取快取的 Microsoft.Identity.Client (MSAL) 呼叫所使用的參數。 ITokenCacheSerializer 介面可在 TokenCacheNotificationArgs 回呼中使用。

    將下列程式碼新增至 TokenCacheHelper 類別:

        static class TokenCacheHelper
        {
            static TokenCacheHelper()
            {...}
            public static string CacheFilePath { get; private set; }
            private static readonly object FileLock = new object();
    
            public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
            {
                lock (FileLock)
                {
                    args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
                            ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                                     null,
                                                     DataProtectionScope.CurrentUser)
                            : null);
                }
            }
    
            public static void AfterAccessNotification(TokenCacheNotificationArgs args)
            {
                if (args.HasStateChanged)
                {
                    lock (FileLock)
                    {
                        File.WriteAllBytes(CacheFilePath,
                                           ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
                                                                 null,
                                                                 DataProtectionScope.CurrentUser)
                                          );
                    }
                }
            }
        }
    
        internal static void EnableSerialization(ITokenCache tokenCache)
        {
            tokenCache.SetBeforeAccess(BeforeAccessNotification);
            tokenCache.SetAfterAccess(AfterAccessNotification);
        }
    

    BeforeAccessNotification 方法中,您會從檔案系統中讀取快取,而且,如果快取不是空的,則會將其還原序列化並載入。 AfterAccessNotification 方法會在 Microsoft.Identity.Client (MSAL) 存取快取之後進行呼叫。 如果快取已變更,則您會將其序列化,並持續保存快取的變更。

    EnableSerialization 包含 ITokenCache.SetBeforeAccess()ITokenCache.SetAfterAccess() 方法:

    • ITokenCache.SetBeforeAccess() 會設定要在任何程式庫方法存取快取之前收到通知的委派。 這可提供委派的選項,以還原序列化 TokenCacheNotificationArgs 中所指定應用程式和帳戶的快取項目。
    • ITokenCache.SetAfterAccess() 會設定要在任何程式庫方法存取快取之後收到通知的委派。 這可提供委派的選項,以序列化 TokenCacheNotificationArgs 中所指定應用程式和帳戶的快取項目。

建立 WPF 傳統型應用程式 UI

修改 MainWindow.xaml 檔案,以新增應用程式的 UI 元素。 在應用程式的根資料夾中,開啟 MainWindow.xaml 檔案,然後使用 <Grid></Grid> 控制項區段來新增下列程式碼片段。

    <StackPanel Background="Azure">
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
            <Button x:Name="SignInButton" Content="Sign-In" HorizontalAlignment="Right" Padding="5" Click="SignInButton_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="Authentication Result" 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>

此程式碼會新增重要 UI 元素。 可處理 UI 元素功能的方法和物件定義於我們在下一個步驟所建立的 MainWindow.xaml.cs 檔案中。

  • 可登入使用者的按鈕。 使用者選取此按鈕時,會呼叫 SignInButton_Click 方法。
  • 可登出使用者的按鈕。 使用者選取此按鈕時,會呼叫 SignOutButton_Click 方法。
  • 文字方塊,可在使用者嘗試登入之後顯示驗證結果詳細資料。 ResultText 物件會傳回此處所顯示的資訊。
  • 文字方塊,可在使用者成功登入之後顯示權杖詳細資料。 TokenInfoText 物件會傳回此處所顯示的資訊。

將程式碼新增至 MainWindow.xaml.cs 檔案

MainWindow.xaml.cs 檔案包含程式碼,而此程式碼提供 MainWindow.xaml 檔案中 UI 元素行為的執行階段邏輯。

  1. 在應用程式的根資料夾中,開啟 MainWindow.xaml.cs 檔案。

  2. 在檔案中新增下列程式碼以匯入套件,並定義我們所建立方法的預留位置。

    using Microsoft.Identity.Client;
    using System;
    using System.Linq;
    using System.Windows;
    using System.Windows.Interop;
    
    namespace sign_in_dotnet_wpf
    {
        public partial class MainWindow : Window
        {
            string[] scopes = new string[] { };
    
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private async void SignInButton_Click(object sender, RoutedEventArgs e){...}
    
            private async void SignOutButton_Click(object sender, RoutedEventArgs e){...}
    
            private void DisplayBasicTokenInfo(AuthenticationResult authResult){...}
        }
    }
    
  3. 將下列程式碼新增至 SignInButton_Click 方法。 使用者選取 [登入] 按鈕時,會呼叫此方法。

    private async void SignInButton_Click(object sender, RoutedEventArgs e)
    {
        AuthenticationResult authResult = null;
        var app = App.PublicClientApp;
    
        ResultText.Text = string.Empty;
        TokenInfoText.Text = string.Empty;
    
        IAccount firstAccount;
    
        var accounts = await app.GetAccountsAsync();
        firstAccount = accounts.FirstOrDefault();
    
        try
        {
            authResult = await app.AcquireTokenSilent(scopes, firstAccount)
                    .ExecuteAsync();
        }
        catch (MsalUiRequiredException ex)
        {
            try
            {
                authResult = await app.AcquireTokenInteractive(scopes)
                    .WithAccount(firstAccount)
                    .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle) 
                    .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 = "Sign in was successful.";
                DisplayBasicTokenInfo(authResult);
                this.SignInButton.Visibility = Visibility.Collapsed;
                this.SignOutButton.Visibility = Visibility.Visible;
            }
        }
    }
    

    GetAccountsAsync() 會傳回應用程式使用者權杖快取中的所有可用帳戶。 IAccount 介面會呈現單一帳戶的相關資訊。

    若要取得權杖,應用程式會嘗試使用 AcquireTokenSilent 方法以無訊息方式取得權杖,以確認可接受的權杖是否位於快取中。 例如,AcquireTokenSilent 方法可能會失敗,因為使用者已登出。MSAL 偵測到要求互動式動作即可解決問題時,會擲回 MsalUiRequiredException 例外狀況。 此例外狀況會導致應用程式以互動方式取得權杖。

    呼叫 AcquireTokenInteractive 方法時會顯示一個視窗,提示使用者登入。 應用程式通常需要使用者在第一次需要驗證時以互動方式登入。 取得權杖的無訊息作業失敗時,使用者也可能需要登入。 第一次執行 AcquireTokenInteractive 之後,AcquireTokenSilent 會變成用來取得權杖的一般方法

  4. 將下列程式碼新增至 SignOutButton_Click 方法。 使用者選取 [登出] 按鈕時,會呼叫此方法。

    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.TokenInfoText.Text = string.Empty;
                this.SignInButton.Visibility = Visibility.Visible;
                this.SignOutButton.Visibility = Visibility.Collapsed;
            }
            catch (MsalException ex)
            {
                ResultText.Text = $"Error signing-out user: {ex.Message}";
            }
        }
    }
    

    SignOutButton_Click 方法會清除所有帳戶和所有對應存取權杖的快取。 下次使用者嘗試登入時,必須以互動方式登入。

  5. 將下列程式碼新增至 DisplayBasicTokenInfo 方法。 此方法會顯示權杖的基本資訊。

    private void DisplayBasicTokenInfo(AuthenticationResult authResult)
    {
        TokenInfoText.Text = "";
        if (authResult != null)
        {
            TokenInfoText.Text += $"Username: {authResult.Account.Username}" + Environment.NewLine;
            TokenInfoText.Text += $"{authResult.Account.HomeAccountId}" + Environment.NewLine;
        }
    }
    

將程式碼新增至 App.xaml.cs 檔案

App.xaml 是您宣告跨應用程式使用的資源的位置。 這是您應用程式的進入點。 App.xaml.csApp.xaml 的程式碼後置檔案。 App.xaml.cs 也會定義應用程式的開始視窗。

在應用程式的根資料夾中,開啟 App.xaml.cs 檔案,然後在其中新增下列程式碼。

using System.Windows;
using System.Reflection;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;

namespace sign_in_dotnet_wpf
{
    public partial class App : Application
    {
        static App()
        {
            CreateApplication();
        }

        public static void CreateApplication()
        {
            var assembly = Assembly.GetExecutingAssembly();
            using var stream = assembly.GetManifestResourceStream("sign_in_dotnet_wpf.appsettings.json");
            AppConfiguration = new ConfigurationBuilder()
                .AddJsonStream(stream)
                .Build();

            AzureAdConfig azureADConfig = AppConfiguration.GetSection("AzureAd").Get<AzureAdConfig>();

            var builder = PublicClientApplicationBuilder.Create(azureADConfig.ClientId)
                .WithAuthority(azureADConfig.Authority)
                .WithDefaultRedirectUri();

            _clientApp = builder.Build();
            TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);
        }

        private static IPublicClientApplication _clientApp;
        private static IConfiguration AppConfiguration;
        public static IPublicClientApplication PublicClientApp { get { return _clientApp; } }
    }
}

在此步驟中,您會載入 appsettings.json 檔案。 設定建立器可協助您讀取 appsettings.json 檔案中所定義的應用程式設定。 您也會將 WPF 應用程式定義為公用用戶端應用程式,因為其為傳統型應用程式。 TokenCacheHelper.EnableSerialization 方法會啟用權杖快取序列化。

執行應用程式

執行您的應用程式,並登入以測試應用程式

  1. 在您的終端機中,導覽至 WPF 應用程式的根資料夾,然後在終端機中執行 dotnet run 命令來執行應用程式。

  2. 啟動範例之後,您應該會看到具有 [登入] 按鈕的視窗。 選取 [登入] 按鈕。

    螢幕擷取畫面:WPF 傳統型應用程式的登入畫面。

  3. 在登入頁面上,輸入您的帳戶電子郵件地址。 如果您沒有帳戶,請選取 [沒有帳戶? 建立一個],以啟動註冊流程。 請遵循此流程來建立新的帳戶並登入。

  4. 登入之後,您會看到顯示成功登入的畫面,以及已擷取權杖中所儲存使用者帳戶的基本資訊。 基礎資訊會顯示在登入畫面的 [權杖資訊] 區段中

另請參閱