练习 - 自定义标识

已完成

在上一单元中,你了解了自定义在 ASP.NET Core Identity 中的工作原理。 在本单元中,你将扩展标识数据模型并进行相应的 UI 更改。

自定义用户帐户数据

在本部分中,你将创建和自定义用于代替默认 Razor 类库的 Identity 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 选项指定要添加到 Identity 区域的以分号分隔的唯一文件列表。
    • --userClass 选项将创建名为 RazorPagesPizzaUserIdentityUser 派生类。
    • --force 选项将覆盖标识区域中的现有文件。

    提示

    在项目根目录中,运行以下命令以查看 --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 中,需要知道新的标识用户类型才能对 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> 的用户类型。 现在引用 RazorPagesPizzaUser 用户,而非默认的 IdentityUser 类型。 添加了 @using 指令以解析 RazorPagesPizzaUser 引用。

    Pages/Shared/_LoginPartial.cshtml 实际位于 Identity 区域之外。 因此,基架工具不会自动更新该文件。 必须手动进行相应更改。

    提示

    除手动编辑 _LoginPartial.cshtml 文件外,还可以在运行基架工具之前将其删除。 通过对新的 RazorPagesPizzaUser 类的引用重新创建 _LoginPartial.cshtml 文件。

  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 个字符。 相应地定义基础表列的数据类型。 分配默认值 string.Empty,因为在此项目中启用了可为 null 的上下文,并且属性是不可为 null 的字符串。

    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"></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. 在“资源管理器”窗格中,右键单击“服务”文件夹并创建名为 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, [姓] [名]!”。

  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

    第一个用户是在将 FirstNameLastName 添加到架构之前注册的。 因此,关联的 AspNetUsers 表记录在这些列中没有数据。

测试对配置文件管理表单的更改

你还应测试对配置文件管理表单所做的更改。

  1. 在 Web 应用中,使用创建的第一个用户登录。

  2. 选择“Hello, !”链接,导航到配置文件管理表单。

    备注

    该链接无法正确显示,因为此用户的 AspNetUsers 表的行不包含 FirstNameLastName 的值。

  3. 对于“名”和“姓”,请输入有效值。 选择“保存”。

    应用标头更新为“Hello, [姓] [名]!”。

  4. 在 VS Code 的终端窗格中,按 Ctrl+C 来停止应用。

总结

在本单元中,你自定义了标识来存储自定义用户信息。 还自定义了确认电子邮件。 在下一单元中,你将了解如何在标识中实现多重身份验证。