這是完整攻略的第二部分,說明如何在包裝的 Windows 應用程式中,使用 Windows Hello 作為傳統使用者名稱與密碼驗證系統的替代方案。 本文接續第一部分 Windows Hello 登入應用程式 的部分,並擴充功能,示範如何將Windows Hello整合進現有應用程式。
要建立這個 project,你需要一些 C# 和 XAML 的經驗。 你還需要在 Windows 10 或 Windows 11 電腦上使用 Visual Studio 2022。 如需設定開發環境的完整指示,請參閱 開始開發 Windows 應用程式 。
練習 1:伺服器端邏輯
在這個練習中,你將從第一個實驗室建置的 Windows Hello 應用程式開始,並建立本地的模擬伺服器和資料庫。 這個實作實驗室旨在教導如何將 Windows Hello 整合進現有系統。 藉由使用模擬伺服器和模擬資料庫,會消除許多不相關的設定。 在您自己的應用程式中,您必須將模擬物件取代為實際的服務和資料庫。
首先,打開第一個 Windows Hello Hands On Lab 中的 WindowsHelloLogin 解決方案。
您一開始會實作模擬伺服器和模擬資料庫。 建立名為 「AuthService」 的新資料夾。 在Solution Explorer中,右鍵點擊 WindowsHelloLogin project,選擇 Add>New Folder。
建立 UserAccount 和 WindowsHelloDevices 類別,以作為要儲存在模擬資料庫中之數據的模型。 UserAccount 會類似於在傳統驗證伺服器上實作的使用者模型。 以滑鼠右鍵按兩下 AuthService 資料夾,然後新增名為 「UserAccount」 的新類別。
建立 Windows Hello 授權資料夾的截圖
將類別範圍變更為 public,併為 UserAccount 類別新增下列公用屬性。 您必須為
System.ComponentModel.DataAnnotations命名空間新增using語句。using System; using System.ComponentModel.DataAnnotations; namespace WindowsHelloLogin.AuthService { public class UserAccount { [Key, Required] public Guid UserId { get; set; } [Required] public string Username { get; set; } public string Password { get; set; } // public List<WindowsHelloDevice> WindowsHelloDevices = new(); } }您可能已經注意到已批注的 WindowsHelloDevices 清單。 這是您必須在目前實作中對現有使用者模型所做的修改。 WindowsHelloDevices 清單會包含一個 deviceID、由 Windows Hello 製作的公鑰,以及一個 KeyCredentialAttestationResult。 在這個練習中,你需要實作 keyAttestationResult,因為這功能只在搭載 TPM(可信平台模組)晶片的裝置上由 Windows Hello 提供。 KeyCredentialAttestationResult 結合了多個屬性,需要拆分,才能透過資料庫儲存和載入它們。
在名為 「WindowsHelloDevice.cs」 的 AuthService 資料夾中建立新的類別。 這就是上述 Windows Hello 裝置的型號。 將類別範圍變更為公用,並新增下列屬性。
using System; namespace WindowsHelloLogin.AuthService { public class WindowsHelloDevice { // These are the new variables that will need to be added to the existing UserAccount in the Database // The DeviceName is used to support multiple devices for the one user. // This way the correct public key is easier to find as a new public key is made for each device. // The KeyAttestationResult is only used if the User device has a TPM (Trusted Platform Module) chip, // in most cases it will not. So will be left out for this hands on lab. public Guid DeviceId { get; set; } public byte[] PublicKey { get; set; } // public KeyCredentialAttestationResult KeyAttestationResult { get; set; } } }回到UserAccount.cs並取消Windows Hello裝置清單的評論。
using System.Collections.Generic; namespace WindowsHelloLogin.AuthService { public class UserAccount { [Key, Required] public Guid UserId { get; set; } [Required] public string Username { get; set; } public string Password { get; set; } public List<WindowsHelloDevice> WindowsHelloDevices = new(); } }建立 UserAccount 和 WindowsHelloDevice 的模型後,您必須在 AuthService 資料夾中建立另一個新類別,以作為模擬資料庫,因為這是模擬資料庫,而您將從本機儲存和載入用戶帳戶清單。 在真實世界中,這會是您的資料庫實作。 在名為 「MockStore.cs」 的 AuthService 資料夾中建立新的類別。 將類別範圍變更為 public。
當模擬存放區會在本機儲存及載入使用者帳戶清單時,您可以實作邏輯,以使用 XmlSerializer 儲存和載入該清單。 您也必須記住檔名和儲存位置。 在 MockStore.cs中實作下列專案:
using System.Collections.Generic; using System; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; using Windows.Storage; namespace WindowsHelloLogin.AuthService { public class MockStore { private const string USER_ACCOUNT_LIST_FILE_NAME = "userAccountsList.txt"; // This cannot be a const because the LocalFolder is accessed at runtime private string _userAccountListPath = Path.Combine( ApplicationData.Current.LocalFolder.Path, USER_ACCOUNT_LIST_FILE_NAME); private List<UserAccount> _mockDatabaseUserAccountsList; #region Save and Load Helpers /// <summary> /// Create and save a useraccount list file. (Replacing the old one) /// </summary> private async Task SaveAccountListAsync() { string accountsXml = SerializeAccountListToXml(); if (File.Exists(_userAccountListPath)) { StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_userAccountListPath); 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> private async Task LoadAccountListAsync() { if (File.Exists(_userAccountListPath)) { StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_userAccountListPath); string accountsXml = await FileIO.ReadTextAsync(accountsFile); DeserializeXmlToAccountList(accountsXml); } // If the UserAccountList does not contain the sampleUser Initialize the sample users // This is only needed as it in a Hand on Lab to demonstrate a user being migrated. // In the real world, user accounts would just be in a database. if (!_mockDatabaseUserAccountsList.Any(f => f.Username.Equals("sampleUsername"))) { //If the list is empty, call InitializeSampleAccounts and return the list //await InitializeSampleUserAccountsAsync(); } } /// <summary> /// Uses the local list of accounts and returns an XML formatted string representing the list /// </summary> /// <returns>XML formatted list of accounts</returns> private string SerializeAccountListToXml() { var xmlizer = new XmlSerializer(typeof(List<UserAccount>)); var writer = new StringWriter(); xmlizer.Serialize(writer, _mockDatabaseUserAccountsList); 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> private List<UserAccount> DeserializeXmlToAccountList(string listAsXml) { var xmlizer = new XmlSerializer(typeof(List<UserAccount>)); TextReader textreader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(listAsXml))); return _mockDatabaseUserAccountsList = (xmlizer.Deserialize(textreader)) as List<UserAccount>; } #endregion } }在 LoadAccountListAsync 方法中,您可能已經注意到 InitializeSampleUserAccountsAsync 方法被註解掉了。您必須在 MockStore.cs 中建立這個方法。 這個方法會填入使用者帳戶清單,以便進行登入。 在真實世界中,用戶資料庫已經填滿。 在此步驟中,您也將建立建構函式,以初始化使用者清單並呼叫 LoadAccountListAsync。
namespace WindowsHelloLogin.AuthService { public class MockStore { private const string USER_ACCOUNT_LIST_FILE_NAME = "userAccountsList.txt"; // This cannot be a const because the LocalFolder is accessed at runtime private string _userAccountListPath = Path.Combine( ApplicationData.Current.LocalFolder.Path, USER_ACCOUNT_LIST_FILE_NAME); private List<UserAccount> _mockDatabaseUserAccountsList; public MockStore() { _mockDatabaseUserAccountsList = new List<UserAccount>(); _ = LoadAccountListAsync(); } private async Task InitializeSampleUserAccountsAsync() { // Create a sample Traditional User Account that only has a Username and Password // This will be used initially to demonstrate how to migrate to use Windows Hello var sampleUserAccount = new UserAccount() { UserId = Guid.NewGuid(), Username = "sampleUsername", Password = "samplePassword", }; // Add the sampleUserAccount to the _mockDatabase _mockDatabaseUserAccountsList.Add(sampleUserAccount); await SaveAccountListAsync(); } } }現在 InitializeSampleUserAccountsAsync 方法已存在,取消 LoadAccountListAsync 方法中的方法呼叫的批注。
private async Task LoadAccountListAsync() { if (File.Exists(_userAccountListPath)) { StorageFile accountsFile = await StorageFile.GetFileFromPathAsync(_userAccountListPath); string accountsXml = await FileIO.ReadTextAsync(accountsFile); DeserializeXmlToAccountList(accountsXml); } // If the UserAccountList does not contain the sampleUser Initialize the sample users // This is only needed as it in a Hand on Lab to demonstrate a user migrating // In the real world user accounts would just be in a database if (!_mockDatabaseUserAccountsList.Any(f = > f.Username.Equals("sampleUsername"))) { //If the list is empty InitializeSampleUserAccountsAsync and return the list await InitializeSampleUserAccountsAsync(); } }模擬存放區中的用戶帳戶清單現在可以儲存和載入。 應用程式的其他部分需要access這個清單,因此需要一些方法來取得這些資料。 在 InitializeSampleUserAccountsAsync 方法底下,新增下列方法來取得數據。 它們會讓你取得使用者ID、單一使用者、特定Windows Hello裝置的使用者清單,並且取得該裝置使用者的公開金鑰。
public Guid GetUserId(string username) { if (_mockDatabaseUserAccountsList.Any()) { UserAccount account = _mockDatabaseUserAccountsList.FirstOrDefault(f => f.Username.Equals(username)); if (account != null) { return account.UserId; } } return Guid.Empty; } public UserAccount GetUserAccount(Guid userId) { return _mockDatabaseUserAccountsList.FirstOrDefault(f => f.UserId.Equals(userId)); } public List<UserAccount> GetUserAccountsForDevice(Guid deviceId) { var usersForDevice = new List<UserAccount>(); foreach (UserAccount account in _mockDatabaseUserAccountsList) { if (account.WindowsHelloDevices.Any(f => f.DeviceId.Equals(deviceId))) { usersForDevice.Add(account); } } return usersForDevice; } public byte[] GetPublicKey(Guid userId, Guid deviceId) { UserAccount account = _mockDatabaseUserAccountsList.FirstOrDefault(f => f.UserId.Equals(userId)); if (account != null) { if (account.WindowsHelloDevices.Any()) { return account.WindowsHelloDevices.FirstOrDefault(p => p.DeviceId.Equals(deviceId)).PublicKey; } } return null; }要實作的下一個方法會處理簡單的作業,以新增帳戶、移除帳戶,以及移除裝置。 移除裝置是必要的,因為 Windows Hello 是針對特定裝置設計的。 每台你登入的裝置,Windows Hello 都會建立一組新的公鑰與私鑰對。 就像針對您登入的每個裝置使用不同的密碼,唯一的一件事就是您不需要記住所有這些密碼;伺服器會執行此工作。 將下列方法新增至MockStore.cs。
public async Task<UserAccount> AddAccountAsync(string username) { UserAccount newAccount = null; try { newAccount = new UserAccount() { UserId = Guid.NewGuid(), Username = username, }; _mockDatabaseUserAccountsList.Add(newAccount); await SaveAccountListAsync(); } catch (Exception) { throw; } return newAccount; } public async Task<bool> RemoveAccountAsync(Guid userId) { UserAccount userAccount = GetUserAccount(userId); if (userAccount != null) { _mockDatabaseUserAccountsList.Remove(userAccount); await SaveAccountListAsync(); return true; } return false; } public async Task<bool> RemoveDeviceAsync(Guid userId, Guid deviceId) { UserAccount userAccount = GetUserAccount(userId); WindowsHelloDevice deviceToRemove = null; if (userAccount != null) { foreach (WindowsHelloDevice device in userAccount.WindowsHelloDevices) { if (device.DeviceId.Equals(deviceId)) { deviceToRemove = device; break; } } } if (deviceToRemove != null) { //Remove the WindowsHelloDevice userAccount.WindowsHelloDevices.Remove(deviceToRemove); await SaveAccountListAsync(); } return true; }在 MockStore 類別中,新增一個方法,將Windows Hello相關資訊加入現有的 UserAccount。 此方法稱為「WindowsHelloUpdateDetailsAsync」,會取得參數來識別使用者及 Windows Hello 的詳細資訊。 在真實世界的應用程式中建立 WindowsHelloDevice 時,KeyAttestationResult 已被註解掉,但在實際應用中是需要的。
using System.Threading.Tasks; using Windows.Security.Credentials; public async Task WindowsHelloUpdateDetailsAsync(Guid userId, Guid deviceId, byte[] publicKey, KeyCredentialAttestationResult keyAttestationResult) { UserAccount existingUserAccount = GetUserAccount(userId); if (existingUserAccount != null) { if (!existingUserAccount.WindowsHelloDevices.Any(f => f.DeviceId.Equals(deviceId))) { existingUserAccount.WindowsHelloDevices.Add(new WindowsHelloDevice() { DeviceId = deviceId, PublicKey = publicKey, // KeyAttestationResult = keyAttestationResult }); } } await SaveAccountListAsync(); }MockStore 類別現在已完成,因為這代表資料庫應該視為私用。 為了access MockStore,需要一個 AuthService類別來操作資料庫資料。 在 AuthService 資料夾中,建立名為 「AuthService.cs」 的新類別。 將類別範圍變更為公用,並新增單一實例模式,以確保只會建立一個實例。
namespace WindowsHelloLogin.AuthService { public class AuthService { // Singleton instance of the AuthService // The AuthService is a mock of what a real world server and service implementation would be private static AuthService _instance; public static AuthService Instance { get { if (null == _instance) { _instance = new AuthService(); } return _instance; } } private AuthService() { } } }AuthService 類別需要建立 MockStore 類別的一個實例,並提供對 MockStore 物件屬性的存取。
namespace WindowsHelloLogin.AuthService { public class AuthService { //Singleton instance of the AuthService //The AuthService is a mock of what a real world server and database implementation would be private static AuthService _instance; public static AuthService Instance { get { if (null == _instance) { _instance = new AuthService(); } return _instance; } } private AuthService() { } private MockStore _mockStore = new(); public Guid GetUserId(string username) { return _mockStore.GetUserId(username); } public UserAccount GetUserAccount(Guid userId) { return _mockStore.GetUserAccount(userId); } public List<UserAccount> GetUserAccountsForDevice(Guid deviceId) { return _mockStore.GetUserAccountsForDevice(deviceId); } } }你需要在 AuthService 類別中設置方法,以便訪問 MockStore 物件中用於新增、移除和更新 Windows Hello 詳細資料的方法。 在 AuthService 類別定義的結尾,新增下列方法。
using System.Threading.Tasks; using Windows.Security.Credentials; public async Task RegisterAsync(string username) { await _mockStore.AddAccountAsync(username); } public async Task<bool> WindowsHelloRemoveUserAsync(Guid userId) { return await _mockStore.RemoveAccountAsync(userId); } public async Task<bool> WindowsHelloRemoveDeviceAsync(Guid userId, Guid deviceId) { return await _mockStore.RemoveDeviceAsync(userId, deviceId); } public async Task WindowsHelloUpdateDetailsAsync(Guid userId, Guid deviceId, byte[] publicKey, KeyCredentialAttestationResult keyAttestationResult) { await _mockStore.WindowsHelloUpdateDetailsAsync(userId, deviceId, publicKey, keyAttestationResult); }AuthService 類別必須提供方法來驗證認證。 此方法會採用使用者名稱和密碼,並確定帳戶存在且密碼有效。 現有的系統會有與這個相等的方法,可檢查使用者是否已獲得授權。 將下列 ValidateCredentials 方法新增至AuthService.cs檔案。
public bool ValidateCredentials(string username, string password) { if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) { // This would be used for existing accounts migrating to use Windows Hello Guid userId = GetUserId(username); if (userId != Guid.Empty) { UserAccount account = GetUserAccount(userId); if (account != null) { if (string.Equals(password, account.Password)) { return true; } } } } return false; }AuthService 類別需要要求挑戰方法,以將挑戰傳回給用戶端,以驗證使用者是否為他們聲稱的身分。 然後需要在 AuthService 類別中加入另一種方法,以接收來自用戶端的簽署過的挑戰。 針對這個實作課程,用來判斷已簽署的挑戰是否完成的方法尚未提供。 每次將 Windows Hello 實作到現有的認證系統中都會略有不同。 儲存在伺服器上的公鑰必須與用戶端傳回伺服器的結果相符。 將這兩種方法新增至 AuthService.cs。
using Windows.Security.Cryptography; using Windows.Storage.Streams; public IBuffer WindowsHelloRequestChallenge() { return CryptographicBuffer.ConvertStringToBinary("ServerChallenge", BinaryStringEncoding.Utf8); } public bool SendServerSignedChallenge(Guid userId, Guid deviceId, byte[] signedChallenge) { // Depending on your company polices and procedures this step will be different // It is at this point you will need to validate the signedChallenge that is sent back from the client. // Validation is used to ensure the correct user is trying to access this account. // The validation process will use the signedChallenge and the stored PublicKey // for the username and the specific device signin is called from. // Based on the validation result you will return a bool value to allow access to continue or to block the account. // For this sample validation will not happen as a best practice solution does not apply and will need to // be configured for each company. // Simply just return true. // You could get the User's Public Key with something similar to the following: byte[] userPublicKey = _mockStore.GetPublicKey(userId, deviceId); return true; }
練習 2:客戶端邏輯
在此練習中,您將把第一個實驗室的客戶端視圖和輔助類別變更為使用 AuthService 類別。 在真實世界中, AuthService 會是驗證伺服器,而您必須使用 Web API 從伺服器傳送和接收數據。 在這個實際操作實驗中,為了簡化操作,用戶端和伺服器都是設置為本地。 目標是學會如何使用 Windows Hello API。
在MainPage.xaml.cs中,您可以在載入的方法中移除 AccountHelper.LoadAccountListAsync 方法呼叫,因為 AuthService 類別會建立 MockStore 的實例以載入帳戶清單。 方法
Loaded現在看起來應該像下面的代碼段。 請注意,因為沒有等待任何操作,異步方法的定義已被移除。private void MainPage_Loaded(object sender, RoutedEventArgs e) { Frame.Navigate(typeof(UserSelection)); }更新 [登入 ] 頁面介面,以要求輸入密碼。 這個實作實驗室示範如何將現有系統遷移至使用 Windows Hello,現有帳號將擁有使用者名稱和密碼。 同時更新 XAML 底部的說明,以包含預設密碼。 在 Login.xaml 中更新下列 XAML。
<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 credentials below" Margin="0,0,0,20" TextWrapping="Wrap" Width="300" TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <!-- Username Input --> <TextBlock x:Name="UserNameTextBlock" Text="Username: " FontSize="20" Margin="4" Width="100"/> <TextBox x:Name="UsernameTextBox" PlaceholderText="sampleUsername" Width="200" Margin="4"/> </StackPanel> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <!-- Password Input --> <TextBlock x:Name="PasswordTextBlock" Text="Password: " FontSize="20" Margin="4" Width="100"/> <PasswordBox x:Name="PasswordBox" PlaceholderText="samplePassword" Width="200" Margin="4"/> </StackPanel> <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' and default password 'samplePassword'"/> </StackPanel> </Grid>在 Login 類別的程式代碼後置檔案中,您必須將類別頂端的私用變數變更
Account為UserAccount。 將OnNavigateTo事件變更為將型別轉換成UserAccount的形式。 您還需要以下的 using 語句。using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class Login : Page { private UserAccount _account; private bool _isExistingAccount; public Login() { this.InitializeComponent(); } protected override async void OnNavigatedTo(NavigationEventArgs e) { //Check Windows Hello is setup 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 = (UserAccount)e.Parameter; UsernameTextBox.Text = _account.Username; await SignInWindowsHelloAsync(); } } } private async void LoginButton_Click(object sender, RoutedEventArgs e) { ErrorMessage.Text = ""; await SignInWindowsHelloAsync(); } } }由於登入頁面使用
UserAccount物件而非Account物件,因此WindowsHelloHelper.cs需要更新為某些方法的參數類型使用UserAccount。 您必須變更 CreateWindowsHelloKeyAsync、RemoveWindowsHelloAccountAsync 和 GetWindowsHelloAuthenticationMessageAsync 方法的下列參數。 由於UserAccount類別包含Guid的 UserId,因此您將在更多地方開始使用該識別碼,以提高準確性。public static async Task<bool> CreateWindowsHelloKeyAsync(Guid userId, string username) { KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(username, KeyCredentialCreationOption.ReplaceExisting); return true; } public static async Task RemoveWindowsHelloAccountAsync(UserAccount account) { } public static async Task<bool> GetWindowsHelloAuthenticationMessageAsync(UserAccount 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.UserId, account.Username)) { //If the Windows Hello Key was again successfully created, Windows Hello has just been reset. //Now that the Windows 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 方法必須更新,才能使用 AuthService ,而不是 AccountHelper。 驗證認證將會透過 AuthService 進行。 對於這個實際作實驗室,唯一設定的帳戶是 “sampleUsername”。 此帳戶是在 MockStore.cs 的 InitializeSampleUserAccountsAsync 方法中建立。 更新 Login.xaml.cs 中的 SignInWindowsHelloAsync 方法,以反映下列代碼段。
private async Task SignInWindowsHelloAsync() { if (_isExistingAccount) { if (await WindowsHelloHelper.GetWindowsHelloAuthenticationMessageAsync(_account)) { Frame.Navigate(typeof(Welcome), _account); } } else if (AuthService.AuthService.Instance.ValidateCredentials(UsernameTextBox.Text, PasswordBox.Password)) { Guid userId = AuthService.AuthService.Instance.GetUserId(UsernameTextBox.Text); if (userId != Guid.Empty) { //Now that the account exists on server try and create the necessary details and add them to the account if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(userId, UsernameTextBox.Text)) { Debug.WriteLine("Successfully signed in with Windows Hello!"); //Navigate to the Welcome Screen. _account = AuthService.AuthService.Instance.GetUserAccount(userId); Frame.Navigate(typeof(Welcome), _account); } else { //The Windows Hello account creation failed. //Remove the account from the server as the details were not configured await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(userId); ErrorMessage.Text = "Account Creation Failed"; } } } else { ErrorMessage.Text = "Invalid Credentials"; } }由於Windows Hello會在每個裝置上為每個帳號建立不同的公鑰與私鑰配對,Welcome頁面需要顯示已登入帳號的註冊裝置清單,並允許每一個裝置被遺忘。 請在 Welcome.xaml 中的
ForgetButton下方添加以下 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"/> <Button x:Name="ForgetDeviceButton" Content="Forget Device" Click="Button_Forget_Device_Click" Foreground="White" Background="Gray" Margin="0,40,0,20" HorizontalAlignment="Center"/> <TextBlock x:Name="ForgetDeviceErrorTextBlock" Text="Select a device first" TextWrapping="Wrap" Width="300" Foreground="Red" TextAlignment="Center" VerticalAlignment="Center" FontSize="16" Visibility="Collapsed"/> <ListView x:Name="UserListView" MaxHeight="500" MinWidth="350" Width="350" HorizontalAlignment="Center"> <ListView.ItemTemplate> <DataTemplate> <Grid Background="Gray" Height="50" Width="350" HorizontalAlignment="Center" VerticalAlignment="Stretch" > <TextBlock Text="{Binding DeviceId}" HorizontalAlignment="Center" TextAlignment="Center" VerticalAlignment="Center" Foreground="White"/> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackPanel> </Grid>在Welcome.xaml.cs檔案中,您必須將 類別頂端的私
Account用變數變更為私用UserAccount變數。 然後更新OnNavigatedTo方法以使用 AuthService 並擷取目前帳戶的資訊。 當您有帳戶資訊時,您可以設定ItemsSource清單以顯示裝置。 您必須新增 AuthService 命名空間的參考。using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class Welcome : Page { private UserAccount _activeAccount; public Welcome() { InitializeComponent(); } protected override void OnNavigatedTo(NavigationEventArgs e) { _activeAccount = (UserAccount)e.Parameter; if (_activeAccount != null) { UserAccount account = AuthService.AuthService.Instance.GetUserAccount(_activeAccount.UserId); if (account != null) { UserListView.ItemsSource = account.WindowsHelloDevices; UserNameText.Text = account.Username; } } } } }當您移除帳戶時將使用 AuthService,因此可以在 方法中移除對
Button_Forget_User_Click的參考。 方法現在應該如下所示。private async void Button_Forget_User_Click(object sender, RoutedEventArgs e) { //Remove it from Windows Hello await WindowsHelloHelper.RemoveWindowsHelloAccountAsync(_activeAccount); Debug.WriteLine($"User {_activeAccount.Username} deleted."); //Navigate back to UserSelection page. Frame.Navigate(typeof(UserSelection)); }WindowsHelloHelper 方法未使用 AuthService 來移除帳戶。 您必須呼叫 AuthService 並傳遞 userId。
public static async Task RemoveWindowsHelloAccountAsync(UserAccount 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 await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(account.UserId); } //Then delete the account from the machines list of Windows Hello Accounts await KeyCredentialManager.DeleteAsync(account.Username); }在完成實作 歡迎 頁面之前,您必須在 WindowsHelloHelper.cs 中建立方法,以允許移除裝置。 建立新的方法,以在 AuthService 中呼叫 WindowsHelloRemoveDeviceAsync。
public static async Task RemoveWindowsHelloDeviceAsync(UserAccount account, Guid deviceId) { await AuthService.AuthService.Instance.WindowsHelloRemoveDeviceAsync(account.UserId, deviceId); }在 Welcome.xaml.cs 中,實作 Button_Forget_Device_Click 事件處理程式。 這會使用從裝置清單中選定的裝置,並使用 Windows Hello 助手來呼叫移除裝置。 請記得將事件處理程式設為異步。
private async void Button_Forget_Device_Click(object sender, RoutedEventArgs e) { WindowsHelloDevice selectedDevice = UserListView.SelectedItem as WindowsHelloDevice; if (selectedDevice != null) { //Remove it from Windows Hello await WindowsHelloHelper.RemoveWindowsHelloDeviceAsync(_activeAccount, selectedDevice.DeviceId); Debug.WriteLine($"User {_activeAccount.Username} deleted."); if (!UserListView.Items.Any()) { //Navigate back to UserSelection page. Frame.Navigate(typeof(UserSelection)); } } else { ForgetDeviceErrorTextBlock.Visibility = Visibility.Visible; } }您要更新的下一個頁面是 UserSelection 頁面。 UserSelection 頁面必須使用 AuthService 來擷取目前裝置的所有用戶帳戶。 目前,您無法取得裝置標識碼來傳遞至 AuthService ,以便傳回該裝置的用戶帳戶。 在 Utils 資料夾中,建立名為 「Helpers.cs」 的新類別。 將類別範圍變更為公用靜態,然後新增下列方法,讓您擷取目前的裝置標識碼。
using System; using Windows.Security.ExchangeActiveSyncProvisioning; namespace WindowsHelloLogin.Utils { public static class Helpers { public static Guid GetDeviceId() { //Get the Device ID to pass to the server var deviceInformation = new EasClientDeviceInformation(); return deviceInformation.Id; } } }在 UserSelection 頁面類別中,只有程式代碼後置需要變更,而不是使用者介面。 在 UserSelection.xaml.cs中,更新 UserSelection_Loaded 方法和 UserSelectionChanged 方法,以使用
UserAccount類別,而不是Account類別。 您還需要透過 AuthService 取得此裝置的所有使用者。using System.Linq; using WindowsHelloLogin.AuthService; namespace WindowsHelloLogin.Views { public sealed partial class UserSelection : Page { public UserSelection() { InitializeComponent(); Loaded += UserSelection_Loaded; } private void UserSelection_Loaded(object sender, RoutedEventArgs e) { List<UserAccount> accounts = AuthService.AuthService.Instance.GetUserAccountsForDevice(Helpers.GetDeviceId()); if (accounts.Any()) { UserListView.ItemsSource = accounts; UserListView.SelectionChanged += UserSelectionChanged; } else { //If there are no accounts navigate to the Login page Frame.Navigate(typeof(Login)); } } /// <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) { UserAccount account = (UserAccount)((ListView)sender).SelectedValue; if (account != null) { Debug.WriteLine($"Account {account.Username} selected!"); } Frame.Navigate(typeof(Login), account); } } } }WindowsHelloRegister 頁面必須更新程序代碼後置檔案。 使用者介面不需要任何變更。 在 WindowsHelloRegister.xaml.cs中,移除 類別頂端的私
Account用變數,因為不再需要它。 更新 RegisterButton_Click_Async 事件處理程式以使用 AuthService。 此方法會建立新的 UserAccount ,然後嘗試更新其帳戶詳細數據。 如果 Windows Hello 未能建立金鑰,該帳號會因註冊失敗而被移除。private async void RegisterButton_Click_Async(object sender, RoutedEventArgs e) { ErrorMessage.Text = ""; //Validate entered credentials are acceptable if (!string.IsNullOrEmpty(UsernameTextBox.Text)) { //Register an Account on the AuthService so that we can get back a userId await AuthService.AuthService.Instance.RegisterAsync(UsernameTextBox.Text); Guid userId = AuthService.AuthService.Instance.GetUserId(UsernameTextBox.Text); if (userId != Guid.Empty) { //Now that the account exists on server try and create the necessary details and add them to the account if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(userId, UsernameTextBox.Text)) { //Navigate to the Welcome Screen. Frame.Navigate(typeof(Welcome), AuthService.AuthService.Instance.GetUserAccount(userId)); } else { //The Windows Hello account creation failed. //Remove the account from the server as the details were not configured await AuthService.AuthService.Instance.WindowsHelloRemoveUserAsync(userId); ErrorMessage.Text = "Account Creation Failed"; } } } else { ErrorMessage.Text = "Please enter a username"; } }建置並執行應用程式。 使用認證 「sampleUsername」 和 「samplePassword」 登入範例用戶帳戶。 在歡迎畫面上,您可能會注意到雖然顯示了 [忘記裝置] 按鈕,但沒有任何裝置。 當你建立或遷移使用者到Windows Hello時,帳號資訊不會被推送到AuthService。
Windows Hello登入畫面截圖
要將Windows Hello帳號資訊傳送到 AuthService,WindowsHelloHelper.cs 需要更新。 在CreateWindowsHelloKeyAsync方法中,您不僅要在成功的情況下傳回
true,而是需要呼叫一個新的方法,該方法將嘗試獲取KeyAttestation。 即使這個實作實驗課程沒有在 AuthService 中記錄相關資訊,您將會了解到如何在用戶端取得這些資訊。 更新 CreateWindowsHelloKeyAsync 方法,如下所示:public static async Task<bool> CreateWindowsHelloKeyAsync(Guid userId, string username) { KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(username, KeyCredentialCreationOption.ReplaceExisting); switch (keyCreationResult.Status) { case KeyCredentialStatus.Success: Debug.WriteLine("Successfully made key"); await GetKeyAttestationAsync(userId, keyCreationResult); return true; case KeyCredentialStatus.UserCanceled: Debug.WriteLine("User cancelled sign-in process."); break; case KeyCredentialStatus.NotFound: // User needs to setup Windows Hello Debug.WriteLine($"Windows Hello is not set up!{Environment.NewLine}Please go to Windows Settings and set up a PIN to use it."); break; default: break; } return false; }在 WindowsHelloHelper.cs 中建立 GetKeyAttestationAsync 方法。 此方法將示範如何取得 Windows Hello 在特定裝置上每個帳號所能提供的所有必要資訊。
using Windows.Storage.Streams; private static async Task GetKeyAttestationAsync(Guid userId, KeyCredentialRetrievalResult keyCreationResult) { KeyCredential userKey = keyCreationResult.Credential; IBuffer publicKey = userKey.RetrievePublicKey(); KeyCredentialAttestationResult keyAttestationResult = await userKey.GetAttestationAsync(); IBuffer keyAttestation = null; IBuffer certificateChain = null; bool keyAttestationIncluded = false; bool keyAttestationCanBeRetrievedLater = false; KeyCredentialAttestationStatus keyAttestationRetryType = 0; if (keyAttestationResult.Status == KeyCredentialAttestationStatus.Success) { keyAttestationIncluded = true; keyAttestation = keyAttestationResult.AttestationBuffer; certificateChain = keyAttestationResult.CertificateChainBuffer; Debug.WriteLine("Successfully made key and attestation"); } else if (keyAttestationResult.Status == KeyCredentialAttestationStatus.TemporaryFailure) { keyAttestationRetryType = KeyCredentialAttestationStatus.TemporaryFailure; keyAttestationCanBeRetrievedLater = true; Debug.WriteLine("Successfully made key but not attestation"); } else if (keyAttestationResult.Status == KeyCredentialAttestationStatus.NotSupported) { keyAttestationRetryType = KeyCredentialAttestationStatus.NotSupported; keyAttestationCanBeRetrievedLater = false; Debug.WriteLine("Key created, but key attestation not supported"); } Guid deviceId = Helpers.GetDeviceId(); //Update the Windows Hello details with the information we have just fetched above. //await UpdateWindowsHelloDetailsAsync(userId, deviceId, publicKey.ToArray(), keyAttestationResult); }你可能注意到在 GetKeyAttestationAsync 方法中,你剛剛新增的最後一行被註解掉了。這最後一行會是你建立的新方法,會把所有Windows Hello資訊傳送到 AuthService。 在真實世界中,您必須透過 Web API 將此內容傳送至實際伺服器。
using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; public static async Task<bool> UpdateWindowsHelloDetailsAsync(Guid userId, Guid deviceId, byte[] publicKey, KeyCredentialAttestationResult keyAttestationResult) { //In the real world, you would use an API to add Windows Hello signing info to server for the signed in account. //For this tutorial, we do not implement a Web API for our server and simply mock the server locally. //The CreateWindowsHelloKey method handles adding the Windows Hello account locally to the device using the KeyCredential Manager //Using the userId the existing account should be found and updated. await AuthService.AuthService.Instance.WindowsHelloUpdateDetailsAsync(userId, deviceId, publicKey, keyAttestationResult); return true; }在 GetKeyAttestationAsync 方法中取消註解最後一行,讓Windows Hello資訊傳送到 AuthService。
建置並執行應用程式,並使用預設認證登入,如同先前一樣。 在 [ 歡迎使用 ] 頁面上,您現在會看到裝置標識碼已顯示。 如果您在另一個裝置上登入,那麼該裝置也會顯示在此處(如果您使用雲端託管的驗證服務)。 針對這個動手實驗室,將顯示裝置ID。 在實際的實作中,您會想要顯示易記名稱,讓人員瞭解並使用 來識別每個裝置。
為了完成這個實作實驗室,當您從用戶選擇頁面中選擇並重新登入時,需要執行對使用者的請求和挑戰。 AuthService 有您設計用來發出挑戰請求的兩個方法,其中一個是使用已簽署挑戰的方式。 在 WindowsHelloHelper.cs中,建立名為 RequestSignAsync 的新方法。 這會向 AuthService 請求挑戰,並用 Windows Hello API 本地簽署該挑戰,並將簽署的挑戰傳送至 AuthService。 在此實作實驗課中,AuthService 將會收到已簽署的挑戰並傳回
true。 在實際的實作中,您必須實作驗證機制,以判斷挑戰是否已由正確裝置上的正確用戶簽署。 將下列方法新增至WindowsHelloHelper.csprivate static async Task<bool> RequestSignAsync(Guid userId, KeyCredentialRetrievalResult openKeyResult) { // Calling userKey.RequestSignAsync() prompts the uses to enter the PIN or use Biometrics (Windows Hello). // The app would use the private key from the user account to sign the sign-in request (challenge) // The client would then send it back to the server and await the servers response. IBuffer challengeMessage = AuthService.AuthService.Instance.WindowsHelloRequestChallenge(); KeyCredential userKey = openKeyResult.Credential; KeyCredentialOperationResult signResult = await userKey.RequestSignAsync(challengeMessage); if (signResult.Status == KeyCredentialStatus.Success) { // If the challenge from the server is signed successfully // send the signed challenge back to the server and await the servers response return AuthService.AuthService.Instance.SendServerSignedChallenge( userId, Helpers.GetDeviceId(), signResult.Result.ToArray()); } else if (signResult.Status == KeyCredentialStatus.UserCanceled) { // User cancelled the Windows Hello PIN entry. } else if (signResult.Status == KeyCredentialStatus.NotFound) { // Must recreate Windows Hello key } else if (signResult.Status == KeyCredentialStatus.SecurityDeviceLocked) { // Can't use Windows Hello right now, remember that hardware failed and suggest restart } else if (signResult.Status == KeyCredentialStatus.UnknownError) { // Can't use Windows Hello right now, try again later } return false; }在 WindowsHelloHelper 類別中,從 GetWindowsHelloAuthenticationMessageAsync 方法呼叫 RequestSignAsync 方法。
public static async Task<bool> GetWindowsHelloAuthenticationMessageAsync(UserAccount 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. return await RequestSignAsync(account.UserId, openKeyResult); } 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.UserId, account.Username)) { //If the Windows Hello Key was again successfully created, Windows Hello has just been reset. //Now that the Windows 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; }在此練習中,您已更新用戶端應用程式以使用 AuthService。 如此一來,您就能夠消除 Account 類別和 AccountHelper 類別的需求。 刪除 Utils 資料夾中的 Account 類別、Models 資料夾和 AccountHelper 類別。 在解決方案成功建置之前,您必須移除整個應用程式中所有對
WindowsHelloLogin.Models命名空間的參考。建立並執行應用程式,享受使用 Windows Hello 與模擬服務與資料庫的樂趣。
在這個實作實驗室中,你學會了如何使用 Windows Hello API 來取代在 Windows 機器上使用驗證時的密碼需求。 當你考慮到人們維護密碼和支援現有系統遺失密碼所花費的精力時,你應該能理解這套新的 Windows Hello 認證系統帶來的好處。
我們將如何在服務和伺服器端實作驗證的詳情留給您處理。 預計大多數開發者會有現有系統需要遷移,才能開始使用 Windows Hello。 每個系統的詳細數據將會有所不同。