Provider di archiviazione personalizzati per ASP.NET Core Identity

Di Steve Smith

ASP.NET Core Identity è un sistema estendibile che consente di creare un provider di archiviazione personalizzato e connetterlo all'app. Questo argomento descrive come creare un provider di archiviazione personalizzato per ASP.NET Core Identity. Illustra i concetti importanti per la creazione di un provider di archiviazione personalizzato, ma non è una procedura dettagliata. Vedere Identity Personalizzazione del modello per personalizzare un Identity modello.

Introduzione

Per impostazione predefinita, il sistema ASP.NET Core Identity archivia le informazioni utente in un database di SQL Server usando Entity Framework Core. Per molte app, questo approccio funziona bene. Tuttavia, è consigliabile usare un meccanismo di persistenza o uno schema di dati diverso. Ad esempio:

  • Si usano Archiviazione tabelle di Azure o un altro archivio dati.
  • Le tabelle di database hanno una struttura diversa.
  • È possibile usare un approccio di accesso ai dati diverso, ad esempio Dapper.

In ognuno di questi casi, è possibile scrivere un provider personalizzato per il meccanismo di archiviazione e collegare il provider all'app.

ASP.NET Core Identity è incluso nei modelli di progetto in Visual Studio con l'opzione "Account utente individuali".

Quando si usa l'interfaccia della riga di comando di .NET Core, aggiungere -au Individual:

dotnet new mvc -au Individual

Architettura ASP.NET Core Identity

ASP.NET Core Identity è costituito da classi denominate manager e negozi. I manager sono classi di alto livello usate da uno sviluppatore di app per eseguire operazioni, ad esempio la creazione di un Identity utente. Gli archivi sono classi di livello inferiore che specificano la modalità di persistenza delle entità, ad esempio utenti e ruoli. Gli archivi seguono il modello di repository e sono strettamente associati al meccanismo di persistenza. I manager sono separati dagli archivi, il che significa che è possibile sostituire il meccanismo di persistenza senza modificare il codice dell'applicazione (ad eccezione della configurazione).

Il diagramma seguente illustra come un'app Web interagisce con i responsabili, mentre gli archivi interagiscono con il livello di accesso ai dati.

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.

Per creare un provider di archiviazione personalizzato, creare l'origine dati, il livello di accesso ai dati e le classi di archiviazione che interagiscono con questo livello di accesso ai dati (le caselle verdi e grigie nel diagramma precedente). Non è necessario personalizzare i responsabili o il codice dell'app che interagisce con essi (le caselle blu sopra).

Quando si crea una nuova istanza di UserManager o RoleManager si specifica il tipo della classe utente e si passa un'istanza della classe store come argomento. Questo approccio consente di collegare le classi personalizzate in ASP.NET Core.

Riconfigurare l'app per l'uso di un nuovo provider di archiviazione illustra come creare UserManager un'istanza e RoleManager con un archivio personalizzato.

ASP.NET Core Identity archivia i tipi di dati

ASP.NET tipi di dati Core Identity sono descritti in dettaglio nelle sezioni seguenti:

Utenti

Utenti registrati del sito Web. Il IdentityUser tipo può essere esteso o usato come esempio per il tipo personalizzato. Non è necessario ereditare da un particolare tipo per implementare la propria soluzione di archiviazione delle identità personalizzata.

Attestazioni utente

Set di istruzioni (o attestazioni) relative all'utente che rappresentano l'identità dell'utente. Può abilitare un'espressione maggiore dell'identità dell'utente rispetto a quanto possibile tramite i ruoli.

Account di accesso utente

Informazioni sul provider di autenticazione esterno (ad esempio Facebook o un account Microsoft) da usare per l'accesso a un utente. Esempio

Ruoli

Gruppi di autorizzazione per il sito. Include l'ID ruolo e il nome del ruolo ,ad esempio "Amministrazione" o "Dipendente". Esempio

Livello di accesso ai dati

In questo argomento si presuppone che si abbia familiarità con il meccanismo di persistenza che si intende usare e come creare entità per tale meccanismo. Questo argomento non fornisce informazioni dettagliate su come creare i repository o le classi di accesso ai dati; fornisce alcuni suggerimenti sulle decisioni di progettazione quando si lavora con ASP.NET Core Identity.

Si ha molta libertà quando si progetta il livello di accesso ai dati per un provider di archivi personalizzato. Devi solo creare meccanismi di persistenza per le funzionalità che intendi usare nell'app. Ad esempio, se non si usano ruoli nell'app, non è necessario creare l'archiviazione per i ruoli o le associazioni di ruoli utente. La tecnologia e l'infrastruttura esistente possono richiedere una struttura molto diversa dall'implementazione predefinita di ASP.NET Core Identity. Nel livello di accesso ai dati si fornisce la logica per lavorare con la struttura dell'implementazione dell'archiviazione.

Il livello di accesso ai dati fornisce la logica per salvare i dati da ASP.NET Core Identity a un'origine dati. Il livello di accesso ai dati per il provider di archiviazione personalizzato può includere le classi seguenti per archiviare le informazioni sull'utente e sul ruolo.

Context (classe)

Incapsula le informazioni per connettersi al meccanismo di persistenza ed eseguire query. Diverse classi di dati richiedono un'istanza di questa classe, in genere fornita tramite l'inserimento delle dipendenze. Esempio.

Archiviazione utente

Archivia e recupera le informazioni utente, ad esempio il nome utente e l'hash delle password. Esempio

Archiviazione ruolo

Archivia e recupera informazioni sul ruolo, ad esempio il nome del ruolo. Esempio

Archiviazione UserClaims

Archivia e recupera le informazioni sull'attestazione utente, ad esempio il tipo di attestazione e il valore. Esempio

Archiviazione UserLogins

Archivia e recupera le informazioni di accesso utente, ad esempio un provider di autenticazione esterno. Esempio

UserRole Archiviazione

Archivia e recupera i ruoli assegnati agli utenti. Esempio

SUGGERIMENTO: implementa solo le classi che intendi usare nella tua app.

Nelle classi di accesso ai dati specificare il codice per eseguire operazioni sui dati per il meccanismo di persistenza. Ad esempio, all'interno di un provider personalizzato, potrebbe essere presente il codice seguente per creare un nuovo utente nella classe store :

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 logica di implementazione per la creazione dell'utente si trova nel _usersTable.CreateAsync metodo , illustrato di seguito.

Personalizzare la classe utente

Quando si implementa un provider di archiviazione, creare una classe utente equivalente alla Identityclasse User.

Come minimo, la classe utente deve includere una Id proprietà e .UserName

La IdentityUser classe definisce le proprietà chiamate durante l'esecuzione UserManager di operazioni richieste. Il tipo predefinito della Id proprietà è una stringa, ma è possibile ereditare da IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken> e specificare un tipo diverso. Il framework prevede che l'implementazione dell'archiviazione gestisca le conversioni dei tipi di dati.

Personalizzare l'archivio utenti

Creare una UserStore classe che fornisce i metodi per tutte le operazioni sui dati sull'utente. Questa classe equivale alla UserStore<TUser> classe . UserStore Nella classe implementare IUserStore<TUser> e le interfacce facoltative necessarie. Selezionare le interfacce facoltative da implementare in base alle funzionalità fornite nell'app.

Interfacce facoltative

Le interfacce facoltative ereditano da IUserStore<TUser>. È possibile visualizzare un archivio utenti di esempio parzialmente implementato nell'app di esempio.

All'interno della UserStore classe si usano le classi di accesso ai dati create per eseguire operazioni. Questi vengono passati usando l'inserimento delle dipendenze. Ad esempio, nell'implementazione di SQL Server con Dapper la UserStore classe ha il CreateAsync metodo che usa un'istanza di DapperUsersTable per inserire un nuovo record:

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

Interfacce da implementare durante la personalizzazione dell'archivio utenti

  • IUserStore
    L'interfaccia IUserStore<TUser> è l'unica interfaccia che è necessario implementare nell'archivio utenti. Definisce i metodi per la creazione, l'aggiornamento, l'eliminazione e il recupero di utenti.
  • IUserClaimStore
    L'interfaccia IUserClaimStore<TUser> definisce i metodi implementati per abilitare le attestazioni utente. Contiene metodi per l'aggiunta, la rimozione e il recupero di attestazioni utente.
  • IUserLoginStore
    IUserLoginStore<TUser> Definisce i metodi implementati per abilitare provider di autenticazione esterni. Contiene metodi per aggiungere, rimuovere e recuperare gli account di accesso utente e un metodo per recuperare un utente in base alle informazioni di accesso.
  • IUserRoleStore
    L'interfaccia IUserRoleStore<TUser> definisce i metodi implementati per eseguire il mapping di un utente a un ruolo. Contiene metodi per aggiungere, rimuovere e recuperare i ruoli di un utente e un metodo per verificare se un utente è assegnato a un ruolo.
  • IUserPasswordStore
    L'interfaccia IUserPasswordStore<TUser> definisce i metodi implementati per rendere persistenti le password con hash. Contiene metodi per ottenere e impostare la password con hash e un metodo che indica se l'utente ha impostato una password.
  • IUserSecurityStampStore
    L'interfaccia IUserSecurityStampStore<TUser> definisce i metodi implementati per l'uso di un indicatore di sicurezza per indicare se le informazioni sull'account dell'utente sono state modificate. Questo stamp viene aggiornato quando un utente modifica la password o aggiunge o rimuove gli account di accesso. Contiene metodi per ottenere e impostare il timbro di sicurezza.
  • IUserTwoFactorStore
    L'interfaccia IUserTwoFactorStore<TUser> definisce i metodi implementati per supportare l'autenticazione a due fattori. Contiene metodi per ottenere e impostare se l'autenticazione a due fattori è abilitata per un utente.
  • IUser Telefono NumberStore
    L'interfaccia IUserPhoneNumberStore<TUser> definisce i metodi implementati per archiviare i numeri di telefono utente. Contiene metodi per ottenere e impostare il numero di telefono e se il numero di telefono è confermato.
  • IUserEmailStore
    L'interfaccia IUserEmailStore<TUser> definisce i metodi implementati per archiviare gli indirizzi di posta elettronica degli utenti. Contiene metodi per ottenere e impostare l'indirizzo di posta elettronica e se il messaggio di posta elettronica viene confermato.
  • IUserLockoutStore
    L'interfaccia IUserLockoutStore<TUser> definisce i metodi implementati per archiviare informazioni sul blocco di un account. Contiene metodi per tenere traccia dei tentativi di accesso non riusciti e dei blocchi.
  • IQueryableUserStore
    L'interfaccia IQueryableUserStore<TUser> definisce i membri implementati per fornire un archivio utenti su cui è possibile eseguire query.

Implementi solo le interfacce necessarie nell'app. Ad esempio:

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

IdentityUserClaim, IdentityUserLogin e IdentityUserRole

Lo Microsoft.AspNet.Identity.EntityFramework spazio dei nomi contiene implementazioni delle Identityclassi UserClaim, IdentityUserLogine IdentityUserRole . Se usi queste funzionalità, potresti voler creare versioni personalizzate di queste classi e definire le proprietà per la tua app. Tuttavia, a volte è più efficiente non caricare queste entità in memoria durante l'esecuzione di operazioni di base, ad esempio l'aggiunta o la rimozione dell'attestazione di un utente. Le classi dell'archivio back-end possono invece eseguire queste operazioni direttamente nell'origine dati. Ad esempio, il UserStore.GetClaimsAsync metodo può chiamare il userClaimTable.FindByUserId(user.Id) metodo per eseguire direttamente una query su tale tabella e restituire un elenco di attestazioni.

Personalizzare la classe del ruolo

Quando si implementa un provider di archiviazione dei ruoli, è possibile creare un tipo di ruolo personalizzato. Non è necessario implementare un'interfaccia specifica, ma deve avere un oggetto Id e in genere avrà una Name proprietà .

Di seguito è riportata una classe di ruolo di esempio:

using System;

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

Personalizzare l'archivio ruoli

È possibile creare una RoleStore classe che fornisce i metodi per tutte le operazioni sui dati sui ruoli. Questa classe equivale alla classe RoleStore<TRole> . RoleStore Nella classe si implementa eIRoleStore<TRole>, facoltativamente, l'interfaccia IQueryableRoleStore<TRole> .

  • IRoleStore<TRole>
    L'interfaccia IRoleStore<TRole> definisce i metodi da implementare nella classe dell'archivio ruoli. Contiene metodi per la creazione, l'aggiornamento, l'eliminazione e il recupero dei ruoli.
  • RoleStore<TRole>
    Per personalizzare RoleStore, creare una classe che implementa l'interfaccia IRoleStore<TRole> .

Riconfigurare l'app per l'uso di un nuovo provider di archiviazione

Dopo aver implementato un provider di archiviazione, configurare l'app per usarla. Se l'app usa il provider predefinito, sostituirla con il provider personalizzato.

  1. Rimuovere il Microsoft.AspNetCore.EntityFramework.Identity pacchetto NuGet.
  2. Se il provider di archiviazione risiede in un progetto o un pacchetto separato, aggiungere un riferimento.
  3. Sostituire tutti i riferimenti a Microsoft.AspNetCore.EntityFramework.Identity con un'istruzione using per lo spazio dei nomi del provider di archiviazione.
  4. Modificare il AddIdentity metodo per usare i tipi personalizzati. A questo scopo, è possibile creare metodi di estensione personalizzati. Per un esempio, vedere IdentityServiceCollectionExtensions .
  5. Se si usano i ruoli, aggiornare per RoleManager usare la RoleStore classe .
  6. Aggiornare il stringa di connessione e le credenziali alla configurazione dell'app.

Esempio:

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

Riferimenti