共用方式為


教學課程:驗證使用者以存取您的 WPF 桌面應用程式

適用於:白色圓圈內有灰色 X 符號。勞動力租戶綠色圓圈內有白色勾選符號。外部租戶(了解更多

本教學課程示範如何建置 Windows 簡報表單 (WPF) 傳統型應用程式,並準備使用 Microsoft Entra 系統管理中心進行驗證。

在本教學課程中,您會:

  • 設定 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 取代為 Directory (tenant) 子域。
    • 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。 將 Enter_the_Custom_Domain_Here 替換為您的自定義 URL 網域,並將 Enter_the_Tenant_ID_Here 替換為您的租戶ID。 如果您沒有租用戶識別碼,請了解如何讀取租用戶詳細資料
    2. 新增具有值 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",這是一個個別 APP 的讀取/寫入資料夾。

    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. 登入之後,您會看到顯示成功登入的畫面,以及所擷取令牌中所儲存用戶帳戶的基本資訊。 基礎資訊會顯示在登入畫面的 [權杖資訊] 區段中

另請參閱