Benutzerdefinierte Speicheranbieter für ASP.NET Core Identity

Von Steve Smith

ASP.NET Core Identity ist ein erweiterbares System, mit dem Sie einen benutzerdefinierten Speicheranbieter erstellen und mit Ihrer App verbinden können. In diesem Thema wird beschrieben, wie Sie einen benutzerdefinierten Speicheranbieter für ASP.NET Core Identity erstellen. Es werden die wichtigsten Konzepte für die Erstellung eines eigenen Speicheranbieters behandelt, aber es handelt sich nicht um eine detaillierte Schrittanleitung. Informationen zum Anpassen eines Identity-Modells finden Sie unter Identity-Modellanpassung.

Einführung

Standardmäßig speichert das ASP.NET Core Identity-System Benutzer*innen unter Verwendung von Entity Framework Core in einer SQL Server-Datenbank. Dieser Ansatz ist für viele Apps gut geeignet. Möglicherweise ziehen Sie es jedoch vor, einen anderen Persistenzmechanismus oder ein anderes Datenschema zu verwenden. Beispiel:

  • Sie verwenden Azure Table Storage oder einen anderen Datenspeicher.
  • Ihre Datenbanktabellen weisen eine andere Struktur auf.
  • Sie möchten vielleicht einen anderen Ansatz für den Datenzugriff verwenden, beispielsweise Dapper.

In jedem dieser Fälle können Sie einen benutzerdefinierten Anbieter für Ihren Speichermechanismus schreiben und diesen Anbieter in Ihre App integrieren.

ASP.NET Core Identity ist in Projektvorlagen in Visual Studio mit der Option „Einzelne Benutzerkonten“ enthalten.

Wenn Sie die .NET Core CLI verwenden, fügen Sie -au Individual hinzu:

dotnet new mvc -au Individual

Architektur von ASP.NET Core Identity

ASP.NET Core Identity besteht aus Klassen, die als Manager und Stores bezeichnet werden. Manager-Klassen sind allgemeine Klassen, die Entwickler*innen von Apps zur Durchführung von Operationen verwenden, z. B. zum Erstellen von Identity-Benutzer*innen. Store-Klassen sind untergeordnete Klassen, die angeben, wie Entitäten (z. B. Benutzer*innen und Rollen) dauerhaft gespeichert werden. Store-Klassen folgen dem Repositorymuster und sind eng an den Persistenzmechanismus gekoppelt. Die Manager-Klassen sind von den Store-Klassen entkoppelt, d. h. Sie können den Persistenzmechanismus ersetzen, ohne Ihren Anwendungscode zu ändern (außer bei der Konfiguration).

Das folgende Diagramm zeigt, wie eine Web-App mit den Manager-Klassen interagiert, während die Store-Klassen mit der Datenzugriffsebene interagieren.

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.

Wenn Sie einen benutzerdefinierten Speicheranbieter erstellen möchten, erstellen Sie die Datenquelle, die Datenzugriffsebene und die Speicherklassen, die mit dieser Datenzugriffsschicht interagieren (im obigen Diagramm als grüne und graue Kästen dargestellt). Sie müssen weder die Manager-Klassen noch den Code Ihrer App anpassen, der mit diesen interagiert (oben als blaue Kästen dargestellt).

Beim Erstellen einer neuen Instanz von UserManager oder RoleManager geben Sie den Typ der Benutzerklasse an und übergeben eine Instanz der Store-Klasse als Argument. Dieser Ansatz ermöglicht es Ihnen, Ihre benutzerdefinierten Klassen in ASP.NET Core zu integrieren.

Unter Neukonfigurieren der App für die Verwendung eines neuen Speicheranbieters wird gezeigt, wie Sie UserManager und RoleManager mit einer angepassten Speicher instanziieren.

ASP.NET Core Identity-Speicherdatentypen

Die ASP.NET Core Identity-Datentypen werden in den folgenden Abschnitten näher erläutert:

Benutzer

Registrierte Benutzer*innen Ihrer Website. Der IdentityUser-Typ kann erweitert oder als Beispiel für einen eigenen benutzerdefinierten Typ verwendet werden. Sie müssen nicht von einem bestimmten Typ erben, um eine eigene benutzerdefinierte Identitätsspeicherlösung zu implementieren.

Benutzeransprüche

Ein Satz von Anweisungen (oder Ansprüchen) zu den Benutzer*innen, die die Identität des Benutzers repräsentieren. Die Identität der Benutzer*innen kann umfassender festgelegt werden, als dies durch Rollen möglich ist.

Benutzeranmeldungen

Informationen zum externen Authentifizierungsanbieter (wie z. B. Facebook oder ein Microsoft-Konto), der bei der Benutzeranmeldung verwendet werden soll. Beispiel

Rollen

Autorisierungsgruppen für Ihre Website. Umfasst die Rollen-ID und den Rollennamen (wie etwa „Admin“ oder „Mitarbeiter*in“). Beispiel

Datenzugriffsebene

In diesem Thema wird davon ausgegangen, dass Sie mit dem zu verwendenden Persistenzmechanismus vertraut sind und wissen, wie Sie Entitäten für diesen Mechanismus erstellen. Dieses Thema enthält keine Details zur Erstellung von Repositorys oder Datenzugriffsklassen, sondern bietet einige Vorschläge zu Entwurfsentscheidungen bei der Arbeit mit ASP.NET Core Identity.

Beim Entwurf der Datenzugriffsebene für einen kundenspezifischen Speicheranbieter haben Sie großen Spielraum. Sie müssen lediglich Persistenzmechanismen für Features erstellen, die Sie in Ihrer App verwenden möchten. Wenn Sie beispielsweise in Ihrer App keine Rollen verwenden, müssen Sie keinen Speicher für Rollen oder Benutzerrollenzuordnungen erstellen. Ihre Technologie und die vorhandene Infrastruktur erfordern möglicherweise eine Struktur, die sich stark von der Standardimplementierung von ASP.NET Core Identity unterscheidet. In Ihrer Datenzugriffsebene stellen Sie die Logik bereit, um mit der Struktur Ihrer Speicherimplementierung zu arbeiten.

Die Datenzugriffsebene stellt die Logik zum Speichern der Daten von ASP.NET Core Identity in einer Datenquelle bereit. Die Datenzugriffsebene für Ihren benutzerdefinierten Speicheranbieter könnte die folgenden Klassen enthalten, um Benutzer- und Rolleninformationen zu speichern.

Context-Klasse

Kapselt die Informationen, um eine Verbindung mit Ihrem Persistenzmechanismus herzustellen und Abfragen auszuführen. Einige Datenklassen benötigen eine Instanz dieser Klasse, die in der Regel durch Abhängigkeitsinjektion bereitgestellt wird. Beispiel:

Benutzerspeicher

Speichert und ruft Benutzerinformationen ab (z. B. Benutzername und Kennworthash). Beispiel

Rollenspeicher

Speichert und ruft Rolleninformationen ab (z. B. den Rollennamen). Beispiel

UserClaims-Speicher

Speichert und ruft Informationen zum Benutzeranspruch ab (z. B. Anspruchstyp und -wert). Beispiel

UserLogins-Speicher

Speichert und ruft Informationen zur Benutzeranmeldung ab (z. B. einen externen Authentifizierungsanbieter). Beispiel

UserRole-Speicher

Speichert und ruft Informationen dazu ab, welche Rollen welchen Benutzer*innen zugewiesen sind. Beispiel

TIPP: Implementieren Sie nur die Klassen, die Sie in Ihrer App verwenden möchten.

Geben Sie in den Datenzugriffsklassen Code zur Ausführung von Datenoperationen für Ihren Persistenzmechanismus an. In einem benutzerdefinierten Anbieter könnten Sie beispielsweise den folgenden Code verwenden, um neue Benutzer*innen in der Klasse store zu erstellen:

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

Die Implementierungslogik für die Benutzererstellung ist in der _usersTable.CreateAsync-Methode enthalten, wie nachfolgend gezeigt.

Anpassen der Benutzerklasse

Erstellen Sie beim Implementieren eines Speicheranbieters eine Benutzerklasse, die der IdentityUser-Klasse entspricht.

Ihre Benutzerklasse muss mindestens eine Id- und eine UserName-Eigenschaft enthalten.

Die IdentityUser-Klasse definiert die Eigenschaften, die UserManager beim Ausführen der angeforderten Operationen aufruft. Der Standardtyp der Id-Eigenschaft ist eine Zeichenfolge, aber Sie können von IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken> erben und einen anderen Typ angeben. Das Framework erwartet, dass die Speicherimplementierung Datentypkonvertierungen verarbeiten kann.

Anpassen des Benutzerspeichers

Erstellen Sie eine UserStore-Klasse, die die Methoden für sämtliche Datenoperationen bereitstellt, die für Benutzer*innen durchgeführt werden. Diese Klasse entspricht der UserStore<TUser>-Klasse. Implementieren Sie in Ihrer UserStore-Klasse IUserStore<TUser> und die benötigten optionalen Schnittstellen. Sie wählen abhängig von der in Ihrer App bereitgestellten Funktionalität aus, welche optionalen Schnittstellen implementiert werden sollen.

Optionale Schnittstellen

Die optionalen Schnittstellen erben von IUserStore<TUser>. Ein teilweise implementierter beispielhafter Benutzerspeicher wird in der Beispiel-App gezeigt.

Innerhalb der UserStore-Klasse verwenden Sie die von Ihnen erstellten Datenzugriffsklassen, um Operationen auszuführen. Diese werden mithilfe der Abhängigkeitsinjektion übergeben. In der SQL Server-Implementierung mit Dapper umfasst die UserStore-Klasse zum Beispiel die CreateAsync-Methode, die eine Instanz von DapperUsersTable verwendet, um einen neuen Datensatz einzufügen:

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

Zu implementierende Schnittstellen beim Anpassen des Benutzerspeichers

  • IUserStore
    Die IUserStore<TUser>-Schnittstelle ist die einzige Schnittstelle, die Sie im Benutzerspeicher implementieren müssen. Sie definiert Methoden zum Erstellen, Aktualisieren, Löschen und Abrufen von Benutzer*innen.
  • IUserClaimStore
    Die IUserClaimStore<TUser>-Schnittstelle definiert die Methoden, die Sie zur Unterstützung von Benutzeransprüche implementieren. Sie enthält Methoden zum Hinzufügen, Entfernen und Abrufen von Benutzeransprüchen.
  • IUserLoginStore
    Die IUserLoginStore<TUser>-Schnittstelle definiert die Methoden, die Sie zum Aktivieren externer Authentifizierungsanbieter implementieren. Sie enthält Methoden zum Hinzufügen, Entfernen und Abrufen von Benutzer*innen und eine Methode zum Abrufen von Benutzer*innen anhand der Anmeldeinformationen.
  • IUserRoleStore
    Die IUserRoleStore<TUser>-Schnittstelle definiert die Methoden, die Sie implementieren, um Benutzer*innen einer Rolle zuzuordnen. Sie enthält Methoden zum Hinzufügen, Entfernen und Abrufen von Benutzerrollen sowie eine Methode zur Überprüfung, ob Benutzer*innen einer Rolle zugewiesen sind.
  • IUserPasswordStore
    Die IUserPasswordStore<TUser>-Schnittstelle definiert die Methoden, die Sie zum dauerhaften Speichern von gehashten Kennwörtern implementieren. Sie enthält Methoden zum Abrufen und Festlegen des gehashten Kennworts sowie eine Methode, die anzeigt, ob die Benutzer*innen ein Kennwort festgelegt haben.
  • IUserSecurityStampStore
    Die IUserSecurityStampStore<TUser>-Schnittstelle definiert die Methoden, die Sie zur Verwendung eines Sicherheitsstempels implementieren, der anzeigt, ob sich die Kontoinformationen von Benutzer*innen geändert haben. Dieser Stempel wird aktualisiert, wenn Benutzer*innen das Kennwort ändern oder Anmeldungen hinzufügen oder entfernen. Die Schnittstelle enthält Methoden zum Abrufen und Festlegen des Sicherheitsstempels.
  • IUserTwoFactorStore
    Die IUserTwoFactorStore<TUser>-Schnittstelle definiert die Methoden, die Sie zur Unterstützung der Zwei-Faktor-Authentifizierung implementieren. Sie enthält Methoden zum Abrufen und Festlegen, ob die Zwei-Faktor-Authentifizierung für Benutzer*innen aktiviert ist.
  • IUserPhoneNumberStore
    Die IUserPhoneNumberStore<TUser>-Schnittstelle definiert die Methoden, die Sie zum Speichern der Benutzertelefonnummern implementieren. Sie enthält Methoden zum Abrufen und Festlegen der Telefonnummer sowie zur Überprüfung, ob die Telefonnummer bestätigt ist.
  • IUserEmailStore
    Die IUserEmailStore<TUser>-Schnittstelle definiert die Methoden, die Sie zum Speichern der E-Mail-Adressen von Benutzer*innen implementieren. Sie enthält Methoden zum Abrufen und Festlegen der E-Mail-Adresse sowie zur Überprüfung, ob die E-Mail-Adresse bestätigt ist.
  • IUserLockoutStore
    Die IUserLockoutStore<TUser>-Schnittstelle definiert die Methoden, die Sie implementieren, um Informationen zur Sperrung eines Kontos zu speichern. Sie enthält Methoden zur Nachverfolgung von fehlgeschlagenen Zugriffsversuchen und von Sperrungen.
  • IQueryableUserStore
    Die IQueryableUserStore<TUser>-Schnittstelle definiert die Member, die Sie zum Bereitstellen eines abfragbaren Benutzerspeichers implementieren.

Sie implementieren nur die Schnittstellen, die in Ihrer App benötigt werden. Beispiel:

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

IdentityUserClaim, IdentityUserLogin und IdentityUserRole

Der Namespace Microsoft.AspNet.Identity.EntityFramework enthält Implementierungen der Klassen IdentityUserClaim, IdentityUserLogin und IdentityUserRole. Wenn Sie diese Features verwenden, möchten Sie vielleicht eigene Versionen dieser Klassen erstellen und die Eigenschaften für Ihre App definieren. Mitunter ist es jedoch effizienter, diese Entitäten nicht in den Speicher zu laden, wenn grundlegende Operationen durchgeführt werden (z. B. das Hinzufügen oder Entfernen eines Benutzeranspruchs). Stattdessen können die Back-End-Speicherklassen diese Operationen direkt in der Datenquelle ausführen. Zum Beispiel kann die Methode UserStore.GetClaimsAsync die Methode userClaimTable.FindByUserId(user.Id) aufrufen, um eine Abfrage direkt in dieser Tabelle auszuführen und eine Liste der Ansprüche zurückzugeben.

Anpassen der Rollenklasse

Wenn Sie einen Rollenspeicheranbieter implementieren, können Sie einen benutzerdefinierten Rollentyp erstellen. Dieser muss keine bestimmte Schnittstelle implementieren, aber er muss über eine Id verfügen und weist in der Regel eine Name-Eigenschaft auf.

Im Folgenden finden Sie ein Beispiel für eine Rollenklasse:

using System;

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

Anpassen des Rollenspeichers

Sie können eine RoleStore-Klasse erstellen, die die Methoden für sämtliche Datenoperationen bereitstellt, die für Rollen durchgeführt werden. Diese Klasse entspricht der Klasse RoleStore<TRole>-Klasse. In der RoleStore-Klasse implementieren Sie die Schnittstelle IRoleStore<TRole> und optional die Schnittstelle IQueryableRoleStore<TRole>.

  • IRoleStore<TRole>
    Die IRoleStore<TRole>-Schnittstelle definiert die Methoden, die in der Rollenspeicherklasse implementiert werden müssen. Sie definiert Methoden zum Erstellen, Aktualisieren, Löschen und Abrufen von Rollen.
  • RoleStore<TRole>
    Zum Anpassen von RoleStore erstellen Sie eine Klasse, die die IRoleStore<TRole>-Schnittstelle implementiert.

Neukonfigurieren der App zur Verwendung eines neuen Speicheranbieters

Sobald Sie einen Speicheranbieter implementiert haben, konfigurieren Sie Ihre App zur Nutzung dieses Anbieters. Wenn Ihre App bisher den Standardanbieter verwendet, ersetzen Sie ihn durch Ihren eigenen Anbieter.

  1. Entfernen Sie das Microsoft.AspNetCore.EntityFramework.Identity-NuGet-Paket.
  2. Wenn sich der Speicheranbieter in einem separaten Projekt oder Paket befindet, fügen Sie einen Verweis darauf hinzu.
  3. Ersetzen Sie alle Verweise auf Microsoft.AspNetCore.EntityFramework.Identity durch eine using-Anweisung für den Namespace Ihres Speicheranbieters.
  4. Ändern Sie die AddIdentity-Methode, um die benutzerdefinierten Typen zu verwenden. Zu diesem Zweck können Sie eigene Erweiterungsmethoden erstellen. Ein Beispiel finden Sie unter IdentityServiceCollectionExtensions.
  5. Wenn Sie Rollen verwenden, aktualisieren Sie RoleManager, um Ihre RoleStore-Klasse zu verwenden.
  6. Aktualisieren Sie die Verbindungszeichenfolge und die Anmeldedaten in der Konfiguration Ihrer App.

Beispiel:

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

References