연습 - ID 사용자 지정

완료됨

이전 단원에서는 ASP.NET Core ID에서 사용자 지정이 작동하는 방법을 알아보았습니다. 이 단원에서는 ID 데이터 모델을 확장하고 해당 UI를 변경합니다.

사용자 계정 데이터 사용자 지정

이 섹션에서는 기본 Razor 클래스 라이브러리 대신 사용할 ID UI 파일을 만들고 사용자 지정합니다.

  1. 수정할 사용자 등록 파일을 프로젝트에 추가합니다.

    dotnet aspnet-codegenerator identity --dbContext RazorPagesPizzaAuth --files "Account.Manage.EnableAuthenticator;Account.Manage.Index;Account.Register;Account.ConfirmEmail" --userClass RazorPagesPizzaUser --force
    

    이전 명령에서 다음을 확인할 수 있습니다.

    • --dbContext 옵션은 RazorPagesPizzaAuth이라는 기존 DbContext 파생 클래스에 대한 정보를 도구에 제공합니다.
    • --files 옵션은 ID 영역에 추가할 고유 파일의 세미콜론으로 구분된 목록을 지정합니다.
    • --userClass 옵션을 선택하면 IdentityUser라는 RazorPagesPizzaUser 파생 클래스가 생성됩니다.
    • --force 옵션을 선택하면 Identity 영역에 있는 기존 파일을 덮어씁니다.

    프로젝트 루트에서 다음 명령을 실행하여 --files 옵션에 대한 유효한 값을 확인합니다. dotnet aspnet-codegenerator identity --listFiles

    Areas/Identity 디렉터리에 다음 파일이 추가됩니다.

    • Data/
      • RazorPagesPizzaUser.cs
    • Pages/
      • _ViewImports.cshtml
      • Account/
        • _ViewImports.cshtml
        • ConfirmEmail.cshtml
        • ConfirmEmail.cshtml.cs
        • Register.cshtml
        • Register.cshtml.cs
        • Manage/
          • _ManageNav.cshtml
          • _ViewImports.cshtml
          • EnableAuthenticator.cshtml
          • EnableAuthenticator.cshtml.cs
          • Index.cshtml
          • Index.cshtml.cs
          • ManageNavPages.cs

    또한 --force 옵션을 사용했기 때문에 이전 명령을 실행하기 전에 있던 Data/RazorPagesPizzaAuth.cs 파일이 덮어쓰여졌습니다. RazorPagesPizzaAuth 클래스 선언은 이제 새로 만든 사용자 유형 RazorPagesPizzaUser를 참조합니다.

    public class RazorPagesPizzaAuth : IdentityDbContext<RazorPagesPizzaUser>
    

    EnableAuthenticatorConfirmEmail Razor 페이지는 스캐폴드되었지만 이 모듈의 뒷부분을 진행할 때까지 수정되지 않을 것입니다.

  2. Program.cs에서는 새 ID 사용자 유형을 인식하기 위해 AddDefaultIdentity를 호출해야 합니다. 강조 표시된 다음 변경 내용을 통합합니다. (가독성을 위해 다시 포맷된 예제)

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using RazorPagesPizza.Areas.Identity.Data;
    
    var builder = WebApplication.CreateBuilder(args);
    var connectionString = builder.Configuration.GetConnectionString("RazorPagesPizzaAuthConnection");
    builder.Services.AddDbContext<RazorPagesPizzaAuth>(options => options.UseSqlServer(connectionString)); 
    builder.Services.AddDefaultIdentity<RazorPagesPizzaUser>(options => options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<RazorPagesPizzaAuth>();
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    
  3. 상단에 강조 표시된 다음 변경 내용을 통합하도록 업데이트 Pages/Shared/_LoginPartial.cshtml을 업데이트합니다. 변경 내용을 저장합니다.

    @using Microsoft.AspNetCore.Identity
    @using RazorPagesPizza.Areas.Identity.Data
    @inject SignInManager<RazorPagesPizzaUser> SignInManager
    @inject UserManager<RazorPagesPizzaUser> UserManager
    
    <ul class="navbar-nav">
    

    위와 같이 변경하면 @inject 지시문에서 SignInManager<T>UserManager<T> 둘 다에 전달된 사용자 유형이 업데이트됩니다. 이제 기본 IdentityUser 유형 대신, RazorPagesPizzaUser 사용자가 참조됩니다. RazorPagesPizzaUser 참조를 확인하기 위해 @using 지시문을 추가했습니다.

    Pages/Shared/_LoginPartial.cshtml은 물리적으로 Identity 영역 외부에 있습니다. 따라서 파일은 스캐폴드 도구에 의해 자동으로 업데이트되지 않았습니다. 수동으로 적절히 변경해야 합니다.

    _LoginPartial.cshtml 파일을 수동으로 편집하는 대신, 스캐폴드 도구를 실행하기 전에 파일을 삭제할 수 있습니다. _LoginPartial.cshtml 파일이 새 RazorPagesPizzaUser 클래스에 대한 참조를 사용하여 다시 생성됩니다.

  4. 추가 사용자 프로필 데이터의 저장 및 검색을 지원하도록 Areas/Identity/Data/RazorPagesPizzaUser.cs를 업데이트합니다. 다음과 같이 변경합니다.

    1. FirstNameLastName 속성을 추가합니다.

      public class RazorPagesPizzaUser : IdentityUser
      {
          [Required]
          [MaxLength(100)]
          public string FirstName { get; set; } = string.Empty;
      
          [Required]
          [MaxLength(100)]
          public string LastName { get; set; } = string.Empty;
      }
      

      이전 코드 조각의 속성은 기본 AspNetUsers 테이블에 만들 추가 열을 나타냅니다. 두 속성이 모두 필요하므로 [Required] 특성을 주석으로 추가합니다. 그뿐 아니라 [MaxLength] 특성은 최대 100자 길이가 허용됨을 나타냅니다. 기본 테이블 열의 데이터 형식은 그에 따라 정의됩니다. 이 프로젝트에서 nullable 컨텍스트가 활성화되고 속성이 nullable이 아닌 문자열이므로 기본값 string.Empty이 할당됩니다.

    2. 다음 using을 파일의 맨 위에 추가합니다.

      using System.ComponentModel.DataAnnotations;
      

      위의 코드는 FirstNameLastName 속성에 적용된 데이터 주석 특성을 확인합니다.

데이터베이스 업데이트

이제 모델을 변경했으므로 데이터베이스도 함께 변경해야 합니다.

  1. 모든 변경 내용이 저장되었는지 확인합니다.

  2. EF Core 마이그레이션을 만들고 적용하여 기본 데이터 저장소를 업데이트합니다.

    dotnet ef migrations add UpdateUser
    dotnet ef database update
    

    UpdateUser EF Core 마이그레이션은 DDL 변경 스크립트를 AspNetUsers 테이블의 스키마에 적용했습니다. 특히, 다음 마이그레이션 출력과 같이 FirstNameLastName 열이 추가되었습니다.

    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (37ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [FirstName] nvarchar(100) NOT NULL DEFAULT N'';
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (36ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [LastName] nvarchar(100) NOT NULL DEFAULT N'';
    
  3. 데이터베이스를 검사하여 AspNetUsers 테이블의 스키마에 UpdateUser EF Core 마이그레이션이 미치는 영향을 분석합니다.

    SQL Server 창의 dbo.AspNetUsers 테이블에서 노드를 확장합니다

    AspNetUsers 테이블의 스키마를 보여주는 스크린샷.

    RazorPagesPizzaUser 클래스의 FirstNameLastName 속성은 앞의 이미지에서 FirstNameLastName 열에 해당합니다. [MaxLength(100)] 특성으로 인해 nvarchar(100)의 데이터 형식이 두 열 각각에 할당되었습니다. null이 아닌 제약 조건이 추가된 이유는 클래스에서 FirstNameLastName이(가) null을 허용하지 않는 문자열이기 때문입니다. 기존 행은 새 열에 빈 문자열을 표시합니다.

사용자 등록 양식 사용자 지정

FirstNameLastName에 대한 새 열을 추가했습니다. 이제 등록 양식에 일치하는 필드를 표시하도록 UI를 편집해야 합니다.

  1. Areas/Identity/Pages/Account/Register.cshtml에서 강조 표시된 다음 태그를 추가합니다.

    <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
        <h2>Create a new account.</h2>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-floating">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
            <label asp-for="Input.Email"></label>
            <span asp-validation-for="Input.Email" class="text-danger"></span>
        </div>
    

    위의 태그를 사용하여 이름 텍스트 상자를 사용자 등록 양식에 추가합니다.

  2. Areas/Identity/Pages/Account/Register.cshtml.cs에서 이름 텍스트 상자에 대한 지원을 추가합니다.

    1. FirstNameLastName 속성을 InputModel 중첩 클래스에 추가합니다.

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          /// <summary>
          ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
          ///     directly from your code. This API may change or be removed in future releases.
          /// </summary>
          [Required]
          [EmailAddress]
          [Display(Name = "Email")]
          public string Email { get; set; }
      

      [Display] 특성은 텍스트 상자와 연결할 레이블 텍스트를 정의합니다.

    2. OnPostAsync 메서드를 수정하여 RazorPagesPizza 개체에 대해 FirstNameLastName 속성을 설정합니다. 강조 표시된 다음 줄을 추가합니다.

      public async Task<IActionResult> OnPostAsync(string returnUrl = null)
      {
          returnUrl ??= Url.Content("~/");
          ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
          if (ModelState.IsValid)
          {
              var user = CreateUser();
      
              user.FirstName = Input.FirstName;
              user.LastName = Input.LastName;
              
              await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
              await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
              var result = await _userManager.CreateAsync(user, Input.Password);
      
      

      위의 변경 내용은 FirstNameLastName 속성을 등록 양식의 사용자 입력으로 설정합니다.

사이트 헤더 사용자 지정

사용자 등록 중에 수집된 성과 이름을 표시하도록 Pages/Shared/_LoginPartial.cshtml을 업데이트합니다. 다음 코드 조각의 강조 표시된 줄이 필요합니다.

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    RazorPagesPizzaUser user = await UserManager.GetUserAsync(User);
    var fullName = $"{user.FirstName} {user.LastName}";

    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello, @fullName!</a>
    </li>

프로필 관리 양식 사용자 지정

새 필드를 사용자 등록 양식에 추가했지만 기존 사용자가 편집할 수 있도록 프로필 관리 양식에도 추가해야 합니다.

  1. Areas/Identity/Pages/Account/Manage/Index.cshtml에서 강조 표시된 다음 태그를 추가합니다. 변경 내용을 저장합니다.

    <form id="profile-form" method="post">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-floating">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating">
            <input asp-for="Username" class="form-control" disabled />
            <label asp-for="Username" class="form-label"></label>
        </div>
    
  2. Areas/Identity/Pages/Account/Manage/Index.cshtml.cs에서 이름 텍스트 상자를 지원하도록 다음과 같이 변경합니다.

    1. FirstNameLastName 속성을 InputModel 중첩 클래스에 추가합니다.

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          [Phone]
          [Display(Name = "Phone number")]
          public string PhoneNumber { get; set; }
      }
      
    2. 강조 표시된 변경 내용을 LoadAsync 메서드에 통합합니다.

      private async Task LoadAsync(RazorPagesPizzaUser user)
      {
          var userName = await _userManager.GetUserNameAsync(user);
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
      
          Username = userName;
      
          Input = new InputModel
          {
              PhoneNumber = phoneNumber,
              FirstName = user.FirstName,
              LastName = user.LastName
          };
      }
      

      위의 코드는 프로필 관리 양식의 해당 텍스트 상자에 표시할 이름과 성을 검색할 수 있도록 지원합니다.

    3. 강조 표시된 변경 내용을 OnPostAsync 메서드에 통합합니다. 변경 내용을 저장합니다.

      public async Task<IActionResult> OnPostAsync()
      {
          var user = await _userManager.GetUserAsync(User);
          if (user == null)
          {
              return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
          }
      
          if (!ModelState.IsValid)
          {
              await LoadAsync(user);
              return Page();
          }
      
          user.FirstName = Input.FirstName;
          user.LastName = Input.LastName;
          await _userManager.UpdateAsync(user);
      
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
          if (Input.PhoneNumber != phoneNumber)
          {
              var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
              if (!setPhoneResult.Succeeded)
              {
                  StatusMessage = "Unexpected error when trying to set phone number.";
                  return RedirectToPage();
              }
          }
      
          await _signInManager.RefreshSignInAsync(user);
          StatusMessage = "Your profile has been updated";
          return RedirectToPage();
      }
      

      위의 코드는 데이터베이스의 AspNetUsers 테이블에서 이름과 성을 업데이트하도록 지원합니다.

확인 메일 보낸 사람 구성

확인 메일을 보내려면 종속성 주입 시스템에 IEmailSender의 구현을 만들고 등록해야 합니다. 작업을 간단하게 유지하기 위해 구현에서 실제로 SMTP 서버로 메일을 보내지 않습니다. 콘솔에 메일 콘텐츠만 작성합니다.

  1. 콘솔에서 메일을 일반 텍스트로 보려고 하므로 HTML로 인코딩된 텍스트를 제외하도록 생성된 메시지를 변경해야 합니다. Areas/Identity/Pages/Account/Register.cshtml.cs에서 다음 코드를 찾습니다.

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
    

    다음으로 변경합니다.

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by visiting the following URL:\r\n\r\n{callbackUrl}");
    
  2. 탐색기 창에서 Services 폴더를 마우스 오른쪽 단추로 클릭하고 EmailSender.cs라는 새 파일을 만듭니다. 파일을 열고 다음 코드를 추가합니다.

    using Microsoft.AspNetCore.Identity.UI.Services;
    namespace RazorPagesPizza.Services;
    
    public class EmailSender : IEmailSender
    {
        public EmailSender() {}
    
        public Task SendEmailAsync(string email, string subject, string htmlMessage)
        {
            Console.WriteLine();
            Console.WriteLine("Email Confirmation Message");
            Console.WriteLine("--------------------------");
            Console.WriteLine($"TO: {email}");
            Console.WriteLine($"SUBJECT: {subject}");
            Console.WriteLine($"CONTENTS: {htmlMessage}");
            Console.WriteLine();
    
            return Task.CompletedTask;
        }
    }
    

    위의 코드는 메시지 내용을 콘솔에 쓰는 IEmailSender의 구현을 만듭니다. 실제 구현에서는 SendEmailAsync에서 외부 메일 서비스 또는 다른 작업에 연결하여 메일을 보냅니다.

  3. Program.cs에서 다음과 같이 강조 표시된 줄을 추가합니다.

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using RazorPagesPizza.Areas.Identity.Data;
    using Microsoft.AspNetCore.Identity.UI.Services;
    using RazorPagesPizza.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    var connectionString = builder.Configuration.GetConnectionString("RazorPagesPizzaAuthConnection");
    builder.Services.AddDbContext<RazorPagesPizzaAuth>(options => options.UseSqlServer(connectionString)); 
    builder.Services.AddDefaultIdentity<RazorPagesPizzaUser>(options => options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<RazorPagesPizzaAuth>();
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    builder.Services.AddTransient<IEmailSender, EmailSender>();
    
    var app = builder.Build();
    

    위의 코드는 EmailSender를 종속성 삽입 시스템에 IEmailSender(으)로 등록합니다.

등록 양식 변경 내용을 테스트합니다.

이게 전부입니다! 등록 양식 및 확인 메일의 변경 내용을 테스트해 보겠습니다.

  1. 모든 변경 내용을 저장했는지 확인합니다.

  2. 터미널 창에서 dotnet run을(를) 사용하여 프로젝트를 빌드하고 앱을 실행합니다.

  3. 브라우저에서 앱으로 이동합니다. 로그인되어 있으면 로그아웃을 선택합니다.

  4. 등록을 선택하고 업데이트된 양식을 사용하여 새 사용자를 등록합니다.

    참고

    이름 필드에 대한 유효성 검사 제약 조건에는 InputModelFirstNameLastName 속성에 대한 데이터 주석이 반영됩니다.

  5. 등록한 후에는 등록 확인 화면으로 리디렉션됩니다. 터미널 창에서 위로 스크롤하여 다음과 유사한 콘솔 출력을 찾습니다.

    Email Confirmation Message
    --------------------------
    TO: jana.heinrich@contoso.com
    SUBJECT: Confirm your email
    CONTENTS: Please confirm your account by visiting the following URL:
    
    https://localhost:7192/Identity/Account/ConfirmEmail?<query string removed>
    

    Ctrl+클릭을 사용하여 URL로 이동합니다. 확인 화면이 표시됩니다.

    참고

    GitHub Codespaces를 사용하는 경우 전달된 URL의 첫 번째 부분에 -7192를 추가해야 할 수 있습니다. 예들 들어 scaling-potato-5gr4j4-7192.preview.app.github.dev입니다.

  6. 로그인을 선택하고 새 사용자로 로그인합니다. 이제 앱의 헤더에는 Hello, [First name] [Last name]!이 포함됩니다.

  7. VS Code SQL Server 창에서 RazorPagesPizza 데이터베이스를 마우스 오른쪽 단추로 클릭하고 새 쿼리를 선택합니다. 표시되는 탭에서 다음 쿼리를 입력하고 Ctrl+Shift+E를 눌러 실행합니다.

    SELECT UserName, Email, FirstName, LastName
    FROM dbo.AspNetUsers
    

    다음과 유사한 결과가 있는 탭이 나타납니다.

    사용자 이름 Email FirstName LastName
    kai.klein@contoso.com kai.klein@contoso.com
    jana.heinrich@contoso.com jana.heinrich@contoso.com Jana Heinrich

    첫 번째 사용자는 스키마에 FirstNameLastName을 추가하기 전에 등록되었습니다. 따라서 연결된 AspNetUsers 테이블 레코드의 해당 열에는 데이터가 들어 있지 않습니다.

프로필 관리 양식에 대한 변경 내용 테스트

프로필 관리 양식에 대한 변경 내용도 테스트해야 합니다.

  1. 웹앱에서 만든 첫 번째 사용자로 로그인합니다.

  2. Hello, ! 링크를 선택하여 프로필 관리 양식으로 이동합니다.

    참고

    이 사용자에 대한 AspNetUsers 테이블 행에 FirstNameLastName의 값이 포함되어 있지 않으므로 링크가 올바르게 표시되지 않습니다.

  3. 이름의 유효한 값을 입력합니다. 저장을 선택합니다.

    이제 앱의 헤더가 Hello, [First name] [Last name]!으로 업데이트됩니다.

  4. VS Code 터미널 창에서 Ctrl+C를 눌러 앱을 중지합니다.

요약

이 단원에서는 사용자 지정 사용자 정보를 저장하도록 ID를 사용자 지정했습니다. 확인 메일도 사용자 지정했습니다. 다음 단원에서는 ID에서 다단계 인증을 구현하는 방법을 알아봅니다.