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.
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
- IUserRoleStore
- IUserClaimStore
- IUserPasswordStore
- IUserSecurityStampStore
- IUserEmailStore
- IUserPhoneNumberStore
- IQueryableUserStore
- IUserLoginStore
- IUserTwoFactorStore
- IUserLockoutStore
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 IdentityUserClaim, IdentityUserLogin y IdentityUserRole. 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 personalizarRoleStore
, cree una clase que implemente la interfazIRoleStore<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.
- Quite el paquete NuGet
Microsoft.AspNetCore.EntityFramework.Identity
. - Si el proveedor de almacenamiento reside en un proyecto o paquete independiente, agregue una referencia a él.
- Reemplace todas las referencias a
Microsoft.AspNetCore.EntityFramework.Identity
por una instrucción using para el espacio de nombres del proveedor de almacenamiento. - 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. - Si usa roles, actualice el
RoleManager
para usar su claseRoleStore
. - 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
- Personalización de modelos de Identity
- Proveedores de almacenamiento personalizados para ASP.NET 4.x Identity
- ASP.NET Core Identity: este repositorio incluye vínculos a proveedores de almacenes mantenidos por la comunidad.
- Vea o descargue el ejemplo de GitHub.