Compartir a través de


Proveedores de almacenamiento personalizados para ASP.NET Core Identity

Por Steve Smith

ASP.NET Core Identity es un sistema extensible que le permite crear un proveedor de almacenamiento personalizado y conectarlo a su aplicación. En este tema se describe cómo crear un proveedor de almacenamiento personalizado para ASP.NET Core Identity. Trata los conceptos importantes para crear su propio proveedor de almacenamiento, pero no es un tutorial paso a paso. Consulte Personalización del modelo de Identity para personalizar un modelo de Identity.

Introducción

De forma predeterminada, el sistema de ASP.NET Core Identity almacena información de usuario en una base de datos de SQL Server mediante Entity Framework Core. Para muchas aplicaciones, este enfoque funciona bien. Sin embargo, es posible que prefiera usar otro mecanismo de persistencia o esquema de datos. Por ejemplo:

  • Usa Azure Table Storage u otro almacén de datos.
  • Las tablas de base de datos tienen una estructura diferente.
  • Puede querer usar un método de acceso a datos diferente, como Dapper.

En cada uno de estos casos, puede escribir un proveedor personalizado para el mecanismo de almacenamiento y conectar ese proveedor a la aplicación.

ASP.NET Core Identity se incluye en las plantillas de proyecto de Visual Studio con la opción "Cuentas de usuario individuales".

Al usar la CLI de .NET, agregue -au Individual:

dotnet new mvc -au Individual

La arquitectura de ASP.NET Core Identity

ASP.NET Core Identity consta de clases denominadas administradores y almacenes. Los administradores son clases de alto nivel que un desarrollador de aplicaciones usa para realizar operaciones, como la creación de un usuario de Identity. Los almacenes son clases de nivel inferior que especifican cómo se conservan las entidades, como los usuarios y los roles. Los almacenes siguen el patrón de repositorio y están estrechamente unidos al mecanismo de persistencia. Los administradores se desacoplan de almacenes, lo que significa que puede reemplazar el mecanismo de persistencia sin cambiar el código de la aplicación (excepto la configuración).

En el diagrama siguiente se muestra cómo interactúa una aplicación web con los administradores, mientras que los almacenes interactúan con la capa de acceso a datos.

Las aplicaciones de ASP.NET Core funcionan con administradores (por ejemplo, UserManager, RoleManager). Los administradores trabajan con almacenes (por ejemplo, UserStore) que se comunican con un origen de datos usando una biblioteca como Entity Framework Core.

Para crear un proveedor de almacenamiento personalizado, cree el origen de datos, la capa de acceso a datos y las clases de almacén que interactúan con esta capa de acceso a datos (los cuadros verdes y grises del diagrama anterior). No es necesario personalizar los administradores ni el código de la aplicación que interactúa con ellos (los cuadros azules anteriores).

Al crear una nueva instancia de UserManager o RoleManager se proporciona el tipo de la clase de usuario y se pasa una instancia de la clase store como argumento. Este enfoque le permite conectar las clases personalizadas a ASP.NET Core.

Volver a configurar la aplicación para usar un nuevo proveedor de almacenamiento muestra cómo crear instancias de UserManager y RoleManager con un almacén personalizado.

ASP.NET Core Identity almacena tipos de datos

Los tipos de datos de ASP.NET Core Identity se detallan en las secciones siguientes:

Usuarios

Usuarios registrados del sitio web. El tipo IdentityUser se puede extender o usar como ejemplo para su propio tipo personalizado. No es necesario heredar de un tipo determinado para implementar tu propia solución de almacenamiento de identity personalizada.

Notificaciones de usuario

Un conjunto de instrucciones (o notificaciones) sobre el usuario que representa la identity del usuario. Puede habilitar una expresión mayor de la identity del usuario que la que puede conseguirse mediante los roles.

Inicios de sesión de usuario

Información sobre el proveedor de autenticación externo (como Facebook o una cuenta de Microsoft) que se usará al iniciar la sesión de un usuario. Ejemplo

Roles

Grupos de autorización para el sitio. Incluye el identificador de rol y el nombre del rol (como "Administración" o "Empleado"). Ejemplo

Capa de acceso a datos

En este tema se supone que está familiarizado con el mecanismo de persistencia que va a usar y cómo crear entidades para ese mecanismo. En este tema no se proporcionan detalles sobre cómo crear los repositorios o las clases de acceso a datos; proporciona algunas sugerencias sobre las decisiones de diseño al trabajar con ASP.NET Core Identity.

Tiene mucha libertad al diseñar la capa de acceso a datos para un proveedor de almacén personalizado. Solo tiene que crear mecanismos de persistencia para las características que quiera usar en la aplicación. Por ejemplo, si no usa roles en la aplicación, no es necesario crear almacenamiento para roles ni asociaciones de roles de usuario. La tecnología y la infraestructura existente pueden requerir una estructura muy diferente de la implementación predeterminada de ASP.NET Core Identity. En la capa de acceso a datos, se proporciona la lógica para trabajar con la estructura de la implementación de almacenamiento.

La capa de acceso a datos proporciona la lógica para guardar los datos de ASP.NET Core Identity en un origen de datos. La capa de acceso a datos del proveedor de almacenamiento personalizado puede incluir las siguientes clases para almacenar información de usuario y rol.

Context (clase)

Encapsula la información para conectarse al mecanismo de persistencia y ejecutar consultas. Varias clases de datos requieren una instancia de esta clase, que normalmente se proporciona mediante la inserción de dependencias. Ejemplo.

Almacenamiento de usuario

Almacena y recupera información de usuario (como el nombre de usuario y el hash de contraseña). Ejemplo

Almacenamiento de estado

Almacena y recupera información de rol (por ejemplo, el nombre del rol). Ejemplo

Almacenamiento de UserClaims

Almacena y recupera información de notificación de usuario (como el tipo de notificación y el valor). Ejemplo

Almacenamiento de UserLogins

Almacena y recupera información de inicio de sesión de usuario (como un proveedor de autenticación externo). Ejemplo

Almacenamiento de UserRole

Almacena y recupera qué roles se asignan a los usuarios. Ejemplo

SUGERENCIA: Implemente solo las clases que quiera usar en la aplicación.

En las clases de acceso a datos, proporcione código para realizar operaciones de datos para el mecanismo de persistencia. Por ejemplo, dentro de un proveedor personalizado, es posible que tenga el código siguiente para crear un nuevo usuario en la clase almacén:

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

La lógica de implementación para crear el usuario está en el método _usersTable.CreateAsync, que se muestra a continuación.

Personalización de la clase de usuario

Al implementar un proveedor de almacenamiento, crea una clase de usuario equivalente a la clase IdentityUser.

Como mínimo, la clase de usuario debe incluir una propiedad Id y UserName.

La clase IdentityUser define las propiedades a las que llama UserManager al realizar las operaciones solicitadas. El tipo predeterminado de la propiedad Id es una cadena, pero puede heredar de IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken> y especificar un tipo diferente. El marco espera que la implementación de almacenamiento controle las conversiones de tipos de datos.

Personalización del almacén de usuarios

Crear una clase UserStore que proporcione los métodos para todas las operaciones de datos sobre el usuario. Esta clase es equivalente a la clase UserStore<TUser>. En su clase UserStore, implemente IUserStore<TUser> y las interfaces opcionales necesarias. Seleccione qué interfaces opcionales desea implementar en función de las funciones que ofrezca su aplicación.

Interfaces opcionales

Las interfaces opcionales heredan de IUserStore<TUser>. Puede ver un almacén de usuarios de ejemplo parcialmente implementado en la aplicación de ejemplo.

Dentro de la clase UserStore, se usan las clases de acceso a datos que creó para realizar operaciones. Se pasan mediante la inserción de dependencias. Por ejemplo, en la implementación de SQL Server con Dapper, la clase UserStore tiene el método CreateAsync que usa una instancia de DapperUsersTable para insertar un nuevo 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 para implementar al personalizar el almacén de usuarios

  • IUserStore
    La interfaz IUserStore<TUser> es la única que debe implementar en el almacén de usuarios. Define métodos para crear, actualizar, eliminar y recuperar usuarios.
  • IUserClaimStore
    La interfaz IUserClaimStore<TUser> define los métodos que se implementan para habilitar las notificaciones de los usuarios. Contiene métodos para agregar, quitar y recuperar notificaciones de usuario.
  • IUserLoginStore
    IUserLoginStore<TUser> define los métodos que se implementan para habilitar proveedores de autenticación externos. Contiene métodos para agregar, quitar y recuperar inicios de sesión de usuario y un método para recuperar un usuario en función de la información de inicio de sesión.
  • IUserRoleStore
    La interfaz IUserRoleStore<TUser> define los métodos que se implementan para asignar un usuario a un rol. Contiene métodos para agregar, quitar y recuperar los roles de un usuario y un método para comprobar si un usuario está asignado a un rol.
  • IUserPasswordStore
    La interfaz IUserPasswordStore<TUser> define los métodos que se implementan para conservar las contraseñas con hash. Contiene métodos para obtener y establecer la contraseña con hash y un método que indica si el usuario ha establecido una contraseña.
  • IUserSecurityStampStore
    La interfaz IUserSecurityStampStore<TUser> define los métodos que se implementan para usar un sello de seguridad para indicar si la información de la cuenta del usuario ha cambiado. Esta marca se actualiza cuando un usuario cambia la contraseña o agrega o quita los inicios de sesión. Contiene métodos para obtener y establecer la marca de seguridad.
  • IUserTwoFactorStore
    La interfaz IUserTwoFactorStore<TUser> define los métodos que implementa para admitir la autenticación de dos factores. Contiene métodos para obtener y establecer si la autenticación en dos fases está habilitada para un usuario.
  • IUserPhoneNumberStore
    La interfaz IUserPhoneNumberStore<TUser> define los métodos que se implementan para almacenar los números de teléfono de los usuarios. Contiene métodos para obtener y establecer el número de teléfono y si se confirma el número de teléfono.
  • IUserEmailStore
    La interfaz IUserEmailStore<TUser> define los métodos que se implementan para almacenar las direcciones de correo electrónico de los usuarios. Contiene métodos para obtener y establecer la dirección de correo electrónico y si se confirma el correo electrónico.
  • IUserLockoutStore
    La interfaz IUserLockoutStore<TUser> define los métodos que se implementan para almacenar información sobre el bloqueo de una cuenta. Contiene métodos para realizar el seguimiento de intentos de acceso erróneos y bloqueos.
  • IQueryableUserStore
    La interfaz IQueryableUserStore<TUser> define los miembros que implementa para proporcionar un almacén de usuarios consultable.

Solo se implementan las interfaces necesarias en la aplicación. Por ejemplo:

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

IdentityUserClaim, IdentityUserLogin e IdentityUserRole

El espacio de nombres Microsoft.AspNet.Identity.EntityFramework contiene implementaciones de las clases IdentityUserClaimIdentityUserLoginIdentityUserRole. Si usa estas características, es posible que quiera crear sus propias versiones de estas clases y definir las propiedades de la aplicación. Sin embargo, a veces es más eficaz no cargar estas entidades en la memoria al realizar operaciones básicas (como agregar o quitar la notificación de un usuario). En su lugar, las clases de almacén de back-end pueden ejecutar estas operaciones directamente en el origen de datos. Por ejemplo, el método UserStore.GetClaimsAsync puede llamar al método userClaimTable.FindByUserId(user.Id) para ejecutar una consulta en esa tabla directamente y devolver una lista de notificaciones.

Personalización de la clase de rol

Al implementar un proveedor de almacenamiento de roles, puede crear un tipo de rol personalizado. No es necesario que implemente una interfaz concreta, pero debe tener una propiedad Id y normalmente tendrá una propiedad Name.

A continuación se muestra una clase de rol de ejemplo:

using System;

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

Personalización del almacén de roles

Puede crear una clase RoleStore que proporcione los métodos para todas las operaciones de datos sobre roles. Esta clase es equivalente a la clase RoleStore<TRole> . En la clase RoleStore, se implementa IRoleStore<TRole> y opcionalmente la interfaz IQueryableRoleStore<TRole>.

  • IRoleStore<TRole>
    La interfaz IRoleStore<TRole> define los métodos a implementar en la clase de almacén de roles. Contiene métodos para crear, actualizar, eliminar y recuperar roles.
  • RoleStore<TRole>
    Para personalizar RoleStore, cree una clase que implemente la interfaz IRoleStore<TRole>.

Volver a configurar la aplicación para usar un nuevo proveedor de almacenamiento

Una vez que haya implementado un proveedor de almacenamiento, configure la aplicación para usarla. Si la aplicación usó el proveedor predeterminado, reemplácela por el proveedor personalizado.

  1. Quite el paquete NuGet Microsoft.AspNetCore.EntityFramework.Identity.
  2. Si el proveedor de almacenamiento reside en un proyecto o paquete independiente, agregue una referencia a él.
  3. Reemplace todas las referencias a Microsoft.AspNetCore.EntityFramework.Identity por una instrucción using para el espacio de nombres del proveedor de almacenamiento.
  4. Cambie el método AddIdentity para usar los tipos personalizados. Puede crear sus propios métodos de extensión para este propósito. Consulta IdentityServiceCollectionExtensions para obtener un ejemplo.
  5. Si usa roles, actualice el RoleManager para usar su clase RoleStore.
  6. Actualice la cadena de conexión y las credenciales a la configuración de la aplicación.

Ejemplo:

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

Referencias