Provedores de armazenamento personalizados para o ASP.NET Core Identity

Por Steve Smith

O AsP.NET Core Identity é um sistema extensível que permite criar um provedor de armazenamento personalizado e conectá-lo ao seu aplicativo. Este tópico descreve como criar um provedor de armazenamento personalizado para o ASP.NET Core Identity. Ele aborda os conceitos importantes para criar seu próprio provedor de armazenamento, mas não é um passo a passo. Confira Identity personalização de modelo para personalizar um modelo Identity.

Introdução

Por padrão, o sistema ASP.NET Core Identity armazena informações do usuário em um banco de dados do SQL Server usando o Entity Framework Core. Para muitos aplicativos, essa abordagem funciona bem. No entanto, talvez você prefira usar um mecanismo de persistência ou esquema de dados diferente. Por exemplo:

  • Você usa o Armazenamento de Tabelas do Azure ou outro armazenamento de dados.
  • Suas tabelas de banco de dados têm uma estrutura diferente.
  • Talvez você queira usar uma abordagem de acesso a dados diferente, como o Dapper.

Em cada um desses casos, você pode escrever um provedor personalizado para seu mecanismo de armazenamento e conectar esse provedor ao seu aplicativo.

O AsP.NET Core Identity é incluído em modelos de projeto no Visual Studio com a opção “Contas de Usuário Individuais”.

Ao usar a CLI do .NET Core, adicione -au Individual:

dotnet new mvc -au Individual

A arquitetura do ASP.NET Core Identity

O ASP.NET Core Identity consiste em classes chamadas gerentes e repositórios. Gerentes são classes de alto nível que um desenvolvedor de aplicativos usa para executar operações, como a criação de um usuário Identity. Repositórios são classes de nível inferior que especificam como entidades, como usuários e funções, são mantidas. Os repositórios seguem o padrão de repositório e estão diretamente associados ao mecanismo de persistência. Os gerentes são separados dos repositórios, o que significa que você pode substituir o mecanismo de persistência sem alterar o código do aplicativo (exceto a configuração).

O diagrama a seguir mostra como um aplicativo Web interage com os gerentes, enquanto os repositórios interagem com a camada de acesso a dados.

ASP.NET Core Apps work with Managers (for example, UserManager, RoleManager). Managers work with Stores (for example, UserStore) which communicate with a Data Source using a library like Entity Framework Core.

Para criar um provedor de armazenamento personalizado, crie a fonte de dados, a camada de acesso a dados e as classes de repositório que interagem com essa camada de acesso a dados (as caixas verde e cinza no diagrama acima). Você não precisa personalizar os gerentes ou o código do aplicativo que interage com eles (as caixas azuis acima).

Ao criar uma nova instância do UserManager ou RoleManager você fornece o tipo da classe de usuário e passa uma instância da classe de repositório como um argumento. Essa abordagem permite que você conecte suas classes personalizadas ao ASP.NET Core.

Reconfigurar o aplicativo para usar o novo provedor de armazenamento mostra como instanciar UserManager e RoleManager com um repositório personalizado.

O ASP.NET Core Identity armazena tipos de dados

Os tipos de dados do ASP.NET Core Identity são detalhados nas seguintes seções:

Usuários

Usuários registrados do seu site. O tipo IdentityUser pode ser estendido ou usado como um exemplo para seu próprio tipo personalizado. Você não precisa herdar de um tipo específico para implementar sua própria solução de armazenamento de identidade personalizada.

Declarações de usuário

Um conjunto de instruções (ou Declarações) sobre o usuário que representa a identidade do usuário. Pode habilitar uma expressão maior da identidade do usuário do que pode ser obtida por meio de funções.

Logons de usuário

Informações sobre o provedor externo de autenticação (como o Facebook ou uma conta Microsoft) a serem usadas ao fazer logon em um usuário. Exemplo

Funções

Grupos de autorização para seu site. Inclui a ID da função e o nome da função (como “Administrador” ou “Funcionário”). Exemplo

A camada de acesso a dados

Este tópico pressupõe que você conhece o mecanismo de persistência que vai usar e sabe como criar entidades para esse mecanismo. Este tópico não fornece detalhes sobre como criar as classes de acesso a dados ou repositórios; ele fornece algumas sugestões sobre decisões de design ao trabalhar com ASP.NET Core Identity.

Você tem muita liberdade ao projetar a camada de acesso a dados para um provedor de repositório personalizado. Você só precisa criar mecanismos de persistência para recursos que pretende usar em seu aplicativo. Por exemplo, se você não estiver usando funções em seu aplicativo, não precisará criar armazenamento para funções ou associações de função de usuário. Sua tecnologia e a infraestrutura existentes podem exigir uma estrutura muito diferente da implementação padrão do ASP.NET Core Identity. Em sua camada de acesso a dados, você fornece a lógica para trabalhar com a estrutura de sua implementação de armazenamento.

A camada de acesso a dados fornece a lógico para salvar os dados do ASP.NET Core Identity em uma fonte de dados. A camada de acesso a dados para seu provedor de armazenamento personalizado pode incluir as seguintes classes para armazenar informações de usuário e função.

Classe de contexto

Encapsula as informações para se conectar ao mecanismo de persistência e executar consultas. Várias classes de dados exigem uma instância dessa classe, normalmente fornecida por meio de injeção de dependência. Exemplo.

Armazenamento do usuário

Armazena e recupera informações do usuário (como nome de usuário e hash de senha). Exemplo

Armazenamento de função

Armazena e recupera informações de função (como o nome da função). Exemplo

Armazenamento de UserClaims

Armazena e recupera informações de declaração do usuário (como o tipo de declaração e o valor). Exemplo

Armazenamento de UserLogins

Armazena e recupera informações de logon do usuário (como um provedor externo de autenticação). Exemplo

Armazenamento de UserRole

Armazena e recupera quais funções são atribuídas a quais usuários. Exemplo

DICA: implemente apenas as classes que você pretende usar em seu aplicativo.

Nas classes de acesso a dados, forneça código para executar operações de dados para seu mecanismo de persistência. Por exemplo, em um provedor personalizado, você pode ter o código a seguir para criar um novo usuário na classe de repositório:

public async Task<IdentityResult> CreateAsync(ApplicationUser user, 
    CancellationToken cancellationToken = default(CancellationToken))
{
    cancellationToken.ThrowIfCancellationRequested();
    if (user == null) throw new ArgumentNullException(nameof(user));

    return await _usersTable.CreateAsync(user);
}

A lógica de implementação para criar o usuário está no método _usersTable.CreateAsync, mostrado abaixo.

Personalizar a classe de usuário

Ao implementar um provedor de armazenamento, crie uma classe de usuário equivalente à Identityclasse de usuário.

No mínimo, sua classe de usuário deve incluir uma propriedade Id e UserName.

A classe IdentityUser define as propriedades que UserManager chama ao executar operações solicitadas. O tipo padrão da propriedade Id é uma cadeia de caracteres, mas você pode herdá-la de IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken> e especificar um tipo diferente. A estrutura espera que a implementação de armazenamento manipule conversões de tipo de dados.

Personalizar o repositório de usuários

Crie uma classe UserStore que forneça os métodos para todas as operações de dados no usuário. Essa classe é equivalente à classe UserStore<TUser>. Em sua classe UserStore, implemente IUserStore<TUser> e as interfaces opcionais necessárias. Selecione quais interfaces opcionais implementar com base na funcionalidade fornecida em seu aplicativo.

Interfaces opcionais

As interfaces opcionais herdam de IUserStore<TUser>. Você pode ver um repositório de usuários de exemplo parcialmente implementado no aplicativo de exemplo.

Dentro da classe UserStore, você usa as classes de acesso a dados que criou para executar operações. Eles são passados usando a injeção de dependência. Por exemplo, no SQL Server com a implementação do Dapper, a classe UserStore tem o método CreateAsync que usa uma instância de DapperUsersTable para inserir um novo registro:

public async Task<IdentityResult> CreateAsync(ApplicationUser user)
{
    string sql = "INSERT INTO dbo.CustomUser " +
        "VALUES (@id, @Email, @EmailConfirmed, @PasswordHash, @UserName)";

    int rows = await _connection.ExecuteAsync(sql, new { user.Id, user.Email, user.EmailConfirmed, user.PasswordHash, user.UserName });

    if(rows > 0)
    {
        return IdentityResult.Success;
    }
    return IdentityResult.Failed(new IdentityError { Description = $"Could not insert user {user.Email}." });
}

Interfaces a serem implementadas ao personalizar o repositório de usuários

  • IUserStore
    A interface IUserStore<TUser> é a única interface que você deve implementar no repositório de usuários. Define métodos para criar, atualizar, excluir e recuperar usuários.
  • IUserClaimStore
    A interface IUserClaimStore<TUser> define os métodos que você implementa para habilitar declarações de usuário. Contém métodos para adicionar, remover e recuperar declarações de usuário.
  • IUserLoginStore
    O IUserLoginStore<TUser> define os métodos que você implementa para habilitar provedores externos de autenticação. Contém métodos para adicionar, remover e recuperar logons de usuário e um método para recuperar um usuário com base nas informações de logon.
  • IUserRoleStore
    A interface IUserRoleStore<TUser> define os métodos que você implementa para mapear um usuário para uma função. Contém métodos para adicionar, remover e recuperar as funções de um usuário e um método para verificar se um usuário foi atribuído a uma função.
  • IUserPasswordStore
    A interface IUserPasswordStore<TUser> define os métodos que você implementa para persistir senhas com hash. Contém métodos para obter e definir a senha com hash e um método que indica se o usuário definiu uma senha.
  • IUserSecurityStampStore
    A interface IUserSecurityStampStore<TUser> define os métodos que você implementa para usar um carimbo de segurança para indicar se as informações da conta do usuário foram alteradas. Esse carimbo é atualizado quando um usuário altera a senha ou adiciona ou remove logons. Contém métodos para obter e definir o carimbo de segurança.
  • IUserTwoFactorStore
    A interface IUserTwoFactorStore<TUser> define os métodos que você implementa para dar suporte à autenticação de dois fatores. Contém métodos para obter e definir se a autenticação de dois fatores está habilitada para um usuário.
  • IUserPhoneNumberStore
    A interface IUserPhoneNumberStore<TUser> define os métodos que você implementa para armazenar números de telefone do usuário. Contém métodos para obter e definir o número de telefone e se o número de telefone foi confirmado.
  • IUserEmailStore
    A interface IUserEmailStore<TUser> define os métodos que você implementa para armazenar endereços de email do usuário. Contém métodos para obter e definir o endereço de email e se o email foi confirmado.
  • IUserLockoutStore
    A interface IUserLockoutStore<TUser> define os métodos que você implementa para armazenar informações sobre como bloquear uma conta. Contém métodos para acompanhar tentativas de acesso com falha e bloqueios.
  • IQueryableUserStore
    A interface IQueryableUserStore<TUser> defina os membros que você implementa para fornecer um repositório de usuários consultável.

Você implementa apenas as interfaces necessárias em seu aplicativo. Por exemplo:

public class UserStore : IUserStore<IdentityUser>,
                         IUserClaimStore<IdentityUser>,
                         IUserLoginStore<IdentityUser>,
                         IUserRoleStore<IdentityUser>,
                         IUserPasswordStore<IdentityUser>,
                         IUserSecurityStampStore<IdentityUser>
{
    // interface implementations not shown
}

IdentityUserClaim, IdentityUserLogin e IdentityUserRole

O namespace Microsoft.AspNet.Identity.EntityFramework contém implementações das classes IdentityUserClaim, IdentityUserLogin, IdentityUserRole. Se você estiver usando esses recursos, talvez você queira criar suas próprias versões dessas classes e definir as propriedades para seu aplicativo. No entanto, às vezes é mais eficiente não carregar essas entidades na memória ao executar operações básicas (como adicionar ou remover a declaração de um usuário). Em vez disso, as classes de repositório de back-end podem executar essas operações diretamente na fonte de dados. Por exemplo, o método UserStore.GetClaimsAsync pode chamar o método userClaimTable.FindByUserId(user.Id) para executar uma consulta diretamente nessa tabela e retornar uma lista de declarações.

Personalizar a classe de função

Ao implementar um provedor de armazenamento de função, você pode criar um tipo de função personalizada. Ele não precisa implementar uma interface específica, mas deve ter um Id e normalmente terá uma propriedade Name.

O exemplo a seguir é uma classe de função de exemplo:

using System;

namespace CustomIdentityProviderSample.CustomProvider
{
    public class ApplicationRole
    {
        public Guid Id { get; set; } = Guid.NewGuid();
        public string Name { get; set; }
    }
}

Personalizar o repositório de funções

Você pode criar uma classe RoleStore que fornece os métodos para todas as operações de dados em funções. Essa classe é equivalente à classe RoleStore<TRole>. Na classe RoleStore, você implementa o IRoleStore<TRole> e, opcionalmente, a interface IQueryableRoleStore<TRole>.

  • IRoleStore<TRole>
    A interface IRoleStore<TRole> define os métodos a serem implementados na classe de repositório de funções. Ele contém métodos para criar, atualizar, excluir e recuperar funções.
  • RoleStore<TRole>
    Para personalizar RoleStore, crie uma classe que implementa a interface IRoleStore<TRole>.

Reconfigurar o aplicativo para usar um novo provedor de armazenamento

Depois de implementar um provedor de armazenamento, configure seu aplicativo para usá-lo. Se o aplicativo usado o provedor padrão, substitua-o pelo provedor personalizado.

  1. Remova o pacote NuGet Microsoft.AspNetCore.EntityFramework.Identity.
  2. Se o provedor de armazenamento residir um projeto ou pacote separado, adicione uma referência a ele.
  3. Substitua todas as referências a Microsoft.AspNetCore.EntityFramework.Identity por uma instrução using para o namespace do provedor de armazenamento.
  4. Altere o método AddIdentity para usar os tipos personalizados. Você pode criar seus próprios métodos de extensão para essa finalidade. Consulte IdentityServiceCollectionExtensions para obter um exemplo.
  5. Se você estiver usando Funções, atualize o RoleManager para usar sua classe RoleStore.
  6. Atualize a cadeia de conexão e as credenciais para a configuração do aplicativo.

Exemplo:

public void ConfigureServices(IServiceCollection services)
{
    // Add identity types
    services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddDefaultTokenProviders();

    // Identity Services
    services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
    services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
    string connectionString = Configuration.GetConnectionString("DefaultConnection");
    services.AddTransient<SqlConnection>(e => new SqlConnection(connectionString));
    services.AddTransient<DapperUsersTable>();

    // additional configuration
}
var builder = WebApplication.CreateBuilder(args);

// Add identity types
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddDefaultTokenProviders();

// Identity Services
builder.Services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
builder.Services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddTransient<SqlConnection>(e => new SqlConnection(connectionString));
builder.Services.AddTransient<DapperUsersTable>();

// additional configuration

builder.Services.AddRazorPages();

var app = builder.Build();

Referências