创建 Windows Hello 登录应用Create a Windows Hello login app

这是有关如何创建 Windows 10 UWP(通用 Windows 平台)应用的完整演练中的第 1 部分,将使用 Windows Hello 作为传统用户名和密码身份验证系统的替代项。This is Part 1 of a complete walkthrough on how to create a Windows 10 UWP (Universal Windows Platform) app that uses Windows Hello as an alternative to traditional username and password authentication systems. 该应用将用户名用于登录并为每个帐户创建 Hello 密钥。The app uses a username for sign-in and create a Hello Key for each account. 这些帐户在配置 Windows Hello 时将受在 Windows 设置中设置的 PIN 的保护。These accounts will be protected by the PIN that is setup in Windows Settings on configuration of Windows Hello.

本演练分为两个部分:生成应用和连接后端服务。This walkthrough is split into two parts: building the app and connecting the backend service. 完成本文后,请继续学习第2部分:Windows Hello 登录服务When you're finished with this article, continue on to Part 2: Windows Hello login service.

在开始之前,你应阅读 Windows Hello 概述,以便大致了解 Windows Hello 的工作原理。Before you begin, you should read the Windows Hello overview for a general understanding of how Windows Hello works.

立即开始行动Get started

为了生成此项目,你需要具有 C# 和 XAML 方面的一些经验。In order to build this project, you'll need some experience with C#, and XAML. 还需要在 Windows 10 计算机上使用 Visual Studio 2015 (社区版或更高版本)或 Visual Studio 的更高版本。You'll also need to be using Visual Studio 2015 (Community Edition or greater), or a later release of Visual Studio, on a Windows 10 machine. 尽管 Visual Studio 2015 是所需的最低版本,但我们建议使用最新版本的 Visual Studio 来实现最新的开发人员和安全更新。While Visual Studio 2015 is the minimum required version, we recommend that you use the latest version of Visual Studio for the latest developer and security updates.

  • 打开 Visual Studio,然后选择 "文件" > 新建 > 项目 "。Open Visual Studio and select File > New > Project.
  • 这将打开一个“新建项目”窗口。This will open a “New Project” window. 导航到“模板”>“Visual C#”。Navigation to Templates > Visual C#.
  • 选择“空白应用(通用 Windows)”并将你的应用程序命名为“PassportLogin”。Choose Blank App (Universal Windows) and name your application "PassportLogin".
  • 生成并运行新的应用程序 (F5),然后你应该看到屏幕上显示的空白窗口。Build and Run the new application (F5), you should see a blank window shown on the screen. 关闭该应用程序。Close the application.

Windows Hello 新项目

练习1:Microsoft Passport 登录Exercise 1: Login with Microsoft Passport

在本练习中,你将了解如何检查 Windows Hello 是否在计算机上设置,以及如何使用 Windows Hello 登录到某一帐户。In this exercise you will learn how to check if Windows Hello is setup on the machine, and how to sign into an account using Windows Hello.

  • 在新项目中,在解决方案中创建一个名为“Views”的新文件夹。In the new project create a new folder in the solution called "Views". 在本示例中,此文件夹包含将导航到的页面。This folder will contain the pages that will be navigated to in this sample. 在解决方案资源管理器中右键单击该项目、依次选择“添加”>“新文件夹”,然后将该文件夹重命名为 Views。Right click on the project in solution explorer, select Add > New Folder, then rename the folder to Views.

    Windows Hello“添加”文件夹

  • 右键单击新的 Views 文件夹、依次选择“添加”>“新项目”,然后选择“空白页”。Right click on the new Views folder, select Add > New Item and select Blank Page. 将此页面命名为“Login.xaml”。Name this page "Login.xaml".

    Windows Hello 添加空白页

  • 若要为新的登录页定义用户界面,请添加以下 XAML。To define the user interface for the new login page, add the following XAML. 此 XAML 定义 StackPanel 以便对齐以下子元素:This XAML defines a StackPanel to align the following children:

    • 将包含标题的 TextBlock。TextBlock that will contain a title.
    • 用于错误消息的 TextBlock。TextBlock for error messages.
    • 用于要输入的用户名的文本框。TextBox for the username to input.
    • 用于导航至注册页的按钮。Button to navigate to a register page.
    • 包含 Windows Hello 状态的 TextBlock。TextBlock to contain the status of Windows Hello.
    • 用于解释登录页的 TextBlock(当没有任何后端或已配置的用户时)。TextBlock to explain the Login page as there is no backend or configured users.
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <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="PassportSignInButton" Content="Login" Background="DodgerBlue" Foreground="White"
            Click="PassportSignInButton_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="PassportStatus" Background="#22B14C"
                   Margin="0,20" Height="100" >
          <TextBlock x:Name="PassportStatusText" Text="Microsoft Passport is ready to use!"
                 Margin="4" TextAlignment="Center" VerticalAlignment="Center" FontSize="20"/>
        </Border>
        <TextBlock x:Name="LoginExplaination" FontSize="24" TextAlignment="Center" TextWrapping="Wrap" 
            Text="Please Note: To demonstrate a login, validation will only occur using the default username 'sampleUsername'"/>
      </StackPanel>
    </Grid>
    
  • 需要将几个方法添加到代码隐藏,以便生成解决方案。A few methods need to be added to the code behind to get the solution building. 按 F7,或者使用“解决方案资源管理器”来访问 Login.xaml.cs。Either press F7 or use the Solution Explorer to get to the Login.xaml.cs. 添加以下两个事件方法,以处理登录和注册事件。Add in the following two event methods to handle the Login and Register events. 现在,这些方法将 ErrorMessage.Text 设置为空字符串。For now these methods will set the ErrorMessage.Text to an empty string.

    namespace PassportLogin.Views
    {
        public sealed partial class Login : Page
        {
            public Login()
            {
                this.InitializeComponent();
            }
    
            private void PassportSignInButton_Click(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
            private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
        }
    }
    
  • 若要呈现登录页,可在加载 MainPage 时编辑 MainPage 代码以导航到登录页。In order to render the Login page, edit the MainPage code to navigate to the Login page when the MainPage is loaded. 打开 MainPage.xaml.cs 文件。Open the MainPage.xaml.cs file. 在“解决方案资源管理器”中,双击 MainPage.xaml.cs。In the solution explorer double click on MainPage.xaml.cs. 如果你找不到此文件,请单击 MainPage.xaml 旁边的小箭头以显示代码隐藏。If you can’t find this click the little arrow next to MainPage.xaml to show the code behind. 创建将导航到登录页的已加载事件处理程序方法。Create a loaded event handler method that will navigate to the login page. 你将需要添加对 Views 命名空间的引用。You will need to add a reference to the Views namespace.

    using PassportLogin.Views;
    
    namespace PassportLogin
    {
        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 在此计算机上是否可用。In the Login page you need to handle the OnNavigatedTo event to validate if Windows Hello is available on this machine. 在 Login.xaml.cs 中,实现以下项。In Login.xaml.cs implement the following. 你会注意到 MicrosoftPassportHelper 对象标记一个错误。You will notice that the MicrosoftPassportHelper object flags an error. 这是因为我们尚未实现它。This is because we have not implement it yet.

    public sealed partial class Login : Page
    {
        public Login()
        {
            this.InitializeComponent();
        }
    
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            // Check Microsoft Passport is setup and available on this machine
            if (await MicrosoftPassportHelper.MicrosoftPassportAvailableCheckAsync())
            {
            }
            else
            {
                // Microsoft Passport is not setup so inform the user
                PassportStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                PassportStatusText.Text = "Microsoft Passport is not setup!\n" + 
                    "Please go to Windows Settings and set up a PIN to use it.";
                PassportSignInButton.IsEnabled = false;
            }
        }
    }
    
  • 若要创建 MicrosoftPassportHelper 类,请右键单击解决方案 PassportLogin(通用 Windows),然后依次单击“添加”>“新文件夹”。To create the MicrosoftPassportHelper class, right click on the solution PassportLogin (Universal Windows) and click Add > New Folder. 将此文件夹命名为 Utils。Name this folder Utils.

    passport 创建帮助程序类

  • 右键单击 Utils 文件夹,然后依次单击“添加”>“类”。Right click on the Utils folder and click Add > Class. 将此类命名为“MicrosoftPassportHelper.cs”。Name this class "MicrosoftPassportHelper.cs".

  • 将 MicrosoftPassportHelper 的类定义更改为公共静态,然后添加以下方法以通知用户 Windows Hello 是否可供使用。Change the class definition of MicrosoftPassportHelper to public static, then add the following method that to inform the user if Windows Hello is ready to be used or not. 你将需要添加所需的命名空间。You will need to add the required namespaces.

    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using Windows.Security.Credentials;
    
    namespace PassportLogin.Utils
    {
        public static class MicrosoftPassportHelper
        {
            /// <summary>
            /// Checks to see if Passport is ready to be used.
            /// 
            /// Passport 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> MicrosoftPassportAvailableCheckAsync()
            {
                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("Microsoft Passport is not setup!\nPlease go to Windows Settings and set up a PIN to use it.");
                    return false;
                }
    
                return true;
            }
        }
    }
    
  • 在 Login.xaml.cs 中,添加对 Utils 命名空间的引用。In Login.xaml.cs add a reference to the Utils namespace. 这将解决 OnNavigatedTo 方法中的错误。This will resolve the error in the OnNavigatedTo method.

    using PassportLogin.Utils;
    
  • 生成并运行应用程序 (F5)。Build and run the application (F5). 你将导航到登录页,Windows Hello 横幅将指示 Hello 是否可供使用。You will be navigated to the login page and the Windows Hello banner will indicate to you if Hello is ready to be used. 你应该看到绿色或蓝色横幅,这指示你的计算机上的 Windows Hello 状态。You should see either the green or blue banner indicating the Windows Hello status on your machine.

    Windows Hello 登录屏幕就绪

    Windows Hello 登录屏幕未设置

  • 接下来你需要执行的操作是生成用于登录的逻辑。The next thing you need to do is build the logic for signing in. 创建名为“Models”的新文件夹。Create a new folder called "Models".

  • 在 Models 文件夹中,创建名为“Account.cs”的新类。In the Models folder create a new class called "Account.cs". 此类将用作你的帐户模型。This class will act as your account model. 由于这是一个示例,因此它将仅包含一个用户名。As this is a sample it will only contain a username. 将类定义更改为公共,并添加 Username 属性。Change the class definition to public and add the Username property.

    namespace PassportLogin.Models
    {
        public class Account
        {
            public string Username { get; set; }
        }
    }
    
  • 你将需要一种帐户处理方法。You will need a way to handle accounts. 对于本动手实验,由于没有服务器或数据库,因此将本地保存和加载用户列表。For this hands on lab as there is no server, or a database, a list of users will be saved and loaded locally. 右键单击 Utils 文件夹,然后添加名为“AccountHelper.cs”的新类。Right click on the Utils folder and add a new class called "AccountHelper.cs". 将类定义更改为公共静态。Change the class definition to be public static. AccountHelper 为静态类,其中包含本地保存和加载帐户列表的所有必需方法。The AccountHelper is a static class that will contain all the necessary methods to save and load the list of accounts locally. 使用 XmlSerializer 即可进行保存和加载。Saving and loading will work by using an XmlSerializer. 你还需要记住保存的文件及其保存位置。You will also need to remember the file you saved and where you saved it.

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    using System.Xml.Serialization;
    using Windows.Storage;
    using PassportLogin.Models;
    
    namespace PassportLogin.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 = new List<Account>();
    
            /// <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()
            {
                XmlSerializer xmlizer = new XmlSerializer(typeof(List<Account>));
                StringWriter 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)
            {
                XmlSerializer xmlizer = new XmlSerializer(typeof(List<Account>));
                TextReader textreader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(listAsXml)));
    
                return AccountList = (xmlizer.Deserialize(textreader)) as List<Account>;
            }
        }
    }
    
  • 接下来,实现一种可从帐户的本地列表添加和删除帐户的方法。Next, implement a way to add and remove an account from the local list of accounts. 每次执行这些操作都将保存该列表。These actions will each save the list. 你将需要对本动手实验执行的最终方法是验证方法。The final method that you will need for this hands on lab is a validation method. 因为没有用户的身份验证服务器或数据库,因此这将对硬编码的单个用户进行验证。As there is no auth server or database of users, this will validate against a single user which is hard coded. 这些方法应添加到 AccountHelper 类。These methods should be added to the AccountHelper class.

    public static Account AddAccount(string username)
            {
                // Create a new account with the username
                Account 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.
                // For this tutorial however we will just have a existing sample user that is just "sampleUsername"
                // If the username is null or does not match "sampleUsername" it will fail validation. In which case the user should register a new passport user
    
                if (string.IsNullOrEmpty(username))
                {
                    return false;
                }
    
                if (!string.Equals(username, "sampleUsername"))
                {
                    return false;
                }
    
                return true;
            }
    
  • 接下来你需要执行的操作是处理来自用户的登录请求。The next thing you need to do is handle a sign in request from the user. 在 Login.xaml.cs 中,创建一个将保存当前正在登录的帐户的新私有变量。In Login.xaml.cs create a new private variable that will hold the current account logging in. 然后,添加一个名为 SignInPassport 的新方法。Then add a new method call SignInPassport. 这将使用 AccountHelper.ValidateAccountCredentials 方法验证帐户凭据。This will validate the account credentials using the AccountHelper.ValidateAccountCredentials method. 如果输入的用户名与你在上一步中设置的硬编码字符串值相同,此方法将返回一个布尔值。This method will return a Boolean value if the entered user name is the same as the hard coded string value you set in the previous step. 在本示例中,硬编码的值是“sampleUsername”。The hard coded value for this sample is "sampleUsername".

    using PassportLogin.Models;
    using PassportLogin.Utils;
    using System.Diagnostics;
    
    namespace PassportLogin.Views
    {
        public sealed partial class Login : Page
        {
            private Account _account;
    
            public Login()
            {
                this.InitializeComponent();
            }
    
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                // Check Microsoft Passport is setup and available on this machine
                if (await MicrosoftPassportHelper.MicrosoftPassportAvailableCheckAsync())
                {
                }
                else
                {
                    // Microsoft Passport is not setup so inform the user
                    PassportStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                    PassportStatusText.Text = "Microsoft Passport is not setup!\nPlease go to Windows Settings and set up a PIN to use it.";
                    PassportSignInButton.IsEnabled = false;
                }
            }
    
            private void PassportSignInButton_Click(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
                SignInPassport();
            }
    
            private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
            {
                ErrorMessage.Text = "";
            }
    
            private async void SignInPassport()
            {
                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 MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
                    //{
                    //    Debug.WriteLine("Successfully signed in with Microsoft Passport!");
                    //}
                }
                else
                {
                    ErrorMessage.Text = "Invalid Credentials";
                }
            }
        }
    }
    
  • 你可能已经注意到,注释代码引用了 MicrosoftPassportHelper 中的方法。You may have noticed the commented code that was referencing a method in MicrosoftPassportHelper. 在 MicrosoftPassportHelper.cs 中,添加名为 CreatePassportKeyAsync 的新方法。In MicrosoftPassportHelper.cs add in a new method called CreatePassportKeyAsync. 此方法使用 KeyCredentialManager 中的 Windows Hello API。This method uses the Windows Hello API in the KeyCredentialManager. 调用 RequestCreateAsync 将创建特定于 accountId 和本地计算机的 Passport 密钥。Calling RequestCreateAsync will create a Passport key that is specific to the accountId and the local machine. 如果你对在真实应用场景中进行实现感兴趣,请注意 switch 语句中的注释。Please note the comments in the switch statement if you are interested in implementing this in a real world scenario.

    /// <summary>
    /// Creates a Passport key on the machine using the _account id passed.
    /// </summary>
    /// <param name="accountId">The _account id associated with the _account that we are enrolling into Passport</param>
    /// <returns>Boolean representing if creating the Passport key succeeded</returns>
    public static async Task<bool> CreatePassportKeyAsync(string accountId)
    {
        KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(accountId, KeyCredentialCreationOption.ReplaceExisting);
    
        switch (keyCreationResult.Status)
        {
            case KeyCredentialStatus.Success:
                Debug.WriteLine("Successfully made key");
    
                // In the real world authentication would take place on a server.
                // So every time a user migrates or creates a new Microsoft Passport account Passport details should be pushed to the server.
                // The details that would be pushed to the server include:
                // The public key, keyAttesation 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 Passport 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 setup Microsoft Passport
                Debug.WriteLine("Microsoft Passport is not setup!\nPlease go to Windows Settings and set up a PIN to use it.");
                break;
            default:
                break;
        }
    
        return false;
    }
    
  • 现在你已创建 CreatePassportKeyAsync 方法,请返回到 Login.xaml.cs 文件,并取消对 SignInPassport 方法内代码的注释。Now you have created the CreatePassportKeyAsync method, return to the Login.xaml.cs file and uncomment the code inside the SignInPassport method.

    private async void SignInPassport()
    {
        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 MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Microsoft Passport!");
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 生成并运行应用程序。Build and run the application. 你将转到登录页。You will be taken to the Login page. 键入“sampleUsername”,然后单击“登录”。Type in "sampleUsername" and click login. 系统将通过一条 Windows Hello 提示信息提示你输入 PIN。You will be prompted with a Windows Hello prompt asking you to enter your PIN. 在正确输入 PIN 之后,CreatePassportKeyAsync 方法将能够创建 Windows Hello 密钥。Upon entering your PIN correctly the CreatePassportKeyAsync method will be able to create a Windows Hello key. 监视输出窗口,以查看是否显示指示已成功的消息。Monitor the output windows to see if the messages indicating success are shown.

    Windows Hello 登录 PIN 提示

练习2:欢迎用户和用户选择页Exercise 2: Welcome and User Selection Pages

在本练习中,你将从上一练习继续操作。In this exercise, you will continue from the previous exercise. 当用户成功登录时,他们应该进入可从中注销或删除其帐户的欢迎页。When a person successfully logs in they should be taken to a welcome page where they can sign out or delete their account. 当 Windows Hello 为每台计算机创建密钥时,将创建一个用户选择屏幕,该屏幕上将显示已登录这台计算机的所有用户。As Windows Hello creates a key for every machine, a user selection screen can be created, which displays all users that have been signed in on that machine. 然后,用户可以选择这些帐户之一并直接转至欢迎屏幕,而无需重新输入密码,因为已针对这些用户进行身份验证,以便访问该计算机。A user can then select one of these accounts and go directly to the welcome screen without needed to re-enter a password as they have already authenticated to access the machine.

  • 在 Views 文件夹中,添加名为“Welcome.xaml”的新空白页。In the Views folder add a new blank page called "Welcome.xaml". 添加以下 XAML 来完成用户界面。Add the following XAML to complete the user interface. 这将显示一个标题、登录的用户名以及两个按钮。This will display a title, the logged in username, and two buttons. 其中一个按钮将向后导航到用户列表(将在稍后创建),另一个按钮将处理忘记此用户的情况。One of the buttons will navigate back to a user list (that you will create later), and the other button will handle forgetting this user.

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <TextBlock x:Name="Title" Text="Welcome" FontSize="40" TextAlignment="Center"/>
        <TextBlock x:Name="UserNameText" FontSize="28" TextAlignment="Center" Foreground="Black"/>
    
        <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 代码隐藏文件中,添加用于保留已登录的帐户的新私有变量。In the Welcome.xaml.cs code behind file, add a new private variable that will hold the account that is logged in. 你将需要实现某种方法来替代 OnNavigateTo 事件,以便存储传递至欢迎页的帐户。You will need to implement a method to override the OnNavigateTo event, this will store the account passed to the welcome page. 还需要针对 XAML 中定义的两个按钮实现单击事件。You will also need to implement the click event for the two buttons defined in the XAML. 你将需要一个对 Models 和 Utils 文件夹的引用。You will need a reference to the Models and Utils folders.

    using PassportLogin.Models;
    using PassportLogin.Utils;
    using System.Diagnostics;
    
    namespace PassportLogin.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 it from Microsoft Passport
                // MicrosoftPassportHelper.RemovePassportAccountAsync(_activeAccount);
    
                // Remove it from the local accounts list and resave the updated list
                AccountHelper.RemoveAccount(_activeAccount);
    
                Debug.WriteLine("User " + _activeAccount.Username + " deleted.");
            }
        }
    }
    
  • 你可能已注意到,某行在忘记用户单击事件中已有注释。You may have noticed a line commented out in the forget user click event. 该帐户将从你的本地列表中删除,不过当前没有从 Windows Hello 删除的方法。The account is being removed from your local list but currently there is no way to be removed from Windows Hello. 你需要在 MicrosoftPassportHelper.cs 中实现一个新方法,以便处理 Windows Hello 用户的删除操作。You need to implement a new method in MicrosoftPassportHelper.cs that will handle removing a Windows Hello user. 此方法将使用其他 Windows Hello API 打开和删除该帐户。This method will use other Windows Hello APIs to open and delete the account. 实际上,在删除帐户后,应通知服务器或数据库,以便用户数据库保持有效。In the real world when you delete an account the server or database should be notified so the user database remains valid. 你将需要一个对 Models 文件夹的引用。You will need a reference to the Models folder.

    using PassportLogin.Models;
    
    /// <summary>
    /// Function to be called when user requests deleting their account.
    /// Checks the KeyCredentialManager to see if there is a Passport for the current user
    /// Then deletes the local key associated with the Passport.
    /// </summary>
    public static async void RemovePassportAccountAsync(Account account)
    {
        // Open the account with Passport
        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, RemovePassportAccountOnServer(account);
        }
    
        // Then delete the account from the machines list of Passport Accounts
        await KeyCredentialManager.DeleteAsync(account.Username);
    }
    
  • 返回到 Welcome.xaml.cs,并取消用于调用 RemovePassportAccountAsync 的行的注释。Back in Welcome.xaml.cs, uncomment the line that calls RemovePassportAccountAsync.

    private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
    {
        // Remove it from Microsoft Passport
        MicrosoftPassportHelper.RemovePassportAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and resave the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine("User " + _activeAccount.Username + " deleted.");
    }
    
  • 在 SignInPassport 方法(属于 Login.xaml.cs)中,在CreatePassportKeyAsync 成功创建后,应导航到欢迎屏幕并传递该帐户。In the SignInPassport method (of Login.xaml.cs), once the CreatePassportKeyAsync is successful it should navigate to the Welcome screen and pass the Account.

    private async void SignInPassport()
    {
        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 MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Microsoft Passport!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 生成并运行应用程序。Build and run the application. 使用“sampleUsername”登录,然后单击“登录”。Login with "sampleUsername" and click login. 输入你的 PIN,如果成功,你应导航到欢迎屏幕。Enter your PIN and if successful you should be navigated to the welcome screen. 尝试单击“忘记用户”,并监视输出窗口以查看是否已删除该用户。Try clicking forget user and monitor the output window to see if the user was deleted. 请注意,在用户删除后,你将仍位于欢迎页上。Notice that when the user is deleted you remain on the welcome page. 你需要创建应用可以导航到的用户选择页。You will need to create a user selection page that the app can navigate to.

    Windows Hello 欢迎屏幕

  • 在 Views 文件夹中,创建名为“UserSelection.xaml”的新空白页,然后添加以下 XAML 来定义用户界面。In the Views folder create a new blank page called "UserSelection.xaml" and add the following XAML to define the user interface. 此页面将包含一个用于在本地帐户列表中显示所有用户的 ListView,以及一个用于导航到登录页的 Button,从而让用户可以添加其他帐户。This page will contain a ListView that displays all the users in the local accounts list, and a Button that will navigate to the login page to allow the user to add another account.

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <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 中,如果本地列表中不存在任何帐户,则实现将导航到登录页的加载方法。In UserSelection.xaml.cs implement the loaded method that will navigate to the login page if there are no accounts in the local list. 此外,针对 ListView 实现 SelectionChanged 事件,针对 Button 实现单击事件。Also implement the SelectionChanged event for the ListView and a click event for the Button.

    using System.Diagnostics;
    using PassportLogin.Models;
    using PassportLogin.Utils;
    
    namespace PassportLogin.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 LoginPage
                    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 页的应用中的多个位置。There are a few places in the app where you want to navigated to the UserSelection page. 在 MainPage.xaml.cs 中,你应导航到 UserSelection 页而不是登录页。In MainPage.xaml.cs you should navigate to the UserSelection page instead of the Login page. 当你在 MainPage 中执行加载事件时,你需要加载帐户列表,以便 UserSelection 页可以检查是否存在任何帐户。While you are in the loaded event in MainPage you will need to load the accounts list so that the UserSelection page can check if there are any accounts. 此操作不仅需要更改要异步执行的加载方法,还需要添加对 Utils 文件夹的引用。This will require changing the loaded method to be async and also adding a reference to the Utils folder.

    using PassportLogin.Utils;
    
    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        // Load the local Accounts List before navigating to the UserSelection page
        await AccountHelper.LoadAccountListAsync();
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 接下来,需要从欢迎页导航到 UserSelection 页。Next you will want to navigate to the UserSelection page from the Welcome page. 在这两个单击事件中,你应向后导航到 UserSelection 页。In both click events you should navigate back to the UserSelection page.

    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 Microsoft Passport
        MicrosoftPassportHelper.RemovePassportAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and resave the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine("User " + _activeAccount.Username + " deleted.");
    
        // Navigate back to UserSelection page.
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 在登录页中,你需要代码来登录到从 UserSelection 页的列表中选择的帐户。In the Login page you need code to log in to the account selected from the list in the UserSelection page. 在 OnNavigatedTo 事件中,存储已传递给导航的帐户。In OnNavigatedTo event store the account passed to the navigation. 首先,添加新的私有变量,用于标识帐户是否为现有帐户。Start by adding a new private variable that will identify if the account is an existing account. 然后,处理 OnNavigatedTo 事件。Then handle the OnNavigatedTo event.

    namespace PassportLogin.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 Microsoft Passport 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 Microsoft Passport is setup and available on this machine
                if (await MicrosoftPassportHelper.MicrosoftPassportAvailableCheckAsync())
                {
                    if (e.Parameter != null)
                    {
                        _isExistingAccount = true;
                        // Set the account to the existing account being passed in
                        _account = (Account)e.Parameter;
                        UsernameTextBox.Text = _account.Username;
                        SignInPassport();
                    }
                }
                else
                {
                    // Microsoft Passport is not setup so inform the user
                    PassportStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                    PassportStatusText.Text = "Microsoft Passport is not setup!\n" + 
                        "Please go to Windows Settings and set up a PIN to use it.";
                    PassportSignInButton.IsEnabled = false;
                }
            }
        }
    }
    
  • SignInPassport 方法将需要进行更新,以便登录到所选的帐户。The SignInPassport method will need to be updated to sign in to the selected account. MicrosoftPassportHelper 将需要另一种方法来使用 Passport 打开帐户,因为帐户已具有一个为其创建的 Passport 密钥。The MicrosoftPassportHelper will need another method to open the account with Passport, as the account already has a Passport key created for it. 在 MicrosoftPassportHelper.cs 中实现新方法,以便现有用户可以使用 Passport 登录。Implement the new method in MicrosoftPassportHelper.cs to sign in an existing user with passport. 有关代码的每个部分的信息,请阅读整个代码注释。For information on each part of the code please read through the code comments.

    /// <summary>
    /// Attempts to sign a message using the Passport key on the system for the accountId passed.
    /// </summary>
    /// <returns>Boolean representing if creating the Passport authentication message succeeded</returns>
    public static async Task<bool> GetPassportAuthenticationMessageAsync(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 Microsoft Passport.
    
        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 Microsoft Passport 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. Microsoft Passport has been disabled
            // 2. Microsoft Passport has been disabled and re-enabled cause the Microsoft Passport Key to change.
            // Calling CreatePassportKey and passing through the account will attempt to replace the existing Microsoft Passport Key for that account.
            // If the error really is that Microsoft Passport is disabled then the CreatePassportKey method will output that error.
            if (await CreatePassportKeyAsync(account.Username))
            {
                // If the Passport Key was again successfully created, Microsoft Passport has just been reset.
                // Now that the Passport Key has been reset for the _account retry sign in.
                return await GetPassportAuthenticationMessageAsync(account);
            }
        }
    
        // Can't use Passport right now, try again later
        return false;
    }
    
  • 在 Login.xaml.cs 中更新 SignInPassport 方法以处理现有帐户。Update the SignInPassport method in Login.xaml.cs to handle the existing account. 这将在 MicrosoftPassportHelper.cs 中使用新方法。This will use the new method in the MicrosoftPassportHelper.cs. 如果成功,将登录该帐户,并且用户将导航到欢迎屏幕。If successful the account will be signed in and the user navigated to the welcome screen.

    private async void SignInPassport()
    {
        if (_isExistingAccount)
        {
            if (await MicrosoftPassportHelper.GetPassportAuthenticationMessageAsync(_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 MicrosoftPassportHelper.CreatePassportKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Microsoft Passport!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 生成并运行应用程序。Build and run the application. 使用“sampleUsername”登录。Login with "sampleUsername". 键入你的 PIN,如果成功,你将导航到欢迎屏幕。Type in your PIN and if successful you will be navigated to the Welcome screen. 通过单击返回到用户列表。Click back to user list. 你现在应该可以在列表中看到用户。You should now see a user in the list. 通过单击此 Passport,你可以重新登录,而无需重新输入任何密码等。If you click on this Passport enables you to sign back in without having to re-enter any passwords etc.

    Windows Hello 选择用户列表

练习3:注册新的 Windows Hello 用户Exercise 3: Registering a new Windows Hello user

在本练习中,将为你创建一个新页面,将在该页面中使用 Windows Hello 创建一个新帐户。In this exercise you will be creating a new page that will create a new account with Windows Hello. 这将与登录页的工作原理类似。This will work similarly to how the Login page works. 登录页将针对迁移的现有用户进行实现,以便使用 Windows Hello。The Login page is implemented for an existing user that is migrating to use Windows Hello. PassportRegister 页将为新用户创建 Windows Hello 注册。A PassportRegister page will create Windows Hello registration for a new user.

  • 在 Views 文件夹中,创建名为“PassportRegister.xaml”的新空白页。In the views folder create a new blank page called "PassportRegister.xaml". 在 XAML 中,添加以下内容来设置用户界面。In the XAML add in the following to setup the user interface. 此处的界面类似于登录页。The interface here is similar to the Login page.

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
      <StackPanel Orientation="Vertical">
        <TextBlock x:Name="Title" Text="Register New Passport 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="PassportRegisterButton" Content="Register" Background="DodgerBlue" Foreground="White"
            Click="RegisterButton_Click_Async" Width="80" HorizontalAlignment="Center" Margin="0,20"/>
    
        <Border x:Name="PassportStatus" Background="#22B14C"
                   Margin="4" Height="100">
          <TextBlock x:Name="PassportStatusText" Text="Microsoft Passport is ready to use!" FontSize="20"
                 Margin="4" TextAlignment="Center" VerticalAlignment="Center"/>
        </Border>
      </StackPanel>
    </Grid>
    
  • 在 PassportRegister.xaml.cs 代码隐藏文件中,实现私有 Account 变量,并针对注册 Button 实现单击事件。In the PassportRegister.xaml.cs code behind file implement a private Account variable and a click event for the register Button. 这将添加一个新的本地帐户,并创建 Passport 密钥。This will add a new local account and create a Passport key.

    using PassportLogin.Models;
    using PassportLogin.Utils;
    
    namespace PassportLogin.Views
    {
        public sealed partial class PassportRegister : Page
        {
            private Account _account;
    
            public PassportRegister()
            {
                InitializeComponent();
            }
    
            private async void RegisterButton_Click_Async(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
    
                //In the real world you would normally validate the entered credentials and information before 
                //allowing a user to register a new account. 
                //For this sample though we will skip that step and just register an account if username is not null.
    
                if (!string.IsNullOrEmpty(UsernameTextBox.Text))
                {
                    //Register a new account
                    _account = AccountHelper.AddAccount(UsernameTextBox.Text);
                    //Register new account with Microsoft Passport
                    await MicrosoftPassportHelper.CreatePassportKeyAsync(_account.Username);
                    //Navigate to the Welcome Screen. 
                    Frame.Navigate(typeof(Welcome), _account);
                }
                else
                {
                    ErrorMessage.Text = "Please enter a username";
                }
            }
        }
    }
    
  • 当单击注册时,需要从登录页导航至该页。You need to navigate to this page from the Login page when register is clicked.

    private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
    {
        ErrorMessage.Text = "";
        Frame.Navigate(typeof(PassportRegister));
    }
    
  • 生成并运行应用程序。Build and run the application. 尝试注册新用户。Try to register a new user. 然后返回到用户列表并验证你可以选择该用户和登录信息。Then return to the user list and validate that you can select that user and login.

    Windows Hello 注册新用户

在本实验中,你已经掌握了使用新 Windows Hello API 对现有用户进行身份验证和为新用户创建帐户时所需的基本技能。In this lab you have learned the essential skills you need to use the new Windows Hello API to authenticate existing users and create accounts for new users. 借助此新知识,你的用户现在不再需要记住你的应用程序密码,不过请放心,你的应用程序仍然受用户身份验证保护。With this new knowledge you can start removing the need for users to remember passwords for your application, yet remain confident that your applications remain protected by user authentication. Windows 10 使用 Windows Hello 的新身份验证技术来支持其生物识别登录选项。Windows 10 uses Windows Hello's new authentication technology to support its biometrics login options.