Cutting Edge

Armazenar dados do usuário no ASP.NET Identity

Dino Esposito

Dino EspositoO ASP.NET Identity no Visual Studio 2013 é uma forma de simplificar as tarefas chatas, mas essenciais de gerenciamento de dados de usuário e estabelecimento de um sistema de associação mais efetiva. Anteriormente, eu forneci uma visão geral do API do ASP.NET Identity (msdn.microsoft.com/magazine/dn605872) e examinei seu envolvimento com as redes sociais e o protocolo OAuth (msdn.microsoft.com/magazine/dn745860). Neste artigo, eu expandirei os pontos de extensibilidade do ASP.NET Identity, começando com a representação de dados do usuário e o armazenamento de dados subjacente.

Estabelecer a base

Primeiro, vamos criar um novo projeto ASP.NET MVC em branco no Visual Studio 2013. Todos os padrões do assistente fornecidos estão OK, mas certifique-se de selecionar o modelo de autenticação de usuário único. O código em etapas armazena dados do usuário em um arquivo SQL Server local com um nome gerado automaticamente com a seguinte convenção: aspnet-[ProjectName]-[RandomNumber]. O código também usa o Entity Framework para acessar o banco de dados do usuário na leitura e gravação. A representação dos dados do usuário está na classe ApplicationUser:

public class ApplicationUser : IdentityUser
{
}

Como você pode ver, a classe ApplicationUser herda da classe IdentityUser fornecida pelo sistema. Para personalizar a representação do usuário, vamos começar de aqui e adicione um novo usuário para a classe:

public class ApplicationUser : IdentityUser
{
  public String MagicCode { get; set; }
}

O nome da classe - ApplicationUser neste exemplo - não é obrigatório e você pode mudar, se você quiser. Neste exemplo, eu escolhi deliberadamente o campo estranho “código-mágico” para indicar a possibilidade dupla. Você pode adicionar campos que exigem uma interface de usuário como data de aniversário ou número de segurança social ou qualquer outra coisa que você deseja especificar durante o registro. Você também pode adicionar campos que precisa ter, mas também calcular silenciosamente (por exemplo, um código “mágico” específico do aplicativo) quando o registro do usuário é realmente criado. A classe ApplicationUser inclui, por padrão, os membros listados na Figura 1.

Figura 1 Membros definidos na classe de base IdentityUser

Membro Descrição
Id Identificador autogerada exclusiva (GUID) para a tabela. Este campo é a chave principal.
UserName Exibição do nome do usuário.
PasswordHash Hash resultante da senha fornecida.
SecurityStamp Um GUID criado automaticamente em pontos específicos no tempo de vida do objeto UserManager. Geralmente, é criado e atualizado quando a senha muda ou um logon social é adicionado ou removido. A marcação de segurança geralmente leva um instantâneo de informação de usuário e registra automaticamente nos usuários se nada mudou.
Discriminador Esta coluna é específica para o modelo de persistência do Entity Framework e determina a classe no qual a linha particular pertence. Você terá um valor discriminador exclusivo para cada classe na hierarquia com raiz no IdentityUser.

 

Nos campos listados na Figura 1, bem como em outros campos que você adicionou de forma programática na definição de classe ApplicationUser, acabam armazenados em uma tabela de banco de dados. O nome da tabela padrão é AspNetUsers. Além disso, a classe IdentityUser expõe mais algumas propriedades como Logons, Reclamações e Funções. Essas propriedades não são armazenadas na tabela AspNetUsers, mas localizadas em seu lugar em outras tabelas laterais no mesmo banco de dados—AspNetUserRoles, AspNetUserLogins e AspNetUserClaims (vejaFigura 2).

Estrutura padrão do banco de dados do usuário do ASP.NET Identity
Figura 2 Estrutura padrão do banco de dados do usuário do ASP.NET Identity

Modificar a classe ApplicationUser não garante que você terá os campos adicionais imediatamente refletidos na interface do usuário e salvos no banco de dados. Felizmente, a atualização do banco de dados não tem muito trabalho.

Mudanças ao código em etapas

Você precisará editar as exibições e modelos do aplicativo para refletir os novos campos. Na forma do aplicativo onde os novos usuários se registram no site, você precisará adicionar alguma marcação para mostrar a interface do usuário para o código mágico e quaisquer outros campos extras. AFigura 3 mostra o código alterado para o arquivo CSHTML Razor por trás da forma de registro.

Figura 3 Arquivo Razor para apresentar aos usuários com campos extras

@using (Html.BeginForm("Register", "Account",
  FormMethod.Post, new 
    { @class = "form-horizontal", role = "form" }))
{
  @Html.AntiForgeryToken()
  <h4>Create a new account.</h4>
  <hr />
  @Html.ValidationSummary()
  <div class="form-group">
    @Html.LabelFor(m => m.MagicCode, 
      new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
      @Html.TextBoxFor(m => m.MagicCode, 
        new { @class = "form-control" })
    </div>
  </div>   
  ...
}

A forma do registro CSHTML é baseada em uma classe de modelo de visão, convencionalmente chamada de RegisterViewModel. AFigura 4 mostra as mudanças necessárias à classe RegisterViewModel para conectá-la ao mecanismo de validação clássica da maioria dos aplicativos ASP.NET MVC com base nas anotações dos dados.

Figura 4 Mudanças no Registro de Exibição da Classe do modelo

public class RegisterViewModel
{
  [Required]
  [Display(Name = "User name")]
  public string UserName { get; set; }
  [Required]
  [StringLength(100, ErrorMessage = 
    "The {0} must be at least {1} character long.",
    MinimumLength = 6)]
  [DataType(DataType.Password)]
  [Display(Name = "Password")]
  public string Password { get; set; }
  [DataType(DataType.Password)]
  [Display(Name = "Confirm password")]
  [Compare("Password", ErrorMessage = 
    "Password and confirmation do not match.")]
  public string ConfirmPassword { get; set; }
  [Required]
  [Display(Name = "Internal magic code")]
  public string MagicCode { get; set; }
}

Estas mudanças não são suficientes, no entanto. Existe mais uma etapa necessária e é a mais crítica. Você tem que passar os dados adicionais para a camada que gravará para o armazenamento persistente. Você precisa fazer mais alterações no método do controlador que processa a ação POST da forma de registro:

public async Task<ActionResult> Register(RegisterViewModel model)
{
  if (ModelState.IsValid) {
    var user = new ApplicationUser() { UserName = model.UserName,
      MagicCode = model.MagicCode };
    var result = await UserManager.CreateAsync(user, model.Password);
    if (result.Succeeded) {
      await SignInAsync(user, isPersistent: false);
      return RedirectToAction("Index", "Home");
    }
}

A mudança principal é salvar os dados lançados no código mágico (ou qualquer outra coisa que você deseja adicionar à definição do usuário). Isso é salvo na instância ApplicationUser sendo passado no método CreateAsync do objeto UserManager.

Olhando para o armazenamento persistente

No código de amostra gerado pelo modelo ASP.NET MVC, a classe AccountController tem um membro definido aqui:

public UserManager<ApplicationUser> UserManager { get; private set; }

A classe UserManager é instanciada passando pelo objeto de armazenamento do usuário. O ASP.NET Identity vem com um armazenamento do usuário padrão:

var defaultUserStore = new UserStore<ApplicationUser>(new ApplicationDbContext())

Muito parecido com o ApplicationUser, a classe ApplicationDbContext herda de uma classe definida pelo sistema (nomeado como IdentityDbContext) e envolve o Entity Framework para fazer o trabalho de persistência real. A grande notícia é que você pode desconectar o mecanismo de armazenamento padrão completamente e desenvolver o seu próprio. Você pode basear seu mecanismo de armazenamento personalizado no SQL Server e Entity Framework e apenas use um esquema diferente. Você pode também aproveitar um mecanismo de armazenamento completamente diferente, como MySQL ou uma solução NoSQL. Vamos ver como organizar um armazenamento de usuário com base em uma versão inserida do RavenDB (ravendb.net). O protótipo da classe que você precisará é mostrado aqui:

public class RavenDbUserStore<TUser> :
  IUserStore<TUser>, IUserPasswordStore<TUser>
  where TUser : TypicalUser
{
  ...
}

Se você pretende suportar logons, funções e reclamações, você precisará implementar mais interfaces. Para uma solução de processamento mínima, o IUserStore e IUserPasswordStore são suficientes. A classe Typical­User é uma classe personalizada que eu criei para permanecer separado da infraestrutura do ASP.NET Identity o máximo possível:

public class TypicalUser : IUser
{
  // IUser interface
  public String Id { get; set; }
  public String UserName { get; set; }
  // Other members
  public String Password { get; set; }
  public String MagicCode { get; set; }
}

No mínimo, a classe do usuário deve implementar a interface IUser. A interface conta com dois membros - ID e nome de usuário. É provável que você deseje adicionar um membro de Senha, também. Esta é a classe de usuário sendo salva no arquivo RavenDB.

Adicione o suporte RavenDB para seu projeto através do pacote NuGet do RavenDB.Embedded. No global.asax, você também precisará inicializar o banco de dados com o seguinte código:

private static IDocumentStore _instance;
public static IDocumentStore Initialize()
{
  _instance = new EmbeddableDocumentStore 
    { ConnectionStringName = "RavenDB" };
  _instance.Initialize();
  return _instance;
}

A cadeia de caracteres de conexão aponta para o caminho onde você deve criar o banco de dados. Em um aplicativo ASP.NET na Web, o ajuste natural é uma subpasta em App_Data:

<add name="RavenDB" connectionString="DataDir = ~\App_Data\Ravendb" />

A classe de armazenamento do usuário contém código para os métodos nas interfaces IUserStore e IUserPasswordStore. Estas permitem a aplicação do gerenciamento dos usuários e senhas relacionadas. A Figura 5 mostra a implementação do armazenamento.

Figura 5 Um armazenamento de usuário trabalhando minimamente com base no RavenDB

public class RavenDbUserStore<TUser> :
  IUserStore<TUser>, IUserPasswordStore<TUser>
  where TUser : TypicalUser
{
  private IDocumentSession DocumentSession { get; set; }
  public RavenDbUserStore()
  {
    DocumentSession = RavenDbConfig.Instance.OpenAsyncSession();
  }
  public Task CreateAsync(TUser user)
  {
    if (user == null)
      throw new ArgumentNullException();
    DocumentSession.Store(user);
    return Task.FromResult<Object>(null);
  }
  public Task<TUser> FindByIdAsync(String id)
  {
    if (String.IsNullOrEmpty(id))
      throw new ArgumentException();
    var user = DocumentSession.Load<TUser>(id);
    return Task.FromResult<TUser>(user);
  }
  public Task<TUser> FindByNameAsync(String userName)
  {
    if (string.IsNullOrEmpty(userName))
      throw new ArgumentException("Missing user name");
    var user = DocumentSession.Query<TUser>()
      .FirstOrDefault(u => u.UserName == userName);
    return Task.FromResult<TUser>(user);
  }
  public Task UpdateAsync(TUser user)
  {
    if (user != null)
      DocumentSession.Store(user);
    return Task.FromResult<Object>(null);
  }
  public Task DeleteAsync(TUser user)
  {
    if (user != null)
      DocumentSession.Delete(user);
    return Task.FromResult<Object>(null);
  }
  public void Dispose()
  {
    if (DocumentSession == null)
      return;
    DocumentSession.SaveChanges();
    DocumentSession.Dispose();
  }
  public Task SetPasswordHashAsync(TUser user, String passwordHash)
  {
    user.Password = passwordHash;
    return Task.FromResult<Object>(null);
  }
  public Task<String> GetPasswordHashAsync(TUser user)
  {
    var passwordHash = user.Password;
    return Task.FromResult<string>(passwordHash);
  }
  public Task<Boolean> HasPasswordAsync(TUser user)
  {
    var hasPassword = String.IsNullOrEmpty(user.Password);
    return Task.FromResult<Boolean>(hasPassword);
  }
 }
}

Qualquer interação com o RavenDB passa através da abertura e fechamento de uma sessão de armazenamento de documento. No construtor do RavenDbUserStore, você abre a sessão e descartá-lo no método Dispose do objeto de armazenamento. Antes de ignorar a sessão, no entanto, chame o método SaveChanges para persistir todas as alterações pendentes de acordo com o padrão Unidade de trabalho:

public void Dispose()
{
  if (DocumentSession == null)
    return;
  DocumentSession.SaveChanges();
  DocumentSession.Dispose();
}

O API para funcionar com o banco de dados RavenDB é bastante simples. Aqui está o código que você precisará para criar um novo usuário:

public Task CreateAsync(TUser user)
{
  if (user == null)
    throw new ArgumentNullException();
  DocumentSession.Store(user);
  return Task.FromResult<Object>(null);
}

Para recuperar um determinado usuário, use o método de Consulta no objeto Document­Session:

var user = DocumentSession.Load<TUser>(id);

O RavenDB assume qualquer classe que você persiste que tenha uma propriedade ID. Se não, ele vai criar implicitamente uma propriedade para que você possa sempre usar o método Load para recuperar qualquer objeto por ID, se o seu próprio ID ou um ID gerado pelo sistema. Para recuperar um usuário pelo nome, realize uma consulta clássica usando uma sintaxe LINQ:

var user = DocumentSession
              .Query<TUser>()
              .FirstOrDefault(u => u.UserName == userName);

Use o ToList para selecionar uma variedade de objetos e armazene-os na lista gerenciável. Lidar com senhas é igualmente simples. O RavenDB armazena senhas em um formato de hash, mas o hash é gerenciado fora do módulo RavenDB. O módulo SetPasswordHashAsync, de fato, já recebe o hash da senha que o usuário fornece.

Figura 5 é o código-fonte completo para configurar um armazenamento de usuário RavenDB compatível com o ASP.NET Identity. É suficiente para que os usuários entrem e saiam quando você tiver a versão inserida do RavenDB instalado. Para obter mais recursos sofisticados, como logons externos e gerenciamento de conta, você precisa implementar todas as interfaces relacionadas ao usuário do ASP.NET Identity ou retrabalhar significativamente o código no Controlador de Conta que você obtém das etapas.

Conclusão

O ASP.NET Identity permite que você desconecte completamente a infraestrutura de armazenamento com base no Entity Framework e use o RavenDB, um documento e banco de dados livre de esquema. Você pode instalar o RavenDB como um serviço do Windows, um aplicativo IIS ou incorporado como eu fiz aqui. Basta escrever uma classe que implementa algumas interfaces do ASP.NET Identity e injetar essa nova classe de armazenamento na infraestrutura UserManager. Alterar o esquema de tipo de usuário é fácil, mesmo na configuração padrão com base no EF. Se você usa o RavenDB, porém, você se livra de quaisquer problemas de migração que o formato de usuário deve alterar.


Dino Esposito é o co-autor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) e “Programming ASP.NET MVC 5” (Microsoft Press, 2014). Um evangelista técnico para as plataformas Microsoft .NET Framework e Android na JetBrains e palestrante frequente em eventos do setor em todo mundo, Esposito compartilha sua visão do software em software2cents.wordpress.com e no Twitter em twitter.com/despos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Mauro Servienti