練習 - 自訂身分識別

已完成

在上一個單元,您已瞭解自訂作業在 ASP.NET Core 身分識別中運作的方式。 在此單元中,您會擴充身分識別資料模型,並進行對應的 UI 變更。

自訂使用者帳戶資料

在本節中,您將建立和自訂要使用的身分識別 UI 檔案,而不是預設的 Razor 類別庫。

  1. 將要修改的使用者註冊檔案新增至專案:

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

    在上述命令中:

    • --dbContext 選項會以名為 RazorPagesPizzaAuth 的現有 DbContext 衍生類別的知識提供工具。
    • --files 選項會指定要新增至 [身分識別] 區域的唯一檔案清單 (以分號分隔)。
      • Account.Manage.Index 是設定檔管理頁面。 本單元稍後會修改此頁面。
      • Account.Register 是使用者註冊頁面。 本單元中也會修改此頁面。
      • Account.Manage.EnableAuthenticatorAccount.ConfirmEmail 是 Scaffolded,但在此單元中不會修改。

    提示

    從專案根目錄執行下列命令,以檢視 --files 選項的有效值:dotnet aspnet-codegenerator identity --listFiles

    下列檔案會新增至 Areas/Identity 目錄:

    • 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

擴充 IdentityUser

您已獲得儲存使用者名稱的新需求。 由於預設 IdentityUser 類別不包含名字和姓氏的屬性,因此您需要擴充 RazorPagesPizzaUser 類別。

Areas/Identity/Data/RazorPagesPizzaUser.cs 進行下列變更:

  1. 新增 FirstNameLastName 屬性:

    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Identity;
    
    namespace RazorPagesPizza.Areas.Identity.Data;
    
    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 個字元。 基礎資料表資料行的資料類型會隨之定義。 由於此專案已啟用可為 Null 的內容,且該屬性是不可為 Null 的字串,因此系統會指派 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. 請檢查資料庫來分析 UpdateUser EF Core 移轉對 AspNetUsers 資料表結構描述的影響。

    在 [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" role="alert"></div>
        <div class="form-floating mb-3">
            <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 mb-3">
            <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 mb-3">
            <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
            <label asp-for="Input.Email">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>

UserManager.GetUserAsync(User) 會傳回可為 Null 的 RazorPagesPizzaUser 物件。 只有在物件不是 Null 時RazorPagesPizzaUser,Null 條件?.運算子才會用來存取 FirstNameLastName 屬性。

自訂設定檔管理表單

您已將新欄位新增至使用者註冊表單,但也應該將其新增至設定檔管理表單,讓現有的使用者可以編輯這些欄位。

  1. Areas/Identity/Pages/Account/Manage/Index.cshtml 中,新增下列醒目提示的標記。 儲存您的變更。

    <form id="profile-form" method="post">
        <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
        <div class="form-floating mb-3">
            <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 mb-3">
            <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 mb-3">
            <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; }
      
          /// <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>
          [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 的實作,並在相依性插入系統中註冊。 為了保持簡單,本單元中的實作實際上不會傳送電子郵件到 Simple Mail Transfer Protocol (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. 在 [總管] 窗格中,以滑鼠右鍵按一下 [RazorPagesPizza\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,您可能需要將 -7192 新增至轉送 URL 的第一個部分。 例如: 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
    

    具有類似下列結果的索引標籤隨即出現:

    UserName 電子郵件 FirstName LastName
    kai.klein@contoso.com kai.klein@contoso.com
    jana.heinrich@contoso.com jana.heinrich@contoso.com Jana Heinrich

    新增和 LastName 至架構之前FirstName註冊的第一個使用者。 因此,相關聯的 AspNetUsers 資料表記錄在這些資料行中不會有資料。

測試設定檔管理表單的變更

您也應該測試設定檔管理表單的變更內容。

  1. 在 Web 應用程式中,使用您所建立的第一個使用者登入。

  2. 選取 [Hello, !] 連結,瀏覽至設定檔管理表單。

    注意

    此連結未正確顯示,因為此使用者的 AspNetUsers 資料表資料列不包含 FirstNameLastName 的值。

  3. 針對 [名字] 與 [姓氏] 輸入有效的值。 選取 [儲存]。

    應用程式的標頭會更新為 Hello, [First name] [Last name]!

  4. 若要停止應用程式,請在 VS Code 的終端機窗格中按 Ctrl+C。

摘要

在此單元中,您已自訂身分識別來儲存自訂使用者資訊。 同時也自訂確認電子郵件。 在下一個單元中,您將了解如何在身分識別中實作多重要素驗證。