共用方式為


建立 Windows Hello 登入應用程式

這是完整逐步解說的第一個部分,說明如何建立使用 Windows Hello 做為傳統使用者名稱和密碼驗證系統的替代方案的已封裝 Windows 應用程式。 在此情況下,應用程式是 WinUI 應用程式,但相同的方法可以搭配任何已封裝的 Windows 應用程式使用,包括 WPF 和 Windows Forms 應用程式。 應用程式會使用使用者名稱進行登入,併為每個帳戶建立 Hello 金鑰。 這些帳戶將會受到設定 Windows Hello 之 Windows 設定中所設定的 PIN 保護。

本逐步解說分成兩個部分:建置應用程式和連線後端服務。 讀完本文後,請繼續閱讀第 2 部分:Windows Hello 登入服務

在開始之前,請先閱讀 Windows Hello 概觀,以大致了解 Windows Hello 的運作方式。

開始使用

若要建置此專案,您需要具備 C# 和 XAML 經驗。 您也必須在 Windows 10 或 Windows 11 計算機上使用 Visual Studio 2022。 如需設定開發環境的完整指示,請參閱 開始使用 WinUI

  • 在 Visual Studio 中,選取 [檔案]>[新增]>[專案]
  • 在 [ 新增專案 ] 對話框的下拉式篩選中,分別選取 C#/C++WindowsWinUI
  • 選擇 [空白應用程式]、[已封裝] (Desktop 中的 WinUI 3), 並將您的應用程式命名為 “WindowsHelloLogin”。
  • 建置並執行新的應用程式 (F5),您應該會看到畫面上顯示的空白視窗。 關閉應用程式。

第一次執行之新 Windows Hello 登入應用程式的螢幕快照

練習 1:使用 Windows Hello 登入

在本練習中,您將瞭解如何檢查電腦上是否安裝了 Windows Hello,以及如何使用 Windows Hello 登入帳戶。

  • 在新專案中,在解決方案中建立一個名為「Views」的新資料夾。 此資料夾將包含本範例中將瀏覽到的頁面。 以滑鼠右鍵按兩下 方案總管中的項目,選取 [新增>資料夾],然後將資料夾重新命名為 [檢視]。

    將名為 Views 的新資料夾新增至 Windows Hello 登入專案的螢幕快照

  • 開啟MainWindow.xaml,並以空白StackPanelGrid控件取代Window內容。 我們會在載入 MainWindow 時實作頁面導覽並流覽至新頁面,因此我們不需要 MainWindow 中的任何內容。

  • 在 XAML 中將 Title 屬性新增至 MainWindow 。 屬性看起來應該像這樣: Title="Windows Hello Login"

  • 從 MainWindow.xaml.cs 移除myButton_Click事件處理程式,以避免任何編譯錯誤。 此範例不需要此事件處理程式。

  • 以滑鼠右鍵按下新的 [檢視] 資料夾,選取 [新增>專案],然後選取 [空白頁面] 範本。 將此頁面命名為 「MainPage.xaml」。

    將新的空白頁面新增至 Windows Hello 登入項目的螢幕快照

  • 開啟App.xaml.cs檔案並更新 OnLaunched 處理程式,以實作應用程式的頁面流覽。 您也需要新增 RootFrame_NavigationFailed 處理程式方法來處理載入頁面時發生的任何錯誤。

    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        m_window = new MainWindow();
        var rootFrame = new Frame();
        rootFrame.NavigationFailed += RootFrame_NavigationFailed;
        rootFrame.Navigate(typeof(MainPage), args);
        m_window.Content = rootFrame;
        m_window.Activate();
    }
    
    private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
    {
        throw new Exception($"Error loading page {e.SourcePageType.FullName}");
    }
    
  • 您也需要將四個 using 語句新增至App.xaml.cs檔案頂端,以解決程式代碼中的編譯錯誤。

    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Navigation;
    using System;
    using WindowsHelloLogin.Views;
    
  • 以滑鼠右鍵按下新的 [檢視] 資料夾,選取 [新增>專案],然後選取 [空白頁面] 範本。 將此頁面命名為「Login.xaml」。

  • 若要定義新登入頁面的使用者介面,請新增以下 XAML。 此 XAML 會 StackPanel 定義 來對齊下列子系:

    • TextBlock,其中包含標題。

    • TextBlock錯誤訊息的 。

    • TextBox要輸入的使用者名稱的 。

    • Button要瀏覽至註冊頁面的 。

    • TextBlock,包含 Windows Hello 的狀態。

    • TextBlock說明 [登入] 頁面的 ,因為還沒有後端或已設定的使用者。

      <Grid>
        <StackPanel>
          <TextBlock Text="Login" FontSize="36" Margin="4" TextAlignment="Center"/>
          <TextBlock x:Name="ErrorMessage" Text="" FontSize="20" Margin="4" Foreground="Red" TextAlignment="Center"/>
          <TextBlock Text="Enter your username below" Margin="0,0,0,20"
                     TextWrapping="Wrap" Width="300"
                     TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
          <TextBox x:Name="UsernameTextBox" Margin="4" Width="250"/>
          <Button x:Name="LoginButton" Content="Login" Background="DodgerBlue" Foreground="White"
                  Click="LoginButton_Click" Width="80" HorizontalAlignment="Center" Margin="0,20"/>
          <TextBlock Text="Don't have an account?"
                     TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
          <TextBlock x:Name="RegisterButtonTextBlock" Text="Register now"
                     PointerPressed="RegisterButtonTextBlock_OnPointerPressed"
                     Foreground="DodgerBlue"
                     TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
          <Border x:Name="WindowsHelloStatus" Background="#22B14C"
                  Margin="0,20" Height="100" >
            <TextBlock x:Name="WindowsHelloStatusText" Text="Windows Hello is ready to use!"
                       Margin="4" TextAlignment="Center" VerticalAlignment="Center" FontSize="20"/>
          </Border>
          <TextBlock x:Name="LoginExplanation" FontSize="24" TextAlignment="Center" TextWrapping="Wrap" 
                     Text="Please Note: To demonstrate a login, validation will only occur using the default username 'sampleUsername'"/>
        </StackPanel>
      </Grid>
      
  • 您必須將一些方法新增至程式代碼後置檔案,以取得解決方案建置。 按 F7 或使用 方案總管 來編輯Login.xaml.cs檔案。 在下列兩個事件方法中新增 ,以處理 LoginRegister 事件。 現在,這些方法會將 設定 ErrorMessage.Text 為空字串。 請務必包含下列using語句。 後續步驟需要它們。

    using Microsoft.UI.Xaml;
    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Input;
    using Microsoft.UI.Xaml.Media;
    using Microsoft.UI.Xaml.Navigation;
    using System;
    
    namespace WindowsHelloLogin.Views
    {
        public sealed partial class Login : Page
        {
            public Login()
            {
                this.InitializeComponent();
            }
            private void LoginButton_Click(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
            private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
        }
    }
    
  • 若要轉譯 [登入] 頁面,請編輯 MainPage 程式代碼,以在載入 MainPage流覽至 [登入] 頁面。 開啟 MainPage.xaml.cs 檔案。 在 方案總管 中,按兩下MainPage.xaml.cs。 如果找不到此專案,請按兩下MainPage.xaml旁的小箭號以顯示程式代碼後置檔案。 建立將巡覽至 [登入] 頁面的 Loaded 事件處理程式方法。

    namespace WindowsHelloLogin.Views
    {
        public sealed partial class MainPage : Page
        {
            public MainPage()
            {
                this.InitializeComponent();
                Loaded += MainPage_Loaded;
            }
    
            private void MainPage_Loaded(object sender, RoutedEventArgs e)
            {
                Frame.Navigate(typeof(Login));
            }
        }
    }
    
  • 在 [ 登入 ] 頁面中,您必須處理 OnNavigatedTo 事件,以驗證目前計算機上是否有 Windows Hello 可用。 在 Login.xaml.cs 中,實作下列程序代碼。 您會發現 WindowsHelloHelper 物件指出發生錯誤。 這是因為我們尚未建立此協助程序類別。

    public sealed partial class Login : Page
    {
        public Login()
        {
            this.InitializeComponent();
        }
    
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            // Check if Windows Hello is set up and available on this machine
            if (await WindowsHelloHelper.WindowsHelloAvailableCheckAsync())
            {
            }
            else
            {
                // Windows Hello isn't set up, so inform the user
                WindowsHelloStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                WindowsHelloStatusText.Text = $"Windows Hello is not set up!{Environment.NewLine}Please go to Windows Settings and set up a PIN to use it.";
                LoginButton.IsEnabled = false;
            }
        }
    }
    
  • 若要建立 WindowsHelloHelper 類別,請以滑鼠右鍵按兩下 WindowsHelloLogin 專案,然後按兩下 [ 新增>資料夾]。 將此資料夾 命名為Utils

  • 滑鼠右鍵按兩下 [Utils] 資料夾,然後選取[新增>類別]。 將此新類別命名為 「WindowsHelloHelper.cs」。

    建立 Windows Hello 登入協助程式類別的螢幕快照

  • 將 WindowsHelloHelper 類別的範圍變更為 public static,然後新增下列方法,告知使用者是否準備好使用 Windows Hello。 您必須新增必要的命名空間。

    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using Windows.Security.Credentials;
    
    namespace WindowsHelloLogin.Utils
    {
        public static class WindowsHelloHelper
        {
            /// <summary>
            /// Checks to see if Windows Hello is ready to be used.
            /// 
            /// Windows Hello has dependencies on:
            ///     1. Having a connected Microsoft Account
            ///     2. Having a Windows PIN set up for that account on the local machine
            /// </summary>
            public static async Task<bool> WindowsHelloAvailableCheckAsync()
            {
                bool keyCredentialAvailable = await KeyCredentialManager.IsSupportedAsync();
                if (keyCredentialAvailable == false)
                {
                    // Key credential is not enabled yet as user 
                    // needs to connect to a Microsoft Account and select a PIN in the connecting flow.
                    Debug.WriteLine("Windows Hello is not set up!\nPlease go to Windows Settings and set up a PIN to use it.");
                    return false;
                }
    
                return true;
            }
        }
    }
    
  • 在 Login.xaml.cs 中,新增命名空間的 WindowsHelloLogin.Utils 參考。 這會解決 方法中的 OnNavigatedTo 錯誤。

    using WindowsHelloLogin.Utils;
    
  • 建置並執行應用程式。 您將流覽至登入頁面,而 Windows Hello 橫幅會指出是否已準備好使用 Windows Hello。 您應該會看到綠色或藍色橫幅,表示電腦上的 Windows Hello 狀態。

    具有就緒狀態的 Windows Hello 登入畫面螢幕快照

  • 接下來您需要建置登入邏輯。 在名為 「Models」 的專案中建立新資料夾。

  • 在 Models 資料夾中,建立名為 「Account.cs」 的新類別。 此類別將作為您的帳戶模型。 由於這是範例專案,它只會包含用戶名稱。 將類別範圍變更為 public ,並新增 Username 屬性。

    namespace WindowsHelloLogin.Models
    {
        public class Account
        {
            public string Username { get; set; }
        }
    }
    
  • 應用程式需要處理帳戶的方法。 針對此實際操作實驗室,因為沒有伺服器或資料庫,因此會在本機儲存並載入用戶清單。 以 滑鼠右鍵按兩下 Utils 資料夾,然後新增名為 「AccountHelper.cs」 的新類別。 將類別範圍變更為 public staticAccountHelper 是靜態類別,其中包含儲存及載入本機帳戶清單所需的所有方法。 儲存和載入的運作方式是使用 XmlSerializer。 您也需要記住已儲存的檔案,以及儲存的位置。

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    using System.Xml.Serialization;
    using Windows.Storage;
    using WindowsHelloLogin.Models;
    
    namespace WindowsHelloLogin.Utils
    {
        public static class AccountHelper
        {
            // In the real world this would not be needed as there would be a server implemented that would host a user account database.
            // For this tutorial we will just be storing accounts locally.
            private const string USER_ACCOUNT_LIST_FILE_NAME = "accountlist.txt";
            private static string _accountListPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, USER_ACCOUNT_LIST_FILE_NAME);
            public static List<Account> AccountList = [];
    
            /// <summary>
            /// Create and save a useraccount list file. (Updating the old one)
            /// </summary>
            private static async void SaveAccountListAsync()
            {
                string accountsXml = SerializeAccountListToXml();
    
                if (File.Exists(_accountListPath))
                {
                    StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_accountListPath);
                    await FileIO.WriteTextAsync(accountsFile, accountsXml);
                }
                else
                {
                    StorageFile accountsFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(USER_ACCOUNT_LIST_FILE_NAME);
                    await FileIO.WriteTextAsync(accountsFile, accountsXml);
                }
            }
    
            /// <summary>
            /// Gets the useraccount list file and deserializes it from XML to a list of useraccount objects.
            /// </summary>
            /// <returns>List of useraccount objects</returns>
            public static async Task<List<Account>> LoadAccountListAsync()
            {
                if (File.Exists(_accountListPath))
                {
                    StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_accountListPath);
    
                    string accountsXml = await FileIO.ReadTextAsync(accountsFile);
                    DeserializeXmlToAccountList(accountsXml);
                }
    
                return AccountList;
            }
    
            /// <summary>
            /// Uses the local list of accounts and returns an XML formatted string representing the list
            /// </summary>
            /// <returns>XML formatted list of accounts</returns>
            public static string SerializeAccountListToXml()
            {
                var xmlizer = new XmlSerializer(typeof(List<Account>));
                var writer = new StringWriter();
                xmlizer.Serialize(writer, AccountList);
    
                return writer.ToString();
            }
    
            /// <summary>
            /// Takes an XML formatted string representing a list of accounts and returns a list object of accounts
            /// </summary>
            /// <param name="listAsXml">XML formatted list of accounts</param>
            /// <returns>List object of accounts</returns>
            public static List<Account> DeserializeXmlToAccountList(string listAsXml)
            {
                var xmlizer = new XmlSerializer(typeof(List<Account>));
                TextReader textreader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(listAsXml)));
    
                return AccountList = (xmlizer.Deserialize(textreader)) as List<Account>;
            }
        }
    }
    
  • 接下來,實作從本機帳戶清單中新增和移除帳戶的方式。 這些動作會分別儲存清單。 此實際操作實驗室所需的最後一個方法是驗證方法。 由於沒有授權伺服器或用戶資料庫,這會針對硬式編碼的單一使用者進行驗證。 這些方法應該新增至 AccountHelper 類別。

    public static Account AddAccount(string username)
    {
        // Create a new account with the username
        var account = new Account() { Username = username };
        // Add it to the local list of accounts
        AccountList.Add(account);
        // SaveAccountList and return the account
        SaveAccountListAsync();
        return account;
    }
    
    public static void RemoveAccount(Account account)
    {
        // Remove the account from the accounts list
        AccountList.Remove(account);
        // Re save the updated list
        SaveAccountListAsync();
    }
    
    public static bool ValidateAccountCredentials(string username)
    {
        // In the real world, this method would call the server to authenticate that the account exists and is valid.
        // However, for this tutorial, we'll just have an existing sample user that's named "sampleUsername".
        // If the username is null or does not match "sampleUsername" validation will fail. 
        // In this case, the user should register a new Windows Hello user.
    
        if (string.IsNullOrEmpty(username))
        {
            return false;
        }
    
        if (!string.Equals(username, "sampleUsername"))
        {
            return false;
        }
    
        return true;
    }
    
  • 您需要做的下一件事是處理使用者的登入要求。 在 Login.xaml.cs中,建立將保存目前帳戶登入的新私用變數。 然後新增名為 SignInWindowsHelloAsync 的新方法。 這會使用 AccountHelper.ValidateAccountCredentials 方法來驗證帳戶認證 。 如果輸入的使用者名稱與您在上一 個步驟中設定的硬式編碼字元串值相同,這個方法會傳回 Boolean 值。 此範例的硬式編碼值為 「sampleUsername」。

    using WindowsHelloLogin.Models;
    using WindowsHelloLogin.Utils;
    using System.Diagnostics;
    using System.Threading.Tasks;
    
    namespace WindowsHelloLogin.Views
    {
        public sealed partial class Login : Page
        {
            private Account _account;
    
            public Login()
            {
                this.InitializeComponent();
            }
    
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                // Check if Windows Hello is set up and available on this machine
                if (await WindowsHelloHelper.WindowsHelloAvailableCheckAsync())
                {
                }
                else
                {
                    // Windows Hello is not set up, so inform the user
                    WindowsHelloStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                    WindowsHelloStatusText.Text = "Windows Hello is not set up!\nPlease go to Windows Settings and set up a PIN to use it.";
                    LoginButton.IsEnabled = false;
                }
            }
    
            private async void LoginButton_Click(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
                await SignInWindowsHelloAsync();
            }
    
            private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
    
            private async Task SignInWindowsHelloAsync()
            {
                if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
                {
                    // Create and add a new local account
                    _account = AccountHelper.AddAccount(UsernameTextBox.Text);
                    Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
                    //if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(UsernameTextBox.Text))
                    //{
                    //    Debug.WriteLine("Successfully signed in with Windows Hello!");
                    //}
                }
                else
                {
                    ErrorMessage.Text = "Invalid Credentials";
                }
            }
        }
    }
    
  • 您可能已經注意到在 WindowsHelloHelper參考方法的批注程序代碼。 在WindowsHelloHelper.cs中,新增名為 CreateWindowsHelloKeyAsync 的新方法。 此方法會使用 KeyCredentialManager 中的 Windows Hello API。 呼叫 RequestCreateAsync 會建立 accountId 和本機電腦專屬的 Windows Hello 金鑰。 如果您想在現實場景中實作此功能,請注意 switch 陳述式中的註解。

    /// <summary>
    /// Creates a Windows Hello key on the machine using the account ID provided.
    /// </summary>
    /// <param name="accountId">The account ID associated with the account that we are enrolling into Windows Hello</param>
    /// <returns>Boolean indicating if creating the Windows Hello key succeeded</returns>
    public static async Task<bool> CreateWindowsHelloKeyAsync(string accountId)
    {
        KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(accountId, KeyCredentialCreationOption.ReplaceExisting);
    
        switch (keyCreationResult.Status)
        {
            case KeyCredentialStatus.Success:
                Debug.WriteLine("Successfully created key");
    
                // In the real world, authentication would take place on a server.
                // So, every time a user migrates or creates a new Windows Hello
                // account, details should be pushed to the server.
                // The details that would be pushed to the server include:
                // The public key, keyAttestation (if available), 
                // certificate chain for attestation endorsement key (if available),  
                // status code of key attestation result: keyAttestationIncluded or 
                // keyAttestationCanBeRetrievedLater and keyAttestationRetryType.
                // As this sample has no concept of a server, it will be skipped for now.
                // For information on how to do this, refer to the second sample.
    
                // For this sample, just return true
                return true;
            case KeyCredentialStatus.UserCanceled:
                Debug.WriteLine("User cancelled sign-in process.");
                break;
            case KeyCredentialStatus.NotFound:
                // User needs to set up Windows Hello
                Debug.WriteLine("Windows Hello is not set up!\nPlease go to Windows Settings and set up a PIN to use it.");
                break;
            default:
                break;
        }
    
        return false;
    }
    
  • 現在您已建立 CreateWindowsHelloKeyAsync 方法,請返回Login.xaml.cs檔案,並取消批注 SignInWindowsHelloAsync 方法內的程式碼。

    private async void SignInWindowsHelloAsync()
    {
        if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            //Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Windows Hello!");
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 建置並執行應用程式。 系統將帶您前往「登入」頁面。 輸入使用者名稱作為 「sampleUsername」,然後按兩下 [登入]。 系統將顯示 Windows Hello 提示,並要求您輸入 PIN。 正確輸入 PIN 碼時, CreateWindowsHelloKeyAsync 方法將能夠建立 Windows Hello 密鑰。 監視輸出視窗,以查看是否顯示指示成功的訊息。

    Windows Hello 登入釘選提示的螢幕快照

練習 2:歡迎和使用者選取畫面

在本練習中,您將接續上一個練習。 當使用者成功登入時,應該將其帶到可註銷或刪除其帳戶的歡迎頁面。 由於 Windows Hello 會為每台電腦建立金鑰,因此可以建立使用者選取畫面,其中會顯示已在該電腦上登入的所有使用者。 然後,使用者可以選取其中一個帳戶,並直接進入歡迎畫面,而不需要重新輸入密碼,因為他們已經通過驗證,能夠存取電腦。

  • 在 Views 資料夾中,新增名為 「Welcome.xaml」 的新空白頁面。 新增下列 XAML 以完成頁面的用戶介面。 這將顯示標題、登入的使用者名稱和兩個按鈕。 其中一個按鈕將瀏覽回使用者清單 (在稍後建立),另一個按鈕將處理忘記該使用者。

    <Grid>
      <StackPanel>
        <TextBlock x:Name="Title" Text="Welcome" FontSize="40" TextAlignment="Center"/>
        <TextBlock x:Name="UserNameText" FontSize="28" TextAlignment="Center"/>
    
        <Button x:Name="BackToUserListButton" Content="Back to User List" Click="Button_Restart_Click"
                HorizontalAlignment="Center" Margin="0,20" Foreground="White" Background="DodgerBlue"/>
    
        <Button x:Name="ForgetButton" Content="Forget Me" Click="Button_Forget_User_Click"
                Foreground="White"
                Background="Gray"
                HorizontalAlignment="Center"/>
      </StackPanel>
    </Grid>
    
  • 在Welcome.xaml.cs程序代碼後置檔案中,新增將保留已登入帳戶的新私用變數。 您必須實作方法來覆寫OnNavigateTo事件,這會儲存傳遞至歡迎頁面的帳戶。 您也必須針對 XAML 中定義的兩個按鈕實 Click 作 事件。 您必須為 和 WindowsHelloLogin.Utils 命名空間新增using語句WindowsHelloLogin.Models

    using WindowsHelloLogin.Models;
    using WindowsHelloLogin.Utils;
    using System.Diagnostics;
    
    namespace WindowsHelloLogin.Views
    {
        public sealed partial class Welcome : Page
        {
            private Account _activeAccount;
    
            public Welcome()
            {
                InitializeComponent();
            }
    
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                _activeAccount = (Account)e.Parameter;
                if (_activeAccount != null)
                {
                    UserNameText.Text = _activeAccount.Username;
                }
            }
    
            private void Button_Restart_Click(object sender, RoutedEventArgs e)
            {
            }
    
            private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
            {
                // Remove the account from Windows Hello
                // WindowsHelloHelper.RemoveWindowsHelloAccountAsync(_activeAccount);
    
                // Remove it from the local accounts list and re-save the updated list
                AccountHelper.RemoveAccount(_activeAccount);
    
                Debug.WriteLine($"User {_activeAccount.Username} deleted.");
            }
        }
    }
    
  • 您可能注意到事件處理程式中有 Button_Forget_User_Click 一行已批注。 該帳戶正在從您的本機清單中移除,但目前無法從 Windows Hello 中移除。 您必須在WindowsHelloHelper.cs中實作新的方法,以處理移除 Windows Hello 使用者。 此方法將使用其他 Windows Hello API 來開啟和刪除帳戶。 在真實世界中,當您刪除帳戶時,伺服器或資料庫應該會收到通知,讓使用者資料庫維持有效狀態。 您將需要參考命名空間的 WindowsHelloLogin.Models using 語句。

    using WindowsHelloLogin.Models;
    
    /// <summary>
    /// Function to be called when user requests deleting their account.
    /// Checks the KeyCredentialManager to see if there is a Windows Hello
    /// account for the current user.
    /// It then deletes the local key associated with the account.
    /// </summary>
    public static async void RemoveWindowsHelloAccountAsync(Account account)
    {
        // Open the account with Windows Hello
        KeyCredentialRetrievalResult keyOpenResult = await KeyCredentialManager.OpenAsync(account.Username);
    
        if (keyOpenResult.Status == KeyCredentialStatus.Success)
        {
            // In the real world you would send key information to server to unregister
            //for example, RemoveWindowsHelloAccountOnServer(account);
        }
    
        // Then delete the account from the machine's list of Windows Hello accounts
        await KeyCredentialManager.DeleteAsync(account.Username);
    }
    
  • 回到Welcome.xaml.cs,取消批注呼叫 RemoveWindowsHelloAccountAsync 的行。

    private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
    {
        // Remove it from Windows Hello
        WindowsHelloHelper.RemoveWindowsHelloAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and re-save the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine($"User {_activeAccount.Username} deleted.");
    }
    
  • SignInWindowsHelloAsync 方法中(在 Login.xaml.cs 中),一旦 CreateWindowsHelloKeyAsync 成功,它應該流覽至 [歡迎] 頁面並傳遞 [帳戶]。

    private async void SignInWindowsHelloAsync()
    {
        if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            // Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Windows Hello!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 建置並執行應用程式。 使用 “sampleUsername” 登入,然後按兩下 [ 登入]。 輸入您的 PIN,如果成功,您應該瀏覽至 歡迎 畫面。 嘗試單擊 [ 忘記使用者 ] 並監視 Visual Studio 的 [輸出 ] 視窗,以查看使用者是否已刪除。 請注意,刪除使用者時,您會保留在 [歡迎] 頁面上。 您必須建立應用程式可以瀏覽的使用者選取頁面。

    Windows Hello 歡迎畫面的螢幕快照

  • 在 Views 資料夾中,建立名為 「UserSelection.xaml」 的新空白頁面,並新增下列 XAML 來定義使用者介面。 此頁面將包含 ListView ,其中顯示本機帳戶清單中的所有使用者,以及 Button 會巡覽至 [登入 ] 頁面,以允許使用者新增另一個帳戶。

    <Grid>
      <StackPanel>
        <TextBlock x:Name="Title" Text="Select a User" FontSize="36" Margin="4" TextAlignment="Center" HorizontalAlignment="Center"/>
    
        <ListView x:Name="UserListView" Margin="4" MaxHeight="200" MinWidth="250" Width="250" HorizontalAlignment="Center">
          <ListView.ItemTemplate>
            <DataTemplate>
              <Grid Background="DodgerBlue" Height="50" Width="250" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <TextBlock Text="{Binding Username}" HorizontalAlignment="Center" TextAlignment="Center" VerticalAlignment="Center" Foreground="White"/>
              </Grid>
            </DataTemplate>
          </ListView.ItemTemplate>
        </ListView>
    
        <Button x:Name="AddUserButton" Content="+" FontSize="36" Width="60" Click="AddUserButton_Click" HorizontalAlignment="Center"/>
      </StackPanel>
    </Grid>
    
  • 在 UserSelection.xaml.cs中,實 Loaded 作 方法,如果本機清單中沒有帳戶,則會巡覽至 [登入 ] 頁面。 也實作 SelectionChanged ListView 的 事件,以及 ClickButton事件。

    using System.Diagnostics;
    using WindowsHelloLogin.Models;
    using WindowsHelloLogin.Utils;
    
    namespace WindowsHelloLogin.Views
    {
        public sealed partial class UserSelection : Page
        {
            public UserSelection()
            {
                InitializeComponent();
                Loaded += UserSelection_Loaded;
            }
    
            private void UserSelection_Loaded(object sender, RoutedEventArgs e)
            {
                if (AccountHelper.AccountList.Count == 0)
                {
                    // If there are no accounts, navigate to the Login page
                    Frame.Navigate(typeof(Login));
                }
    
    
                UserListView.ItemsSource = AccountHelper.AccountList;
                UserListView.SelectionChanged += UserSelectionChanged;
            }
    
            /// <summary>
            /// Function called when an account is selected in the list of accounts
            /// Navigates to the Login page and passes the chosen account
            /// </summary>
            private void UserSelectionChanged(object sender, RoutedEventArgs e)
            {
                if (((ListView)sender).SelectedValue != null)
                {
                    Account account = (Account)((ListView)sender).SelectedValue;
                    if (account != null)
                    {
                        Debug.WriteLine($"Account {account.Username} selected!");
                    }
                    Frame.Navigate(typeof(Login), account);
                }
            }
    
            /// <summary>
            /// Function called when the "+" button is clicked to add a new user.
            /// Navigates to the Login page with nothing filled out
            /// </summary>
            private void AddUserButton_Click(object sender, RoutedEventArgs e)
            {
                Frame.Navigate(typeof(Login));
            }
        }
    }
    
  • 應用程式中有幾個地方您想要流覽至 UserSelection 頁面。 在MainPage.xaml.cs中,您應該流覽至 UserSelection 頁面, 而不是 [登入 ] 頁面。 當您在 MainPage 的載入事件中時,您必須載入帳戶清單,讓 UserSelection 頁面可以檢查是否有任何帳戶。 這需要將 方法變更 Loaded 為異步,同時新增 命名空間的 WindowsHelloLogin.Utils using語句。

    using WindowsHelloLogin.Utils;
    
    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        // Load the local account list before navigating to the UserSelection page
        await AccountHelper.LoadAccountListAsync();
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 接下來,應用程式需要從 [歡迎] 頁面流覽至 [UserSelection] 頁面。 在這兩 Click 個事件中,您應該流覽回 UserSelection 頁面。

    private void Button_Restart_Click(object sender, RoutedEventArgs e)
    {
        Frame.Navigate(typeof(UserSelection));
    }
    
    private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
    {
        // Remove it from Windows Hello
        WindowsHelloHelper.RemoveWindowsHelloAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and re-save the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine($"User {_activeAccount.Username} deleted.");
    
        // Navigate back to UserSelection page.
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 在 [登入] 頁面中,您需要程序代碼才能登入從UserSelection 頁面中清單中選取的帳戶。 在事件中 OnNavigatedTo ,儲存瀏覽期間傳遞的帳戶。 首先新增一個新的私有變數,該變數將識別該帳戶是否為現有帳戶。 然後處理 OnNavigatedTo 事件。

    namespace WindowsHelloLogin.Views
    {
        public sealed partial class Login : Page
        {
            private Account _account;
            private bool _isExistingAccount;
    
            public Login()
            {
                InitializeComponent();
            }
    
            /// <summary>
            /// Function called when this frame is navigated to.
            /// Checks to see if Windows Hello is available and if an account was passed in.
            /// If an account was passed in set the "_isExistingAccount" flag to true and set the _account.
            /// </summary>
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                // Check Windows Hello is set up and available on this machine
                if (await WindowsHelloHelper.WindowsHelloAvailableCheckAsync())
                {
                    if (e.Parameter != null)
                    {
                        _isExistingAccount = true;
                        // Set the account to the existing account being passed in
                        _account = (Account)e.Parameter;
                        UsernameTextBox.Text = _account.Username;
                        await SignInWindowsHelloAsync();
                    }
                }
                else
                {
                    // Windows Hello is not set up, so inform the user
                    WindowsHelloStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                    WindowsHelloStatusText.Text = $"Windows Hello is not set up!{Environment.NewLine}Please go to Windows Settings and set up a PIN to use it.";
                    LoginButton.IsEnabled = false;
                }
            }
        }
    }
    
  • SignInWindowsHelloAsync 方法必須更新,才能登入選取的帳戶。 WindowsHelloHelper 需要另一種方法,才能使用 Windows Hello 開啟帳戶,因為帳戶已經有為其建立的帳戶密鑰。 在 WindowsHelloHelper.cs 中實作新的方法,以使用 Windows Hello 登入現有的使用者。 如需程式代碼每個部分的資訊,請閱讀程式代碼批註。

    /// <summary>
    /// Attempts to sign a message using the account key on the system for the accountId passed.
    /// </summary>
    /// <returns>Boolean representing if creating the Windows Hello authentication message succeeded</returns>
    public static async Task<bool> GetWindowsHelloAuthenticationMessageAsync(Account account)
    {
        KeyCredentialRetrievalResult openKeyResult = await KeyCredentialManager.OpenAsync(account.Username);
        // Calling OpenAsync will allow the user access to what is available in the app and will not require user credentials again.
        // If you wanted to force the user to sign in again you can use the following:
        // var consentResult = await Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync(account.Username);
        // This will ask for the either the password of the currently signed in Microsoft Account or the PIN used for Windows Hello.
    
        if (openKeyResult.Status == KeyCredentialStatus.Success)
        {
            // If OpenAsync has succeeded, the next thing to think about is whether the client application requires access to backend services.
            // If it does here you would request a challenge from the server. The client would sign this challenge and the server
            // would check the signed challenge. If it is correct, it would allow the user access to the backend.
            // You would likely make a new method called RequestSignAsync to handle all this.
            // For example, RequestSignAsync(openKeyResult);
            // Refer to the second Windows Hello sample for information on how to do this.
    
            // For this sample, there is not concept of a server implemented so just return true.
            return true;
        }
        else if (openKeyResult.Status == KeyCredentialStatus.NotFound)
        {
            // If the account is not found at this stage. It could be one of two errors. 
            // 1. Windows Hello has been disabled
            // 2. Windows Hello has been disabled and re-enabled cause the Windows Hello Key to change.
            // Calling CreateWindowsHelloKeyAsync and passing through the account will attempt to replace the existing Windows Hello Key for that account.
            // If the error really is that Windows Hello is disabled then the CreateWindowsHelloKeyAsync method will output that error.
            if (await CreateWindowsHelloKeyAsync(account.Username))
            {
                // If the Hello Key was again successfully created, Windows Hello has just been reset.
                // Now that the Hello Key has been reset for the account retry sign in.
                return await GetWindowsHelloAuthenticationMessageAsync(account);
            }
        }
    
        // Can't use Windows Hello right now, try again later
        return false;
    }
    
  • 更新 Login.xaml.cs 中的 SignInWindowsHelloAsync 方法來處理現有的帳戶。 這會在WindowsHelloHelper.cs中使用新的方法。 如果成功,帳戶將會登入,且使用者流覽至 歡迎 頁面。

    private async Task SignInWindowsHelloAsync()
    {
        if (_isExistingAccount)
        {
            if (await WindowsHelloHelper.GetWindowsHelloAuthenticationMessageAsync(_account))
            {
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            //Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Windows Hello!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 建置並執行應用程式。 使用「sampleUsername」登入。 輸入您的 PIN,如果成功,您將流覽至 [歡迎] 頁面。 按一下返回使用者清單。 現在您應該會在清單中看到一個使用者。 如果您按下此選項,WindowsHello 可讓您重新登入,而不需要重新輸入任何密碼等。

    Windows Hello 選取使用者清單的螢幕快照

練習 3:註冊新的 Windows Hello 使用者

在此練習中,您會建立新的頁面,以使用 Windows Hello 建立新的帳戶。 這與 [ 登入 ] 頁面的運作方式類似。 針對移轉為使用 Windows Hello 的現有用戶實作 [ 登入 ] 頁面。 WindowsHelloRegister 頁面將會為新使用者建立 Windows Hello 註冊。

  • 在 Views 資料夾中,建立名為 「WindowsHelloRegister.xaml」 的新空白頁面。 在 XAML 中新增以下內容,以設定使用者介面。 此頁面上的介面類似於 [ 登入 ] 頁面。

    <Grid>
      <StackPanel>
        <TextBlock x:Name="Title" Text="Register New Windows Hello User" FontSize="24" Margin="4" TextAlignment="Center"/>
    
        <TextBlock x:Name="ErrorMessage" Text="" FontSize="20" Margin="4" Foreground="Red" TextAlignment="Center"/>
    
        <TextBlock Text="Enter your new username below" Margin="0,0,0,20"
                   TextWrapping="Wrap" Width="300"
                   TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
    
        <TextBox x:Name="UsernameTextBox" Margin="4" Width="250"/>
    
        <Button x:Name="RegisterButton" Content="Register" Background="DodgerBlue" Foreground="White"
                Click="RegisterButton_Click_Async" Width="80" HorizontalAlignment="Center" Margin="0,20"/>
    
        <Border x:Name="WindowsHelloStatus" Background="#22B14C" Margin="4" Height="100">
          <TextBlock x:Name="WindowsHelloStatusText" Text="Windows Hello is ready to use!" FontSize="20"
                     Margin="4" TextAlignment="Center" VerticalAlignment="Center"/>
        </Border>
      </StackPanel>
    </Grid>
    
  • 在WindowsHelloRegister.xaml.cs程式代碼後置檔案中,實作私 Account 用變數和 Click 註冊按鈕的事件。 這會新增本機帳戶並建立 Windows Hello 金鑰。

    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml;
    using WindowsHelloLogin.Models;
    using WindowsHelloLogin.Utils;
    
    namespace WindowsHelloLogin.Views
    {
        public sealed partial class WindowsHelloRegister : Page
        {
            private Account _account;
    
            public WindowsHelloRegister()
            {
                InitializeComponent();
            }
    
            private async void RegisterButton_Click_Async(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
    
                // In the real world, you would validate the entered credentials and information before 
                // allowing a user to register a new account. 
                // For this sample, we'll skip that step and just register an account if the username is not null.
    
                if (!string.IsNullOrEmpty(UsernameTextBox.Text))
                {
                    // Register a new account
                    _account = AccountHelper.AddAccount(UsernameTextBox.Text);
                    // Register new account with Windows Hello
                    await WindowsHelloHelper.CreateWindowsHelloKeyAsync(_account.Username);
                    // Navigate to the Welcome page. 
                    Frame.Navigate(typeof(Welcome), _account);
                }
                else
                {
                    ErrorMessage.Text = "Please enter a username";
                }
            }
        }
    }
    
  • 按兩下註冊時,您必須從 [登入 ] 頁面流覽至此頁面。

    private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
    {
        ErrorMessage.Text = "";
        Frame.Navigate(typeof(WindowsHelloRegister));
    }
    
  • 建置並執行應用程式。 嘗試註冊一個新使用者。 然後返回使用者清單,並確認您是否可以選擇該使用者並登入。

    Windows Hello 註冊新用戶頁面的螢幕快照

在此實驗室中,您已瞭解使用新 Windows Hello API 驗證現有使用者及為新使用者建立帳戶所需的基本技能。 有了這個新的知識,您就可以開始移除使用者記住應用程式密碼的需求,但仍確信您的應用程式仍受到使用者驗證的保護。 Windows 使用 Windows Hello 的新驗證技術來支援其生物特徵辨識登入選項。