Exercício – Personalizar Identidade

Concluído

Na unidade anterior, aprendeu como funciona a personalização no ASP.NET Core Identity. Nesta unidade, vai expandir o modelo de dados identidade e efetuar as alterações de IU correspondentes.

Personalizar os dados da conta de utilizador

Nesta secção, vai criar e personalizar os ficheiros de IU de Identidade a utilizar em vez da Biblioteca de Classes do Razor predefinida.

  1. Adicione os ficheiros de registo de utilizador a serem modificados no projeto:

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

    No comando anterior:

    • A opção --dbContext dá à ferramenta conhecimentos da classe derivada de DbContext com o nome RazorPagesPizzaAuth.
    • A opção --files especifica uma lista delimitada por pontos e vírgulas de ficheiros únicos a adicionar à área Identity (Identidade).
    • A opção --userClass resulta na criação de uma classe derivada de IdentityUser com o nome RazorPagesPizzaUser.
    • A --force opção faz com que os ficheiros existentes na área Identidade sejam substituídos.

    Dica

    Execute o seguinte comando a partir da raiz do projeto para ver valores válidos para a opção --files : dotnet aspnet-codegenerator identity --listFiles

    Os seguintes ficheiros são adicionados ao diretório 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

    Além disso, o ficheiro Data/RazorPagesPizzaAuth.cs, que já existia antes de executar o comando anterior, foi substituído porque foi utilizada a opção --force. A declaração de classe RazorPagesPizzaAuth referencia agora o tipo de utilizador recentemente criado de RazorPagesPizzaUser:

    public class RazorPagesPizzaAuth : IdentityDbContext<RazorPagesPizzaUser>
    

    As EnableAuthenticator páginas e ConfirmEmail do Razor foram estruturadas, embora só sejam modificadas mais tarde no módulo.

  2. No Program.cs, a chamada para AddDefaultIdentity tem de ser informada do novo tipo de utilizador Identidade. Incorpore as seguintes alterações realçadas. (Exemplo reformatado para legibilidade.)

    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. Atualize Pages/Shared/_LoginPartial.cshtml para incorporar as seguintes alterações realçadas na parte superior. Guarde as alterações.

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

    As alterações anteriores atualizam o tipo de utilizador transmitido para SignInManager<T> e UserManager<T> nas diretivas @inject. Em vez do tipo predefinido IdentityUser, é agora referenciado o utilizador RazorPagesPizzaUser. A diretiva @using foi adicionada para resolver as referências RazorPagesPizzaUser.

    Pages/Shared/_LoginPartial.cshtml situa-se fisicamente fora da área Identity. Assim, o ficheiro não foi atualizado automaticamente pela ferramenta de estrutura. As alterações adequadas têm de ser feitas manualmente.

    Dica

    Como alternativa à edição manual do ficheiro _LoginPartial.cshtml, este pode ser eliminado antes de executar a ferramenta de estrutura. O _LoginPartial.cshtml ficheiro é recriado com referências à nova RazorPagesPizzaUser classe.

  4. Atualize Areas/Identity/Data/RazorPagesPizzaUser.cs para suportar o armazenamento e a obtenção de dados adicionais de perfis de utilizadores. Efetue as seguintes alterações:

    1. Adicione as propriedades FirstName e LastName:

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

      As propriedades no fragmento anterior representam colunas adicionais a criar na tabela subjacente AspNetUsers. Ambas as propriedades são necessárias e, portanto, são anotadas com o atributo [Required]. Além disso, o atributo [MaxLength] indica que é permitido um comprimento máximo de 100 carateres. O tipo de dados da coluna da tabela subjacente é definido em conformidade. Um valor predefinido de string.Empty é atribuído, uma vez que o contexto nulo está ativado neste projeto e as propriedades são cadeias não anuláveis.

    2. Adicione a seguinte instrução using na parte superior do ficheiro.

      using System.ComponentModel.DataAnnotations;
      

      O código anterior resolve os atributos de anotação de dados aplicados às propriedades FirstName e LastName.

Atualizar a base de dados

Agora que as alterações ao modelo foram efetuadas, as alterações que acompanham têm de ser efetuadas na base de dados.

  1. Certifique-se de que todas as suas alterações são guardadas.

  2. Crie e aplique uma migração EF Core para atualizar o arquivo de dados subjacente:

    dotnet ef migrations add UpdateUser
    dotnet ef database update
    

    A migração EF Core UpdateUser aplicou um script de alterações DDL ao esquema da tabela AspNetUsers. Especificamente, foram adicionadas as colunas FirstName e LastName, conforme visto no seguinte excerto de saída de migração:

    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. Examine a base de dados para analisar o efeito da migração do UpdateUser EF Core no AspNetUsers esquema da tabela.

    No painel SQL Server, expanda o nó Colunas no dbo. Tabela AspNetUsers.

    Captura de ecrã do esquema da tabela AspNetUsers.

    As FirstName propriedades e LastName na RazorPagesPizzaUser classe correspondem às FirstName colunas e LastName na imagem anterior. Um tipo de dados de nvarchar(100) foi atribuído a cada uma das duas colunas devido aos atributos [MaxLength(100)]. A restrição não nula foi adicionada porque FirstName e LastName na classe são cadeias não anuláveis. As linhas existentes mostram cadeias vazias nas novas colunas.

Personalizar o formulário de registo do utilizador

Adicionou novas colunas para FirstName e LastName. Agora, tem de editar a IU para apresentar campos correspondentes no formulário de registo.

  1. Em Areas/Identity/Pages/Account/Register.cshtml, adicione a seguinte markup realçada:

    <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>
    

    Com a anterior markup, as caixas de texto Nome próprio e Apelido são adicionadas ao formulário de registo de utilizador.

  2. Em Areas/Identity/Pages/Account/Register.cshtml.cs, adicione suporte para caixas de texto de nome.

    1. Adicione as propriedades FirstName e LastName à classe aninhada 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; }
      

      Os atributos [Display] definem o texto da etiqueta como estando associado às caixas de texto.

    2. Modifique o método OnPostAsync para definir as propriedades FirstName e LastName no objeto RazorPagesPizza. Adicione as seguintes linhas realçadas:

      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);
      
      

      A alteração anterior define as propriedades FirstName e LastName para a entrada de utilizador do formulário de registo.

Personalizar o cabeçalho do site

Atualize Pages/Shared/_LoginPartial.cshtml para mostrar o nome próprio e apelido recolhidos durante o registo do utilizador. As linhas realçadas no fragmento seguinte são necessárias:

<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>

Personalizar o formulário de gestão de perfil

Adicionou os novos campos ao formulário de registo do utilizador, mas também deve adicioná-los ao formulário de gestão de perfis para que os utilizadores existentes os possam editar.

  1. Em Areas/Identity/Pages/Account/Manage/Index.cshtml, adicione a seguinte markup realçada. Guarde as alterações.

    <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. Em Areas/Identity/Pages/Account/Manage/Index.cshtml.cs, faça as seguintes alterações para suportar as caixas de texto de nome.

    1. Adicione as propriedades FirstName e LastName à classe aninhada 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. Incorpore as alterações realçadas no método 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
          };
      }
      

      O código anterior suporta a obtenção de nomes próprios e apelidos para apresentação nas caixas de texto correspondentes do formulário de gestão de perfil.

    3. Incorpore as alterações realçadas no método OnPostAsync. Guarde as alterações.

      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();
      }
      

      O código anterior suporta a atualização dos primeiros nomes e apelidos na tabela AspNetUsers da base de dados.

Configurar o remetente de e-mail de confirmação

Para enviar o e-mail de confirmação, tem de criar uma implementação e IEmailSender registá-la no sistema de injeção de dependências. Para manter as coisas simples, a implementação não envia e-mails para um servidor SMTP. Apenas escreve o conteúdo do e-mail na consola do .

  1. Uma vez que vai ver o e-mail em texto simples na consola, deve alterar a mensagem gerada para excluir texto codificado por HTML. Em Áreas/Identidade/Páginas/Conta/Register.cshtml.cs, localize o seguinte código:

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

    Altere-o para:

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by visiting the following URL:\r\n\r\n{callbackUrl}");
    
  2. No painel Explorador , clique com o botão direito do rato na pasta Serviços e crie um novo ficheiro com o nome EmailSender.cs. Abra o ficheiro e adicione o seguinte código:

    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;
        }
    }
    

    O código anterior cria uma implementação que IEmailSender escreve o conteúdo da mensagem na consola do . Numa implementação real, SendEmailAsync ligar-se-ia a um serviço de correio externo ou a qualquer outra ação para enviar e-mail.

  3. Em Program.cs, adicione as linhas realçadas:

    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();
    

    O registo anterior é EmailSender registado como um IEmailSender no sistema de injeção de dependências.

Testar as alterações ao formulário de registo

É tudo! Vamos testar as alterações ao formulário de registo e ao e-mail de confirmação.

  1. Certifique-se de que guardou todas as suas alterações.

  2. No painel de terminal, crie o projeto e execute a aplicação com dotnet run.

  3. No seu browser, navegue para a aplicação. Selecione Logout (Terminar sessão) se ainda estiver ligado.

  4. Selecione Register (Registar) e utilize o formulário atualizado para registar um novo utilizador.

    Nota

    As restrições de validação nos campos First name (Nome próprio) e Last name (Apelido) refletem as anotações de dados nas propriedades FirstName e LastName de InputModel.

  5. Depois de se registar, é redirecionado para o ecrã de confirmação Registar . No painel de terminal, desloque-se para cima para encontrar o resultado da consola semelhante ao seguinte:

    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>
    

    Navegue para o URL com Ctrl+clique. O ecrã de confirmação é apresentado.

    Nota

    Se estiver a utilizar o GitHub Codespaces, poderá ter de adicionar -7192 à primeira parte do URL reencaminhado. Por exemplo, scaling-potato-5gr4j4-7192.preview.app.github.dev.

  6. Selecione Iniciar sessão e inicie sessão com o novo utilizador. Agora, o cabeçalho da aplicação contém Hello, [Nome próprio] [Apelido]!.

  7. No painel SQL Server no VS Code, clique com o botão direito do rato na base de dados RazorPagesPizza e selecione Nova consulta. No separador apresentado, introduza a seguinte consulta e prima Ctrl+Shift+E para executá-la.

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

    É apresentado um separador com resultados semelhantes aos seguintes:

    Nome de Utilizador E-mail FirstName LastName
    kai.klein@contoso.com kai.klein@contoso.com
    jana.heinrich@contoso.com jana.heinrich@contoso.com Jana Heinrich

    O primeiro utilizador registado antes de adicionar FirstName e LastName ao esquema. Assim, o registo de tabela associado AspNetUsers não tem dados nessas colunas.

Testar as alterações ao formulário de gestão de perfil

Também deve testar as alterações efetuadas ao formulário de gestão de perfis.

  1. Na aplicação Web, inicie sessão com o primeiro utilizador que criou.

  2. Selecione a ligação Olá, ! para navegar para o formulário de gestão de perfis.

    Nota

    A ligação não é apresentada corretamente porque a linha da tabela AspNetUsers correspondente a este utilizador não contém valores para FirstName e LastName.

  3. Introduza valores válidos para Nome próprio e Apelido. Selecione Save (Guardar).

    O cabeçalho da aplicação é atualizado para Hello, [Nome próprio] [Apelido]!.

  4. Prima Ctrl+C no painel de terminal no VS Code para parar a aplicação.

Resumo

Nesta unidade, personalizou a Identidade para armazenar informações de utilizador personalizadas. Também personalizou o e-mail de confirmação. Na próxima unidade, irá aprender a implementar a autenticação multifator na Identidade.