Visão geral de provedores de armazenamento personalizados para a Identidade do ASP.NET

por Tom FitzMacken

ASP.NET Identity é um sistema extensível que permite criar seu próprio provedor de armazenamento e conectá-lo ao seu aplicativo sem retrabalhar com o aplicativo. Este tópico descreve como criar um provedor de armazenamento personalizado para ASP.NET Identity. Ele aborda os conceitos importantes para a criação de seu próprio provedor de armazenamento, mas não é uma explicação passo a passo da implementação de um provedor de armazenamento personalizado.

Para obter um exemplo de implementação de um provedor de armazenamento personalizado, consulte implementando um provedor de armazenamento de ASP.net Identity do MySQL personalizado.

Este tópico foi atualizado para o ASP.NET Identity 2,0.

Versões de software usadas no tutorial

  • Visual Studio 2013 com atualização 2
  • ASP.NET Identity 2

Introdução

Por padrão, o sistema de ASP.NET Identity armazena informações de usuário em um banco de dados SQL Server e usa Entity Framework Code First para criar o banco de dados. Para muitos aplicativos, essa abordagem funciona bem. No entanto, você pode preferir usar um tipo diferente de mecanismo de persistência, como o armazenamento de tabelas do Azure, ou talvez você já tenha tabelas de banco de dados com uma estrutura muito diferente da implementação padrão. Em ambos os casos, você pode escrever um provedor personalizado para seu mecanismo de armazenamento e conectar esse provedor ao seu aplicativo.

O ASP.NET Identity é incluído por padrão em muitos dos modelos de Visual Studio 2013. Você pode obter atualizações para ASP.NET Identity por meio do pacote NuGet do Microsoft ASPNET Identity EntityFramework.

Este tópico inclui as seções a seguir:

Entender a arquitetura

ASP.NET Identity consiste em classes chamadas de gerentes e lojas. Os gerentes são classes de alto nível que o desenvolvedor de aplicativo usa para executar operações, como a criação de um usuário, no sistema ASP.NET Identity. As lojas são classes de nível inferior que especificam como as entidades, como usuários e funções, são mantidas. As lojas estão intimamente ligadas ao mecanismo de persistência, mas os gerentes são dissociados das lojas, o que significa que você pode substituir o mecanismo de persistência sem interromper todo o aplicativo.

O diagrama a seguir mostra como o aplicativo Web interage com os gerentes e armazena a interação com a camada de acesso a dados.

Para criar um provedor de armazenamento personalizado para ASP.NET Identity, você precisa criar a fonte de dados, a camada de acesso a dados e as classes de armazenamento que interagem com essa camada de acesso a dados. Você pode continuar usando as mesmas APIs de Gerenciador para executar operações de dados no usuário, mas agora esses dados são salvos em um sistema de armazenamento diferente.

Você não precisa personalizar as classes de Gerenciador porque ao criar uma nova instância de usermanager ou roleManager, você fornece o tipo da classe de usuário e passa uma instância da classe Store como um argumento. Essa abordagem permite que você conecte suas classes personalizadas à estrutura existente. Você verá como criar uma instância de usermanager e roleManager com suas classes de armazenamento personalizadas na seção reconfigurar aplicativo para usar o novo provedor de armazenamento.

Entender os dados que são armazenados

Para implementar um provedor de armazenamento personalizado, você deve entender os tipos de dados usados com ASP.NET Identity e decidir quais recursos são relevantes para seu aplicativo.

data DESCRIÇÃO
Usuários Usuários registrados do seu site da Web. Inclui a ID de usuário e o nome de usuário. Pode incluir uma senha com hash se os usuários fizerem logon com credenciais específicas para seu site (em vez de usar credenciais de um site externo, como o Facebook) e o carimbo de segurança para indicar se alguma coisa foi alterada nas credenciais do usuário. Também pode incluir endereço de email, número de telefone, se a autenticação de dois fatores está habilitada, o número atual de logons com falha e se uma conta foi bloqueada.
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 obtido por meio de funções.
Logons de usuário Informações sobre o provedor de autenticação externa (como o Facebook) para usar ao fazer logon em um usuário.
Funções Grupos de autorização para seu site. Inclui a ID da função e o nome da função (como "admin" ou "Employee").

Criar a camada de acesso a dados

Este tópico pressupõe que você esteja familiarizado com o mecanismo de persistência que você pretende usar e como criar entidades para esse mecanismo. Este tópico não fornece detalhes sobre como criar repositórios ou classes de acesso a dados; em vez disso, ele fornece algumas sugestões sobre as decisões de design que você precisa fazer ao trabalhar com ASP.NET Identity.

Você tem muita liberdade ao criar repositórios para um provedor de armazenamento personalizado. Você só precisa criar repositórios para os 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 funções de usuário. Sua tecnologia e infraestrutura existente podem exigir uma estrutura muito diferente da implementação padrão do ASP.NET Identity. Na camada de acesso a dados, você fornece a lógica para trabalhar com a estrutura de seus repositórios.

Para obter uma implementação do MySQL de repositórios de dados para o ASP.NET Identity 2,0, consulte MySQLIdentity. SQL.

Na camada de acesso a dados, você fornece a lógica para salvar os dados de ASP.NET Identity em sua 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 DESCRIÇÃO Exemplo
Contexto Encapsula as informações para se conectar ao mecanismo de persistência e executar consultas. Essa classe é fundamental para sua camada de acesso a dados. As outras classes de dados exigirão uma instância dessa classe para executar suas operações. Você também inicializará suas classes de armazenamento com uma instância dessa classe. MySQLDatabase
Armazenamento do usuário Armazena e recupera informações do usuário (como o nome de usuário e o hash de senha). Usertable (MySQL)
Armazenamento de função Armazena e recupera informações de função (como o nome da função). Função (MySQL)
Armazenamento de userreivindicações Armazena e recupera informações de declaração do usuário (como o tipo e o valor da declaração). O MySQL (userclaimtable)
Armazenamento de userlogons Armazena e recupera informações de logon de usuário (como um provedor de autenticação externa). UserLoginsTable (MySQL)
Armazenamento de UserRole Armazena e recupera a quais funções um usuário é atribuído. Sqlroletable (MySQL)

Novamente, você só precisa implementar as classes que pretende usar em seu aplicativo.

Nas classes de acesso a dados, você fornece código para executar operações de dados para seu mecanismo de persistência específico. Por exemplo, dentro da implementação do MySQL, a classe Usertable contém um método para inserir um novo registro na tabela de banco de dados users. A variável chamada _database é uma instância da classe MySQLDatabase.

public int Insert(TUser user)
{
    string commandText = @"Insert into Users (UserName, Id, PasswordHash, SecurityStamp,Email,EmailConfirmed,PhoneNumber,PhoneNumberConfirmed, AccessFailedCount,LockoutEnabled,LockoutEndDateUtc,TwoFactorEnabled)
        values (@name, @id, @pwdHash, @SecStamp,@email,@emailconfirmed,@phonenumber,@phonenumberconfirmed,@accesscount,@lockoutenabled,@lockoutenddate,@twofactorenabled)";
    Dictionary<string, object> parameters = new Dictionary<string, object>();
    parameters.Add("@name", user.UserName);
    parameters.Add("@id", user.Id);
    parameters.Add("@pwdHash", user.PasswordHash);
    parameters.Add("@SecStamp", user.SecurityStamp);
    parameters.Add("@email", user.Email);
    parameters.Add("@emailconfirmed", user.EmailConfirmed);
    parameters.Add("@phonenumber", user.PhoneNumber);
    parameters.Add("@phonenumberconfirmed", user.PhoneNumberConfirmed);
    parameters.Add("@accesscount", user.AccessFailedCount);
    parameters.Add("@lockoutenabled", user.LockoutEnabled);
    parameters.Add("@lockoutenddate", user.LockoutEndDateUtc);
    parameters.Add("@twofactorenabled", user.TwoFactorEnabled);

    return _database.Execute(commandText, parameters);
}

Depois de criar suas classes de acesso a dados, você deve criar classes de armazenamento que chamam os métodos específicos na camada de acesso a dados.

Personalizar a classe de usuário

Ao implementar seu próprio provedor de armazenamento, você deve criar uma classe de usuário que seja equivalente à classe IdentityUser no namespace Microsoft. ASP. net. Identity. EntityFramework :

O diagrama a seguir mostra a classe IdentityUser que você deve criar e a interface a ser implementada nessa classe.

A interface IUser<TKey> define as propriedades que o usermanager tenta chamar ao executar as operações solicitadas. A interface contém duas propriedades-ID e nome de usuário. A interface IUser<TKey> permite que você especifique o tipo da chave para o usuário por meio do parâmetro de TKey genérico. O tipo da propriedade de ID corresponde ao valor do parâmetro TKey.

A estrutura de identidade também fornece a interface IUser (sem o parâmetro Generic) quando você deseja usar um valor de cadeia de caracteres para a chave.

A classe IdentityUser implementa IUser e contém quaisquer propriedades ou construtores adicionais para os usuários no seu site. O exemplo a seguir mostra uma classe IdentityUser que usa um inteiro para a chave. O campo ID é definido como int para corresponder ao valor do parâmetro Generic.

public class IdentityUser : IUser<int>
{
    public IdentityUser() { ... }
    public IdentityUser(string userName) { ... }
    public int Id { get; set; }
    public string UserName { get; set; }
    // can also define optional properties such as:
    //    PasswordHash
    //    SecurityStamp
    //    Claims
    //    Logins
    //    Roles
}

Para obter uma implementação completa, consulte IdentityUser (MySQL).

Personalizar o armazenamento do usuário

Você também cria uma classe USERSTORE que fornece os métodos para todas as operações de dados no usuário. Essa classe é equivalente à classe USERSTORE<TUser> no namespace Microsoft. ASP. net. Identity. EntityFramework . Em sua classe USERSTORE, você implementa o IUserStore<TUser, TKey> e qualquer uma das interfaces opcionais. Você seleciona quais interfaces opcionais implementar com base na funcionalidade que deseja fornecer em seu aplicativo.

A imagem a seguir mostra a classe USERSTORE que você deve criar e as interfaces relevantes.

O modelo de projeto padrão no Visual Studio contém código que assume que muitas das interfaces opcionais foram implementadas no armazenamento do usuário. Se você estiver usando o modelo padrão com um armazenamento de usuário personalizado, deverá implementar interfaces opcionais em seu repositório de usuários ou alterar o código do modelo para não chamar mais métodos nas interfaces que você não implementou.

O exemplo a seguir mostra uma classe de armazenamento de usuário simples. O parâmetro genérico TUser usa o tipo de sua classe de usuário que geralmente é a classe IdentityUser que você definiu. O parâmetro genérico TKey usa o tipo de sua chave de usuário.

public class UserStore : IUserStore<IdentityUser, int>
{
    public UserStore() { ... }
    public UserStore(ExampleStorage database) { ... }
    public Task CreateAsync(IdentityUser user) { ... }
    public Task DeleteAsync(IdentityUser user) { ... }
    public Task<IdentityUser> FindByIdAsync(int userId) { ... }
    public Task<IdentityUser> FindByNameAsync(string userName) { ... }
    public Task UpdateAsync(IdentityUser user) { ... }
    public void Dispose() { ... }
}

Neste exemplo, o construtor que usa um parâmetro denominado banco de dados do tipo ExampleDatabase é apenas uma ilustração de como transmitir sua classe de acesso a dados. Por exemplo, na implementação do MySQL, esse construtor usa um parâmetro do tipo MySQLDatabase.

Em sua classe USERSTORE, você usa as classes de acesso a dados que você criou para executar operações. Por exemplo, na implementação do MySQL, a classe USERSTORE tem o método createasync que usa uma instância de Usertable para inserir um novo registro. O método Insert no objeto usertable é o mesmo método mostrado na seção anterior.

public Task CreateAsync(IdentityUser user)
{
    if (user == null) {
        throw new ArgumentNullException("user");
    }

    userTable.Insert(user);

    return Task.FromResult<object>(null);
}

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

A imagem a seguir mostra mais detalhes sobre a funcionalidade definida em cada interface. Todas as interfaces opcionais herdam de IUserStore.

  • IUserStore
    A interface IUserStore<TUser, TKey> é a única interface que você deve implementar em seu repositório de usuários. Ele define métodos para criar, atualizar, excluir e recuperar usuários.

  • IUserClaimStore
    A interface IUserClaimStore<TUser, TKey> define os métodos que você deve implementar em seu repositório de usuários para habilitar as declarações do usuário. Ele contém métodos ou adição, remoção e recuperação de declarações do usuário.

  • IUserLoginStore
    O IUserLoginStore<TUser, TKey> define os métodos que você deve implementar em seu repositório de usuários para habilitar provedores de autenticação externa. Ele 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<TKey, TUser> define os métodos que você deve implementar em seu repositório de usuários para mapear um usuário para uma função. Ele contém métodos para adicionar, remover e recuperar funções de um usuário e um método para verificar se um usuário está atribuído a uma função.

  • IUserPasswordStore
    A interface IUserPasswordStore<TUser, TKey> define os métodos que você deve implementar em seu repositório de usuários para manter as senhas com hash. Ele contém métodos para obter e definir a senha de hash e um método que indica se o usuário definiu uma senha.

  • IUserSecurityStampStore
    A interface IUserSecurityStampStore<TUser, TKey> define os métodos que você deve implementar em seu repositório de usuários 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. Ele contém métodos para obter e definir o carimbo de segurança.

  • IUserTwoFactorStore
    A interface IUserTwoFactorStore<TUser, TKey> define os métodos que devem ser implementados para implementar a autenticação de dois fatores. Ele 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, TKey> define os métodos que devem ser implementados para armazenar números de telefone do usuário. Ele contém métodos para obter e definir o número de telefone e se o número de telefone é confirmado.

  • IUserEmailStore
    A interface IUserEmailStore<TUser, TKey> define os métodos que devem ser implementados para armazenar endereços de email do usuário. Ele contém métodos para obter e definir o endereço de email e se o email é confirmado.

  • IUserLockoutStore
    A interface IUserLockoutStore<TUser, TKey> define os métodos que devem ser implementados para armazenar informações sobre o bloqueio de uma conta. Ele contém métodos para obter o número atual de tentativas de acesso com falha, obter e definir se a conta pode ser bloqueada, obter e definir a data de término do bloqueio, incrementar o número de tentativas com falha e redefinir o número de tentativas com falha.

  • IQueryableUserStore
    A interface IQueryableUserStore<TUser, TKey> define os membros que você deve implementar para fornecer um repositório de usuários passível de consulta. Ele contém uma propriedade que contém os usuários consultáveis.

    Você implementa as interfaces que são necessárias em seu aplicativo; como, as interfaces IUserClaimStore, IUserLoginStore, IUserRoleStore, IUserPasswordStore e IUserSecurityStampStore, conforme mostrado abaixo.

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

Para obter uma implementação completa (incluindo todas as interfaces), consulte USERSTORE (MySQL).

IdentityUserClaim, IdentityUserLogin e IdentityUserRole

O namespace Microsoft. AspNet. Identity. EntityFramework contém implementações das classes IdentityUserClaim, IdentityUserLogine IdentityUserRole . Se você estiver usando esses recursos, talvez queira criar suas próprias versões dessas classes e definir as propriedades do 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 uma declaração de usuário). Em vez disso, as classes de armazenamento de back-end podem executar essas operações diretamente na fonte de dados. Por exemplo, o método USERSTORE. GetClaimsAsync () pode chamar userclaimtable. FindByUserId (User. ID) para executar uma consulta na tabela diretamente e retornar uma lista de declarações.

public Task<IList<Claim>> GetClaimsAsync(IdentityUser user)
{
    ClaimsIdentity identity = userClaimsTable.FindByUserId(user.Id);
    return Task.FromResult<IList<Claim>>(identity.Claims.ToList());
}

Personalizar a classe de função

Ao implementar seu próprio provedor de armazenamento, você deve criar uma classe de função que seja equivalente à classe IdentityRole no namespace Microsoft. ASP. net. Identity. EntityFramework :

O diagrama a seguir mostra a classe IdentityRole que você deve criar e a interface a ser implementada nessa classe.

A interface IRole<TKey> define as propriedades que o roleManager tenta chamar ao executar as operações solicitadas. A interface contém duas propriedades-ID e nome. A interface IRole<TKey> permite que você especifique o tipo da chave para a função por meio do parâmetro de TKey genérico. O tipo da propriedade de ID corresponde ao valor do parâmetro TKey.

A estrutura de identidade também fornece a interface IRole (sem o parâmetro Generic) quando você deseja usar um valor de cadeia de caracteres para a chave.

O exemplo a seguir mostra uma classe IdentityRole que usa um inteiro para a chave. O campo ID é definido como int para corresponder ao valor do parâmetro Generic.

public class IdentityRole : IRole<int>
{
    public IdentityRole() { ... }
    public IdentityRole(string roleName) { ... }
    public int Id { get; set; }
    public string Name { get; set; }
}

Para obter uma implementação completa, consulte IdentityRole (MySQL).

Personalizar o repositório de funções

Você também cria uma classe RoleStore que fornece os métodos para todas as operações de dados em funções. Essa classe é equivalente à classe RoleStore<TRole> no namespace Microsoft. ASP. net. Identity. EntityFramework. Na sua classe RoleStore, você implementa o IRoleStore<TRole, tkey> e, opcionalmente , a interface IQueryableRoleStore<TRole, TKey> .

O exemplo a seguir mostra uma classe de repositório de funções. O parâmetro genérico TRole usa o tipo de sua classe de função que geralmente é a classe IdentityRole que você definiu. O parâmetro genérico TKey usa o tipo de sua chave de função.

public class RoleStore : IRoleStore<IdentityRole, int>
{
    public RoleStore() { ... }
    public RoleStore(ExampleStorage database) { ... }
    public Task CreateAsync(IdentityRole role) { ... }
    public Task DeleteAsync(IdentityRole role) { ... }
    public Task<IdentityRole> FindByIdAsync(int roleId) { ... }
    public Task<IdentityRole> FindByNameAsync(string roleName) { ... }
    public Task UpdateAsync(IdentityRole role) { ... }
    public void Dispose() { ... }
}
  • IRoleStore<TRole>
    A interface IRoleStore define os métodos a serem implementados em sua classe de armazenamento de função. Ele contém métodos para criar, atualizar, excluir e recuperar funções.

  • RoleStore<TRole>
    Para personalizar o RoleStore, crie uma classe que implemente a interface IRoleStore. Você só precisa implementar essa classe se quiser usar funções em seu sistema. O construtor que usa um parâmetro denominado banco de dados do tipo ExampleDatabase é apenas uma ilustração de como transmitir sua classe de acesso a dados. Por exemplo, na implementação do MySQL, esse construtor usa um parâmetro do tipo MySQLDatabase.

    Para obter uma implementação completa, consulte RoleStore (MySQL) .

Reconfigurar o aplicativo para usar o novo provedor de armazenamento

Você implementou seu novo provedor de armazenamento. Agora, você deve configurar seu aplicativo para usar esse provedor de armazenamento. Se o provedor de armazenamento padrão foi incluído no seu projeto, você deve remover o provedor padrão e substituí-lo pelo seu provedor.

Substituir o provedor de armazenamento padrão no projeto MVC

  1. Na janela gerenciar pacotes NuGet , desinstale o pacote do Microsoft ASP.net Identity EntityFramework . Você pode encontrar esse pacote pesquisando nos pacotes instalados para Identity. EntityFramework.
    será perguntado se você também deseja desinstalar o Entity Framework. Se você não precisar dele em outras partes do seu aplicativo, poderá desinstalá-lo.

  2. No arquivo IdentityModels.cs na pasta modelos, exclua ou comente as classes ApplicationUser e ApplicationDbContext . Em um aplicativo MVC, você pode excluir o arquivo IdentityModels.cs inteiro. Em um aplicativo Web Forms, exclua as duas classes, mas lembre-se de manter a classe auxiliar que também está localizada no arquivo IdentityModels.cs.

  3. Se o provedor de armazenamento residir em um projeto separado, adicione uma referência a ele em seu aplicativo Web.

  4. Substitua todas as referências a using Microsoft.AspNet.Identity.EntityFramework; com uma instrução using para o namespace do seu provedor de armazenamento.

  5. Na classe Startup.auth.cs , altere o método ConfigureAuth para usar uma única instância do contexto apropriado.

    public void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(ExampleStorageContext.Create);
        app.CreatePerOwinContext(ApplicationUserManager.Create);
        ...
    
  6. Na pasta iniciar do aplicativo_, abra IdentityConfig.cs. Na classe ApplicationUserManager, altere o método Create para retornar um Gerenciador de usuários que usa seu armazenamento de usuário personalizado.

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
    {
        var manager = new ApplicationUserManager(new UserStore(context.Get<ExampleStorageContext>()));
        ...
    }
    
  7. Substitua todas as referências a ApplicationUser por IdentityUser.

  8. O projeto padrão inclui alguns membros na classe de usuário que não estão definidos na interface IUser; como email, PasswordHash e GenerateUserIdentityAsync. Se sua classe de usuário não tiver esses membros, você deverá implementá-los ou alterar o código que usa esses membros.

  9. Se você tiver criado qualquer instância de roleManager, altere esse código para usar sua nova classe RoleStore.

    var roleManager = new RoleManager<IdentityRole>(new RoleStore(context.Get<ExampleStorageContext>()));
    
  10. O projeto padrão é projetado para uma classe de usuário que tem um valor de cadeia de caracteres para a chave. Se sua classe de usuário tiver um tipo diferente para a chave (como um inteiro), você deverá alterar o projeto para trabalhar com seu tipo. Consulte alterar chave primária para usuários em ASP.net Identity.

  11. Se necessário, adicione a cadeia de conexão ao arquivo Web. config.

Outros recursos