다음을 통해 공유


Windows Hello 로그인 앱 만들기

기존 사용자 이름 및 암호 인증 시스템의 대안으로 Windows Hello를 사용하는 패키지된 Windows 앱을 만드는 방법에 대한 전체 연습의 첫 번째 부분입니다. 이 경우 앱은 WinUI 앱이지만 WPF 및 Windows Forms 앱을 포함하여 패키지된 모든 Windows 앱에서 동일한 방법을 사용할 수 있습니다. 앱은 로그인에 사용자 이름을 사용하고 각 계정에 대한 Hello 키를 만듭니다. 이러한 계정은 Windows Hello 구성 시 Windows 설정에 설정된 PIN으로 보호됩니다.

이 안내서는 앱을 만드는 파트 그리고 백 엔드 서비스를 연결하는 파트, 이렇게 2가지 파트로 나누어져 있습니다. 이 문서를 마치면 제2부: Windows Hello 로그인 서비스를 계속 진행하세요.

시작하기 전에 Windows Hello 작동 방식을 전반적으로 이해하기 위해 Windows Hello 개요를 읽으셔야 합니다.

시작하기

이 프로젝트를 만드려면 C#과 XAML에 대한 약간의 경험이 필요합니다. Windows 10 또는 Windows 11 컴퓨터에서도 Visual Studio 2022를 사용해야 합니다. 개발 환경 설정에 대한 전체 지침은 WinUI 시작을 참조하세요.

  • Visual Studio에서 파일>새로 만들기>프로젝트를 선택합니다.
  • 새 프로젝트 대화 상자의 드롭다운 필터에서 각각 C#/C++, WindowsWinUI를 선택합니다.
  • 빈 앱( 패키지됨(데스크톱의 WinUI 3)을 선택하고 애플리케이션 이름을 "WindowsHelloLogin"으로 지정합니다.
  • 새 애플리케이션을 만들고 실행(F5)하면 화면에 빈 창이 나타납니다. 애플리케이션을 닫습니다.

처음으로 실행되는 새 Windows Hello 로그인 앱의 스크린샷

연습 1: Windows Hello로 로그인

이 연습에서는 컴퓨터에 Windows Hello가 설정되어 있는지 확인하는 방법과 Windows Hello를 사용하여 계정에 로그인하는 방법을 알아봅니다.

  • 새 프로젝트에서 솔루션 안에 "Views"라는 새 폴더를 만듭니다. 이 폴더에는 이 예시에서 탐색할 페이지들을 담게 됩니다. 솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고 새 폴더 추가>를 선택한 다음 폴더 이름을 보기바꿉니다.

    Windows Hello 로그인 프로젝트에 Views라는 새 폴더를 추가하는 스크린샷

  • MainWindow.xaml을 열고 내용을 빈 StackPanel 컨트롤 또는 Grid 컨트롤로 바꿉 Window 니다. MainWindow가 로드될 때 페이지 탐색을 구현하고 새 페이지로 이동하므로 MainWindow에 콘텐츠가 필요하지 않습니다.

  • XAML에서 Title MainWindow 에 속성을 추가합니다. 특성은 다음과 Title="Windows Hello Login"같습니다.

  • 컴파일 오류를 방지하려면 MainWindow.xaml.cs myButton_Click 이벤트 처리기를 제거합니다. 이 샘플에는 이 이벤트 처리기가 필요하지 않습니다.

  • 보기 폴더를 마우스 오른쪽 단추로 클릭하고 새 항목 추가>를 선택하고 빈 페이지 서식 파일을 선택합니다. 이 페이지의 이름을 "MainPage.xaml"로 지정합니다.

    Windows Hello 로그인 프로젝트에 새 빈 페이지를 추가하는 스크린샷

  • App.xaml.cs 파일을 열고 OnLaunched 처리기를 업데이트하여 앱에 대한 페이지 탐색을 구현합니다. 또한 페이지를 로드하는 동안 발생하는 모든 오류를 처리하기 위해 RootFrame_NavigationFailed 처리기 메서드를 추가해야 합니다.

    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        m_window = new MainWindow();
        var rootFrame = new Frame();
        rootFrame.NavigationFailed += RootFrame_NavigationFailed;
        rootFrame.Navigate(typeof(MainPage), args);
        m_window.Content = rootFrame;
        m_window.Activate();
    }
    
    private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
    {
        throw new Exception($"Error loading page {e.SourcePageType.FullName}");
    }
    
  • 또한 코드의 컴파일 오류를 해결하려면 App.xaml.cs 파일 맨 위에 네 개의 using 문을 추가해야 합니다.

    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Navigation;
    using System;
    using WindowsHelloLogin.Views;
    
  • 보기 폴더를 마우스 오른쪽 단추로 클릭하고 새 항목 추가>를 선택하고 빈 페이지 서식 파일을 선택합니다. 이 페이지의 이름을 "Login.xaml"로 지정하세요.

  • 새 로그인 페이지에 대한 사용자 인터페이스를 설정하려면 다음 XAML을 추가합니다. 이 XAML은 다음 자식을 정렬하는 방법을 정의합니다 StackPanel .

    • 제목을 포함하는 A TextBlock 입니다.

    • 오류 메시지의 경우입니다 TextBlock .

    • 입력할 사용자 이름에 대한 A TextBox 입니다.

    • Button 등록 페이지로 이동하는 A입니다.

    • Windows Hello의 상태를 포함하는 A TextBlock 입니다.

    • TextBlock 백 엔드 또는 구성된 사용자가 아직 없으므로 로그인 페이지를 설명하는 A입니다.

      <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));
            }
        }
    }
    
  • 로그인 페이지에서 현재 컴퓨터에서 Windows Hello를 OnNavigatedTo 사용할 수 있는지 확인하기 위해 이벤트를 처리해야 합니다. Login.xaml.cs 다음 코드를 구현합니다. WindowsHelloHelper 개체는 오류가 있음을 나타냅니다. 이 도우미 클래스를 아직 만들지 않았기 때문입니다.

    public sealed partial class Login : Page
    {
        public Login()
        {
            this.InitializeComponent();
        }
    
        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            // Check if Windows Hello is set up and available on this machine
            if (await WindowsHelloHelper.WindowsHelloAvailableCheckAsync())
            {
            }
            else
            {
                // Windows Hello isn't set up, so inform the user
                WindowsHelloStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                WindowsHelloStatusText.Text = $"Windows Hello is not set up!{Environment.NewLine}Please go to Windows Settings and set up a PIN to use it.";
                LoginButton.IsEnabled = false;
            }
        }
    }
    
  • WindowsHelloHelper 클래스를 만들려면 WindowsHelloLogin 프로젝트를 마우스 오른쪽 단추로 클릭하고 새 폴더 추가>를 클릭합니다. 이 폴더 이름을 Utils로 지정합니다.

  • Utils 폴더를 마우스 오른쪽 단추로 클릭하고 클래스 추가>를 선택합니다. 이 새 클래스의 이름을 "WindowsHelloHelper.cs"로 지정합니다.

    Windows Hello 로그인 도우미 클래스를 만드는 스크린샷

  • WindowsHelloHelper 클래스public static범위를 로 변경한 다음 Windows Hello를 사용할 준비가 되었는지 여부를 사용자에게 알리는 다음 메서드를 추가합니다. 필요한 네임스페이스를 추가해야 합니다.

    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using Windows.Security.Credentials;
    
    namespace WindowsHelloLogin.Utils
    {
        public static class WindowsHelloHelper
        {
            /// <summary>
            /// Checks to see if Windows Hello is ready to be used.
            /// 
            /// Windows Hello has dependencies on:
            ///     1. Having a connected Microsoft Account
            ///     2. Having a Windows PIN set up for that account on the local machine
            /// </summary>
            public static async Task<bool> WindowsHelloAvailableCheckAsync()
            {
                bool keyCredentialAvailable = await KeyCredentialManager.IsSupportedAsync();
                if (keyCredentialAvailable == false)
                {
                    // Key credential is not enabled yet as user 
                    // needs to connect to a Microsoft Account and select a PIN in the connecting flow.
                    Debug.WriteLine("Windows Hello is not set up!\nPlease go to Windows Settings and set up a PIN to use it.");
                    return false;
                }
    
                return true;
            }
        }
    }
    
  • Login.xaml.cs 네임스페이스에 대한 참조를 WindowsHelloLogin.Utils 추가합니다. 이렇게 하면 메서드의 오류가 OnNavigatedTo 해결됩니다.

    using WindowsHelloLogin.Utils;
    
  • 애플리케이션을 빌드 및 실행합니다. 로그인 페이지로 이동하면 Windows Hello를 사용할 준비가 되었으면 Windows Hello 배너가 표시됩니다. 컴퓨터에 Windows Hello 상태를 나타내는 녹색 또는 파란색 배너가 표시됩니다.

    준비 상태가 있는 Windows Hello 로그인 화면의 스크린샷

  • 다음으로 해야 될 일은 로그인을 위한 로직을 빌드하는 겁니다. 프로젝트에 "Models"라는 새 폴더를 만듭니다.

  • Models 폴더에서 "Account.cs"라는 새 클래스를 만듭니다. 이 클래스는 계정 모델 역할을 하게 됩니다. 샘플 프로젝트이므로 사용자 이름만 포함됩니다. 클래스 범위를 변경하고 public 속성을 추가합니다 Username .

    namespace WindowsHelloLogin.Models
    {
        public class Account
        {
            public string Username { get; set; }
        }
    }
    
  • 앱은 계정을 처리하는 방법이 필요합니다. 이 실습 랩의 경우 서버 또는 데이터베이스가 없으므로 사용자 목록이 로컬로 저장되고 로드됩니다. Utils 폴더를 마우스 오른쪽 단추로 클릭하고 "AccountHelper.cs"이라는 새 클래스를 추가합니다. 클래스 범위를 .로 변경합니다 public 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라는 새 메서드를 추가합니다. 이 메서드는 KeyCredentialManager에서 Windows Hello API를 사용합니다. RequestCreateAsync를 호출하면 accountId 및 로컬 컴퓨터와 관련된 Windows Hello 키가 만들어집니다. 실사용 상황에서 이를 구현하는대 관심이 있는 경우 switch 문의 주석을 확인 하세요.

    /// <summary>
    /// Creates a Windows Hello key on the machine using the account ID provided.
    /// </summary>
    /// <param name="accountId">The account ID associated with the account that we are enrolling into Windows Hello</param>
    /// <returns>Boolean indicating if creating the Windows Hello key succeeded</returns>
    public static async Task<bool> CreateWindowsHelloKeyAsync(string accountId)
    {
        KeyCredentialRetrievalResult keyCreationResult = await KeyCredentialManager.RequestCreateAsync(accountId, KeyCredentialCreationOption.ReplaceExisting);
    
        switch (keyCreationResult.Status)
        {
            case KeyCredentialStatus.Success:
                Debug.WriteLine("Successfully created key");
    
                // In the real world, authentication would take place on a server.
                // So, every time a user migrates or creates a new Windows Hello
                // account, details should be pushed to the server.
                // The details that would be pushed to the server include:
                // The public key, keyAttestation (if available), 
                // certificate chain for attestation endorsement key (if available),  
                // status code of key attestation result: keyAttestationIncluded or 
                // keyAttestationCanBeRetrievedLater and keyAttestationRetryType.
                // As this sample has no concept of a server, it will be skipped for now.
                // For information on how to do this, refer to the second sample.
    
                // For this sample, just return true
                return true;
            case KeyCredentialStatus.UserCanceled:
                Debug.WriteLine("User cancelled sign-in process.");
                break;
            case KeyCredentialStatus.NotFound:
                // User needs to set up Windows Hello
                Debug.WriteLine("Windows Hello is not set up!\nPlease go to Windows Settings and set up a PIN to use it.");
                break;
            default:
                break;
        }
    
        return false;
    }
    
  • 이제 CreateWindowsHelloKeyAsync 메서드를 만들었으므로 Login.xaml.cs 파일로 돌아가 SignInWindowsHelloAsync 메서드 내에서 코드의 주석 처리를 제거합니다.

    private async void SignInWindowsHelloAsync()
    {
        if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            //Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Windows Hello!");
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 애플리케이션을 빌드 및 실행합니다. 로그인 페이지로 이동하게 됩니다. 사용자 이름을 "sampleUsername"으로 입력하고 로그인을 클릭합니다. PIN을 입력하라는 Windows Hello 프롬프트가 표시됩니다. PIN을 올바르게 입력하면 CreateWindowsHelloKeyAsync 메서드가 Windows Hello 키를 만들 수 있습니다. 성공 메시지가 표시되는지 출력 창을 모니터링 하세요.

    Windows Hello 로그인 핀 프롬프트의 스크린샷

예제 2: 시작 및 사용자 선택 페이지

이 예제에서 당신은 이전 예제를 이어서 진행하게 됩니다. 사용자가 성공적으로 로그인하면 계정을 로그아웃하거나 삭제할 수 있는 시작 페이지로 이동해야 합니다. Windows Hello는 모든 컴퓨터에 대해 키를 만들므로 해당 컴퓨터에 로그인한 모든 사용자를 표시하는 사용자 선택 화면을 만들 수 있습니다. 그러면 사용자는 기기 사용 인증이 이미 된 상황이므로 이러한 계정 중 하나를 선택하여 암호를 다시 입력할 필요 없이 시작 화면으로 바로 이동할 수 있습니다.

  • Views 폴더에 "Welcome.xaml"이라는 새 빈 페이지를 추가합니다. 다음 XAML을 추가하여 페이지의 사용자 인터페이스를 완료합니다. 그러면 제목, 로그인한 사용자 이름 그리고 두 개의 버튼이 표시됩니다. 버튼 중 하나는 사용자 목록(나중에 만들 예정)으로 다시 이동하고 다른 버튼은 이 사용자를 포겟 처리합니다.

    <Grid>
      <StackPanel>
        <TextBlock x:Name="Title" Text="Welcome" FontSize="40" TextAlignment="Center"/>
        <TextBlock x:Name="UserNameText" FontSize="28" TextAlignment="Center"/>
    
        <Button x:Name="BackToUserListButton" Content="Back to User List" Click="Button_Restart_Click"
                HorizontalAlignment="Center" Margin="0,20" Foreground="White" Background="DodgerBlue"/>
    
        <Button x:Name="ForgetButton" Content="Forget Me" Click="Button_Forget_User_Click"
                Foreground="White"
                Background="Gray"
                HorizontalAlignment="Center"/>
      </StackPanel>
    </Grid>
    
  • Welcome.xaml.cs 코드 숨김 파일에서 로그인한 계정을 보유할 새 프라이빗 변수를 추가합니다. 이벤트를 재정 OnNavigateTo 의하는 메서드를 구현해야 합니다. 그러면 시작 페이지에 전달된 계정이 저장됩니다. 또한 XAML에 정의된 두 단추에 대한 이벤트를 구현 Click 해야 합니다. 및 WindowsHelloLogin.Utils 네임스페이스에 대한 WindowsHelloLogin.Models using 문을 추가해야 합니다.

    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에서 제거할 방법이 없습니다. Windows Hello 사용자 제거를 처리하는 새 메서드를 WindowsHelloHelper.cs 구현해야 합니다. 이 메서드는 다른 Windows Hello API를 사용하여 계정을 열고 삭제합니다. 실제 환경에서 계정을 삭제할 때 사용자 데이터베이스가 유효한 상태로 유지되도록 서버 또는 데이터베이스에 알림을 보내야 합니다. 네임스페이스를 참조하는 WindowsHelloLogin.Models using 문이 필요합니다.

    using WindowsHelloLogin.Models;
    
    /// <summary>
    /// Function to be called when user requests deleting their account.
    /// Checks the KeyCredentialManager to see if there is a Windows Hello
    /// account for the current user.
    /// It then deletes the local key associated with the account.
    /// </summary>
    public static async void RemoveWindowsHelloAccountAsync(Account account)
    {
        // Open the account with Windows Hello
        KeyCredentialRetrievalResult keyOpenResult = await KeyCredentialManager.OpenAsync(account.Username);
    
        if (keyOpenResult.Status == KeyCredentialStatus.Success)
        {
            // In the real world you would send key information to server to unregister
            //for example, RemoveWindowsHelloAccountOnServer(account);
        }
    
        // Then delete the account from the machine's list of Windows Hello accounts
        await KeyCredentialManager.DeleteAsync(account.Username);
    }
    
  • Welcome.xaml.cs 다시 RemoveWindowsHelloAccountAsync를 호출하는 줄의 주석 처리를 제거합니다.

    private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
    {
        // Remove it from Windows Hello
        WindowsHelloHelper.RemoveWindowsHelloAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and re-save the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine($"User {_activeAccount.Username} deleted.");
    }
    
  • SignInWindowsHelloAsync 메서드(Login.xaml.cs)에서 CreateWindowsHelloKeyAsync가 성공하면 시작 페이지로 이동하여 계정을 전달해야 합니다.

    private async void SignInWindowsHelloAsync()
    {
        if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            // Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Windows Hello!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 애플리케이션을 빌드 및 실행합니다. "sampleUsername"으로 로그인하고 로그인을 클릭합니다. PIN을 입력하고 성공하면 시작 화면으로 이동해야 합니다. 사용자 잊기를 클릭하고 Visual Studio의 출력 창을 모니터링하여 사용자가 삭제되었는지 확인합니다. 사용자가 삭제되면 시작 페이지에 남아 있습니다. 앱이 탐색할 수 있는 사용자 선택 페이지를 만들어야 합니다.

    Windows Hello 시작 화면의 스크린샷

  • Views 폴더에서 "UserSelection.xaml"이라는 새 빈 페이지를 만들고 다음 XAML을 추가하여 사용자 인터페이스를 정의합니다. 이 페이지에는 로컬 계정 목록의 모든 사용자를 표시하는 ListView와 Button 사용자가 다른 계정을 추가할 수 있도록 로그인 페이지로 이동하는 ListView가 포함됩니다.

    <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 합니다. 에 대한 ListView 이벤트 및 Click 이벤트에 대한 이벤트도 구현 SelectionChanged 합니다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에서 로드된 이벤트에 있는 동안 사용자 선택 페이지에서 계정이 있는지 확인할 수 있도록 계정 목록을 로드해야 합니다. 이렇게 하려면 메서드를 Loaded 비동기로 변경하고 네임스페이스에 대한 WindowsHelloLogin.Utils using 문을 추가해야 합니다.

    using WindowsHelloLogin.Utils;
    
    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        // Load the local account list before navigating to the UserSelection page
        await AccountHelper.LoadAccountListAsync();
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 다음으로, 앱은 시작 페이지에서 UserSelection 페이지로 이동해야 합니다. 두 이벤트 모두 Click UserSelection 페이지로 다시 이동해야 합니다.

    private void Button_Restart_Click(object sender, RoutedEventArgs e)
    {
        Frame.Navigate(typeof(UserSelection));
    }
    
    private void Button_Forget_User_Click(object sender, RoutedEventArgs e)
    {
        // Remove it from Windows Hello
        WindowsHelloHelper.RemoveWindowsHelloAccountAsync(_activeAccount);
    
        // Remove it from the local accounts list and re-save the updated list
        AccountHelper.RemoveAccount(_activeAccount);
    
        Debug.WriteLine($"User {_activeAccount.Username} deleted.");
    
        // Navigate back to UserSelection page.
        Frame.Navigate(typeof(UserSelection));
    }
    
  • 로그인 페이지에서 UserSelection 페이지의 목록에서 선택한 계정에 로그인하는 코드가 필요합니다. OnNavigatedTo 이벤트에서 탐색 중에 전달된 계정을 저장합니다. 먼저 계정이 존재하는 계정인지 식별하는 새 프라이빗 변수를 추가합니다. 그런 다음 이벤트를 처리합니다 OnNavigatedTo .

    namespace WindowsHelloLogin.Views
    {
        public sealed partial class Login : Page
        {
            private Account _account;
            private bool _isExistingAccount;
    
            public Login()
            {
                InitializeComponent();
            }
    
            /// <summary>
            /// Function called when this frame is navigated to.
            /// Checks to see if Windows Hello is available and if an account was passed in.
            /// If an account was passed in set the "_isExistingAccount" flag to true and set the _account.
            /// </summary>
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                // Check Windows Hello is set up and available on this machine
                if (await WindowsHelloHelper.WindowsHelloAvailableCheckAsync())
                {
                    if (e.Parameter != null)
                    {
                        _isExistingAccount = true;
                        // Set the account to the existing account being passed in
                        _account = (Account)e.Parameter;
                        UsernameTextBox.Text = _account.Username;
                        await SignInWindowsHelloAsync();
                    }
                }
                else
                {
                    // Windows Hello is not set up, so inform the user
                    WindowsHelloStatus.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 50, 170, 207));
                    WindowsHelloStatusText.Text = $"Windows Hello is not set up!{Environment.NewLine}Please go to Windows Settings and set up a PIN to use it.";
                    LoginButton.IsEnabled = false;
                }
            }
        }
    }
    
  • 선택한 계정에 로그인하려면 SignInWindowsHelloAsync 메서드를 업데이트해야 합니다. 계정에 이미 만든 계정 키가 있으므로 WindowsHelloHelper는 Windows Hello를 사용하여 계정을 여는 다른 방법이 필요합니다. WindowsHelloHelper.cs 새 메서드를 구현하여 Windows Hello를 사용하여 기존 사용자를 로그인합니다. 코드의 각 부분에 대한 자세한 내용은 코드 주석을 참조하세요.

    /// <summary>
    /// Attempts to sign a message using the account key on the system for the accountId passed.
    /// </summary>
    /// <returns>Boolean representing if creating the Windows Hello authentication message succeeded</returns>
    public static async Task<bool> GetWindowsHelloAuthenticationMessageAsync(Account account)
    {
        KeyCredentialRetrievalResult openKeyResult = await KeyCredentialManager.OpenAsync(account.Username);
        // Calling OpenAsync will allow the user access to what is available in the app and will not require user credentials again.
        // If you wanted to force the user to sign in again you can use the following:
        // var consentResult = await Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync(account.Username);
        // This will ask for the either the password of the currently signed in Microsoft Account or the PIN used for Windows Hello.
    
        if (openKeyResult.Status == KeyCredentialStatus.Success)
        {
            // If OpenAsync has succeeded, the next thing to think about is whether the client application requires access to backend services.
            // If it does here you would request a challenge from the server. The client would sign this challenge and the server
            // would check the signed challenge. If it is correct, it would allow the user access to the backend.
            // You would likely make a new method called RequestSignAsync to handle all this.
            // For example, RequestSignAsync(openKeyResult);
            // Refer to the second Windows Hello sample for information on how to do this.
    
            // For this sample, there is not concept of a server implemented so just return true.
            return true;
        }
        else if (openKeyResult.Status == KeyCredentialStatus.NotFound)
        {
            // If the account is not found at this stage. It could be one of two errors. 
            // 1. Windows Hello has been disabled
            // 2. Windows Hello has been disabled and re-enabled cause the Windows Hello Key to change.
            // Calling CreateWindowsHelloKeyAsync and passing through the account will attempt to replace the existing Windows Hello Key for that account.
            // If the error really is that Windows Hello is disabled then the CreateWindowsHelloKeyAsync method will output that error.
            if (await CreateWindowsHelloKeyAsync(account.Username))
            {
                // If the Hello Key was again successfully created, Windows Hello has just been reset.
                // Now that the Hello Key has been reset for the account retry sign in.
                return await GetWindowsHelloAuthenticationMessageAsync(account);
            }
        }
    
        // Can't use Windows Hello right now, try again later
        return false;
    }
    
  • Login.xaml.cs SignInWindowsHelloAsync 메서드를 업데이트하여 기존 계정을 처리합니다. WindowsHelloHelper.cs 새 메서드를 사용합니다. 성공하면 계정이 로그인되고 사용자가 시작 페이지로 이동됩니다.

    private async Task SignInWindowsHelloAsync()
    {
        if (_isExistingAccount)
        {
            if (await WindowsHelloHelper.GetWindowsHelloAuthenticationMessageAsync(_account))
            {
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else if (AccountHelper.ValidateAccountCredentials(UsernameTextBox.Text))
        {
            //Create and add a new local account
            _account = AccountHelper.AddAccount(UsernameTextBox.Text);
            Debug.WriteLine("Successfully signed in with traditional credentials and created local account instance!");
    
            if (await WindowsHelloHelper.CreateWindowsHelloKeyAsync(UsernameTextBox.Text))
            {
                Debug.WriteLine("Successfully signed in with Windows Hello!");
                Frame.Navigate(typeof(Welcome), _account);
            }
        }
        else
        {
            ErrorMessage.Text = "Invalid Credentials";
        }
    }
    
  • 애플리케이션을 빌드 및 실행합니다. "sampleUsername"을 사용하여 로그인합니다. PIN을 입력하면 성공하면 시작 페이지로 이동합니다. 클릭하여 사용자 목록으로 돌아갑니다. 이제 목록에 사용자가 표시되는걸 볼 수 있습니다. 이 항목을 클릭하면 WindowsHello를 사용하면 암호 등을 다시 입력하지 않고도 다시 로그인할 수 있습니다.

    Windows Hello 사용자 목록 선택 스크린샷

연습 3: 새 Windows Hello 사용자 등록

이 연습에서는 Windows Hello를 사용하여 새 계정을 만들 수 있는 새 페이지를 만듭니다. 이는 로그인 페이지의 작동 방식과 유사하게 작동합니다. 로그인 페이지는 Windows Hello를 사용하도록 마이그레이션하는 기존 사용자에 대해 구현됩니다. WindowsHelloRegister 페이지는 새 사용자에 대한 Windows Hello 등록을 만듭니다.

  • Views 폴더에서 "WindowsHelloRegister.xaml"이라는 새 빈 페이지를 만듭니다. XAML에서 다음을 추가하여 사용자 인터페이스를 만듭니다. 이 페이지의 인터페이스는 로그인 페이지와 비슷합니다.

    <Grid>
      <StackPanel>
        <TextBlock x:Name="Title" Text="Register New Windows Hello User" FontSize="24" Margin="4" TextAlignment="Center"/>
    
        <TextBlock x:Name="ErrorMessage" Text="" FontSize="20" Margin="4" Foreground="Red" TextAlignment="Center"/>
    
        <TextBlock Text="Enter your new username below" Margin="0,0,0,20"
                   TextWrapping="Wrap" Width="300"
                   TextAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
    
        <TextBox x:Name="UsernameTextBox" Margin="4" Width="250"/>
    
        <Button x:Name="RegisterButton" Content="Register" Background="DodgerBlue" Foreground="White"
                Click="RegisterButton_Click_Async" Width="80" HorizontalAlignment="Center" Margin="0,20"/>
    
        <Border x:Name="WindowsHelloStatus" Background="#22B14C" Margin="4" Height="100">
          <TextBlock x:Name="WindowsHelloStatusText" Text="Windows Hello is ready to use!" FontSize="20"
                     Margin="4" TextAlignment="Center" VerticalAlignment="Center"/>
        </Border>
      </StackPanel>
    </Grid>
    
  • WindowsHelloRegister.xaml.cs 코드 숨김 파일에서 레지스터 단추에 대한 프라이빗 Account 변수 및 Click 이벤트를 구현합니다. 그러면 새 로컬 계정이 추가되고 Windows Hello 키가 생성됩니다.

    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml;
    using WindowsHelloLogin.Models;
    using WindowsHelloLogin.Utils;
    
    namespace WindowsHelloLogin.Views
    {
        public sealed partial class WindowsHelloRegister : Page
        {
            private Account _account;
    
            public WindowsHelloRegister()
            {
                InitializeComponent();
            }
    
            private async void RegisterButton_Click_Async(object sender, RoutedEventArgs e)
            {
                ErrorMessage.Text = "";
    
                // In the real world, you would validate the entered credentials and information before 
                // allowing a user to register a new account. 
                // For this sample, we'll skip that step and just register an account if the username is not null.
    
                if (!string.IsNullOrEmpty(UsernameTextBox.Text))
                {
                    // Register a new account
                    _account = AccountHelper.AddAccount(UsernameTextBox.Text);
                    // Register new account with Windows Hello
                    await WindowsHelloHelper.CreateWindowsHelloKeyAsync(_account.Username);
                    // Navigate to the Welcome page. 
                    Frame.Navigate(typeof(Welcome), _account);
                }
                else
                {
                    ErrorMessage.Text = "Please enter a username";
                }
            }
        }
    }
    
  • 등록을 클릭하면 로그인 페이지에서 이 페이지로 이동해야 합니다.

    private void RegisterButtonTextBlock_OnPointerPressed(object sender, PointerRoutedEventArgs e)
    {
        ErrorMessage.Text = "";
        Frame.Navigate(typeof(WindowsHelloRegister));
    }
    
  • 애플리케이션을 빌드 및 실행합니다. 새 사용자를 등록해 보세요. 그런 다음, 사용자 목록으로 돌아가서 해당 사용자가 유효한지 확인하고 로그인 시킵니다.

    Windows Hello 새 사용자 등록 페이지의 스크린샷

이 랩에서는 새 Windows Hello API를 사용하여 기존 사용자를 인증하고 새 사용자에 대한 계정을 만드는 데 필요한 필수 기술을 배웠습니다. 이 새로운 지식을 통해 사용자가 애플리케이션에 대한 암호를 기억할 필요를 제거하지만 애플리케이션이 사용자 인증으로 보호되는 상태를 유지할 수 있습니다. Windows는 Windows Hello의 새로운 인증 기술을 사용하여 생체 인식 로그인 옵션을 지원합니다.