Поделиться через


Создание приложения для входа Windows Hello

Это первая часть полного пошагового руководства по созданию упаковаемого приложения Windows, которое использует Windows Hello в качестве альтернативы традиционным системам проверки подлинности имени пользователя и пароля. В этом случае приложение является приложением WinUI, но такой же подход можно использовать с любым упакованным приложением Windows, включая приложения WPF и Windows Forms. Приложение использует имя пользователя для входа и создает ключ Hello для каждой учетной записи. Эти учетные записи будут защищены ПИН-кодом, настроенным в параметрах Windows в конфигурации Windows Hello.

Это пошаговое руководство разделено на две части: создание приложения и подключение серверной службы. Завершив работу с этой статьей, перейдите к части 2. Служба входа Windows Hello.

Прежде чем начать, ознакомьтесь с общими сведениями о работе Windows Hello .

Начать

Чтобы создать этот проект, вам потребуется некоторый опыт работы с C#и XAML. Вам также потребуется использовать Visual Studio 2022 на компьютере с Windows 10 или Windows 11. Полные инструкции по настройке среды разработки см. в статье "Начало работы с WinUI ".

  • В Visual Studio выберите Файл>Создать>Проект.
  • В раскрывающихся фильтрах диалогового окна "Новый проект" выберите C#/C++, Windows и WinUI соответственно.
  • Выберите пустое приложение, упакованная (WinUI 3 в классическом приложении) и присвойте приложению имя WindowsHelloLogin.
  • Создание и запуск нового приложения (F5) должно отобразиться пустое окно на экране. Закройте приложение.

Снимок экрана: новое приложение для входа в Windows Hello, работающее в первый раз

Упражнение 1. Вход с помощью Windows Hello

В этом упражнении вы узнаете, как проверить, настроен ли Windows Hello на компьютере и как войти в учетную запись с помощью Windows Hello.

  • В новом проекте создайте в решении папку "Представления". Эта папка будет содержать страницы, к которым будут переходить в этом примере. Щелкните проект правой кнопкой мыши в Обозреватель решений, выберите "Добавить>новую папку", а затем переименуйте папку в Представления.

    Снимок экрана: добавление новой папки с именем Views в проект входа Windows Hello

  • Откройте MainWindow.xaml и замените содержимое Window пустым StackPanel или Grid элементом управления. Мы будем реализовывать навигацию по страницам и переходить на новую страницу при загрузке MainWindow , поэтому нам не нужен контент в MainWindow.

  • Title Добавьте свойство в MainWindow в XAML. Атрибут должен выглядеть следующим образом: Title="Windows Hello Login"

  • Удалите обработчик событий myButton_Click из MainWindow.xaml.cs, чтобы избежать ошибок компиляции. Этот обработчик событий не нужен для этого примера.

  • Щелкните правой кнопкой мыши папку "Представления", выберите "Добавить>новый элемент" и выберите шаблон "Пустая страница". Назовите эту страницу 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. Добавьте следующие два метода события для обработки событий входа и регистрации . Теперь эти методы будут задавать 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 для входа

  • Измените область действия класса public staticWindowsHelloHelper, а затем добавьте следующий метод, чтобы сообщить пользователю, готов ли 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 создайте новый класс с именем "Account.cs". Этот класс будет выступать в качестве модели учетной записи. Так как это пример проекта, он будет содержать только имя пользователя. Измените область public класса на и добавьте Username свойство.

    namespace WindowsHelloLogin.Models
    {
        public class Account
        {
            public string Username { get; set; }
        }
    }
    
  • Приложению требуется способ обработки учетных записей. Для этой лаборатории, так как нет сервера или базы данных, список пользователей сохраняется и загружается локально. Щелкните правой кнопкой мыши папку Utils и добавьте новый класс с именем "AccountHelper.cs". Измените область public staticкласса. AccountHelper — это статический класс, содержащий все необходимые методы для сохранения и загрузки списка учетных записей локально. Сохранение и загрузка работают с помощью 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 . Этот метод возвращает логическое значение, если введенное имя пользователя совпадает с заданным строковым значением, настроенным на предыдущем шаге. Жестко закодированное значение для этого примера — 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. Этот метод использует API Windows Hello в KeyCredentialManager. Вызов RequestCreateAsync создаст ключ 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. При правильном вводе ПИН-кода метод 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 события необходимо реализовать метод, который будет хранить учетную запись, переданную на страницу приветствия . Кроме того, необходимо реализовать Click событие для двух кнопок, определенных в XAML. Необходимо добавить инструкции using для WindowsHelloLogin.Models пространств имен и WindowsHelloLogin.Utils пространств имен.

    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. Этот метод будет использовать другие API Windows Hello для открытия и удаления учетной записи. В реальном мире при удалении учетной записи сервер или база данных должна быть уведомлена, чтобы пользовательская база данных оставалась допустимой. Вам потребуется инструкция using, ссылающаяся WindowsHelloLogin.Models на пространство имен.

    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 и нажмите кнопку "Войти". Введите ПИН-код и в случае успешного перехода на экран приветствия . Попробуйте щелкнуть "Забыть пользователя" и отслеживать окно вывода Visual Studio, чтобы узнать, был ли пользователь удален. Обратите внимание, что при удалении пользователя вы остаетесь на странице приветствия . Вам потребуется создать страницу выбора пользователя, на которую приложение может перемещаться.

    Снимок экрана: экран приветствия Windows Hello

  • В папке "Представления " создайте пустую страницу с именем 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 события и Click события для Button.

    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 потребуется изменить метод, чтобы быть асинхронным, а также добавить инструкцию using для WindowsHelloLogin.Utils пространства имен.

    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;
    }
    
  • Обновите метод SignInWindowsHelloAsync в Login.xaml.cs для обработки существующей учетной записи. Это будет использовать новый метод в 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. Введите ПИН-код и в случае успешного перехода на страницу приветствия . Вернитесь к списку пользователей. Теперь в списке должен появиться пользователь. Если щелкнуть это, 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

В этой лаборатории вы узнали основные навыки, необходимые для использования нового API Windows Hello для проверки подлинности существующих пользователей и создания учетных записей для новых пользователей. С помощью этих новых знаний вы можете начать удалять необходимость запоминать пароли для приложения, но оставаться уверенным в том, что ваши приложения остаются защищенными с помощью проверки подлинности пользователей. Windows использует новую технологию проверки подлинности Windows Hello для поддержки параметров входа в биометрические данные.