ASP.NET Core Identity 用のカスタム記憶域プロバイダー

作成者: Steve Smith

ASP.NET Core Identity は、カスタム記憶域プロバイダーを作成してアプリに接続できる拡張可能なシステムです。 このトピックでは、ASP.NET Core Identity 用のカスタマイズされた記憶域プロバイダーを作成する方法について説明します。 独自の記憶域プロバイダーを作成するための重要な概念について説明しますが、詳細なチュートリアルではありません。 Identity モデルをカスタマイズするには、Identity モデルのカスタマイズに関するページを参照してください。

はじめに

ASP.NET Core Identity システムの既定では、Entity Framework Core を使用してユーザー情報が SQL Server データベースに格納されます。 このアプローチは多くのアプリに適しています。 ただし、別の永続化メカニズムまたはデータ スキーマを使用することもできます。 次に例を示します。

  • Azure Table Storage または別のデータ ストアを使用している。
  • データベース テーブルの構造が異なる。
  • Dapper などの別のデータ アクセス アプローチを使用したい場合がある。

これらのケースのそれぞれで、記憶域のメカニズムに合わせてカスタマイズしたプロバイダーを作成し、そのプロバイダーをアプリのプラグインとして使用できます。

ASP.NET Core Identity は [個別のユーザー アカウント] オプションによって Visual Studio のプロジェクト テンプレートに含まれます。

.NET Core CLI の使用時に、-au Individual を追加します。

dotnet new mvc -au Individual

ASP.NET Core Identity アーキテクチャ

ASP.NET Core Identity は、マネージャーおよびストアと呼ばれるクラスで構成されます。 "マネージャー" は、アプリ開発者が Identity ユーザーの作成などの操作を実行するために使用する高レベルのクラスです。 "ストア" は、ユーザーやロールなどのエンティティを永続化する方法を指定する下位レベルのクラスです。 ストアはリポジトリ パターンに従い、永続化メカニズムと密接に結び付いています。 マネージャーはストアから切り離されています。つまり、アプリケーション コードを変更せずに永続化メカニズム (構成を除く) を置き換えることができます。

次の図は、ストアとデータ アクセス層の通信が行われる一方で、Web アプリとマネージャーの対話がどのように行われるかを示しています。

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.

カスタム記憶域プロバイダーを作成するには、データ ソース、データ アクセス層、このデータ アクセス層と対話するストア クラス (上の図の緑と灰色のボックス) を作成します。 マネージャー、またはそれらと対話するアプリ コード (上の青いボックス) をカスタマイズする必要はありません。

UserManager または RoleManager の新しいインスタンスを作成するときに、ユーザー クラスの種類を指定し、ストア クラスのインスタンスを引数として渡します。 この方法を使用すると、カスタマイズしたクラスを ASP.NET Core のプラグインとして使用できます。

新しい記憶域プロバイダーを使用するようにアプリを再構成する」には、カスタマイズしたストアで UserManagerRoleManager のインスタンスを作成する方法が示されています。

ASP.NET Core Identity のストアのデータ型

ASP.NET Core Identity のデータ型の詳細については、以下のセクションを参照してください。

ユーザー

Web サイトの登録済みユーザー。 IdentityUser 型は、拡張または独自のカスタム型の例として使用できます。 独自のカスタム ID 記憶域ソリューションを実装するために、特定の型から継承する必要はありません。

ユーザー要求

ユーザーの ID を表す、ユーザーに関するステートメント (または要求) のセット。 ロールで実現できるよりも優れたユーザー ID の表現を実現できます。

ユーザー ログイン

ユーザーのログイン時に使用する外部認証プロバイダー (Facebook や Microsoft アカウントなど) に関する情報。

ロール

サイトの認可グループ。 ロール ID とロール名 ("Admin" や "Employee" など) が含まれます。

データ アクセス層

このトピックは、使用する永続化メカニズムと、そのメカニズムのエンティティを作成する方法を理解していることを前提としています。 このトピックでは、リポジトリまたはデータ アクセス クラスを作成する方法の詳細については説明しません。ASP.NET Core Identity を使用する場合の設計上の判断に関する提案をいくつか示します。

カスタマイズされたストア プロバイダーのデータ アクセス層を設計する際には、高い自由度があります。 必要なのは、アプリで使用する予定の機能の永続化メカニズムを作成することだけです。 たとえば、アプリでロールを使用しない場合は、ロールまたはユーザー ロールの関連付け用の記憶域を作成する必要はありません。 テクノロジと既存のインフラストラクチャのために、ASP.NET Core Identity の既定の実装とは大きく異なる構造が必要な場合があります。 ご自身の記憶域の実装の構造を処理するロジックをデータ アクセス層で提供します。

データ アクセス層では、ASP.NET Core Identity からデータ ソースにデータを保存するロジックが提供されます。 カスタマイズした記憶域プロバイダーのデータ アクセス層には、ユーザーとロールの情報を格納する次のクラスが含まれる場合があります。

Context クラス

永続化メカニズムに接続してクエリを実行するための情報をカプセル化します。 いくつかのデータ クラスには、このクラスのインスタンスが必要であり、それは通常、依存関係の挿入によって提供されます。

ユーザー記憶域

ユーザー情報 (ユーザー名やパスワード ハッシュなど) を格納および取得します。

ロール記憶域

ロール情報 (ロール名など) を格納および取得します。

UserClaims 記憶域

ユーザー要求情報 (要求の種類や値など) を格納および取得します。

UserLogins 記憶域

ユーザー ログイン情報 (外部認証プロバイダーなど) を格納および取得します。

UserRole 記憶域

どのユーザーにどのロールが割り当てられているかを格納および取得します。

ヒント: アプリで使用することを意図しているクラスのみを実装します。

データ アクセス クラスで、永続化メカニズムのデータ操作を実行するコードを指定します。 たとえば、カスタム プロバイダー内で、"ストア" クラスに新しいユーザーを作成する次のコードを使用できます。

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

ユーザーを作成するための実装ロジックは、_usersTable.CreateAsync メソッドに含まれています。これを次に示します。

ユーザー クラスのカスタマイズ

記憶域プロバイダーを実装する場合は、IdentityUser クラスと同等のユーザー クラスを作成します。

少なくとも、ご自身のユーザー クラスには IdUserName プロパティを含める必要があります。

IdentityUser クラスは、要求された操作を実行するときに UserManager が呼び出すプロパティを定義します。 Id プロパティの既定の型は文字列ですが、IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken> から継承し、別の型を指定できます。 このフレームワークでは、記憶域の実装でデータ型の変換が処理される必要があります。

ユーザー ストアのカスタマイズ

ユーザーに対するすべてのデータ操作のメソッドを提供する UserStore クラスを作成します。 このクラスは、UserStore<TUser> クラスと同じです。 ご自身の UserStore クラスで、IUserStore<TUser> と必要なオプションのインターフェイスを実装します。 オプションのインターフェイスのうち、どれを実装するかは、ご自身のアプリで提供する機能に基づいて選択します。

省略可能なインターフェイス

オプションのインターフェイスは、IUserStore<TUser> から継承されます。 サンプル アプリで、部分的に実装されたサンプル ユーザー ストアを確認できます。

UserStore クラス内で、操作を実行するために作成したデータ アクセス クラスを使用します。 それらは、依存関係の挿入を使用して渡されます。 たとえば、Dapper 実装の SQL Server では、UserStore クラスに、DapperUsersTable のインスタンスを使用して新しいレコードを挿入する CreateAsync メソッドがあります。

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

ユーザー ストアをカスタマイズするときに実装するインターフェイス

  • IUserStore
    IUserStore<TUser> インターフェイスは、ユーザー ストアで実装する必要がある唯一のインターフェイスです。 ユーザーの作成、更新、削除、取得を行うためのメソッドを定義します。
  • IUserClaimStore
    IUserClaimStore<TUser> インターフェイスには、ユーザー要求を有効にするために実装するメソッドが定義されています。 ユーザー要求を追加、削除、取得するためのメソッドが含まれています。
  • IUserLoginStore
    IUserLoginStore<TUser> には、外部認証プロバイダーを有効にするために実装するメソッドが定義されています。 ユーザー ログインを追加、削除、取得するためのメソッドと、ログイン情報に基づいてユーザーを取得するためのメソッドが含まれています。
  • IUserRoleStore
    IUserRoleStore<TUser> インターフェイスには、ユーザーをロールにマップするために実装するメソッドが定義されています。 ユーザーのロールを追加、削除、取得するメソッドと、ユーザーがロールに割り当てられているかどうかを確認するメソッドが含まれています。
  • IUserPasswordStore
    IUserPasswordStore<TUser> インターフェイスには、ハッシュされたパスワードを永続化するために実装するメソッドが定義されています。 ハッシュされたパスワードを取得および設定するためのメソッドと、ユーザーがパスワードを設定したかどうかを示すメソッドが含まれています。
  • IUserSecurityStampStore
    IUserSecurityStampStore<TUser> インターフェイスには、ユーザーのアカウント情報が変更されたかどうかを示すセキュリティ スタンプを使用するために実装するメソッドが定義されています。 このスタンプは、ユーザーがパスワードを変更したとき、またはログインを追加または削除したときに更新されます。 セキュリティ スタンプを取得および設定するためのメソッドが含まれています。
  • IUserTwoFactorStore
    IUserTwoFactorStore<TUser> インターフェイスには、2 要素認証をサポートするために実装するメソッドが定義されています。 ユーザーに対して 2 要素認証が有効にされているかどうかを取得および設定するためのメソッドが含まれています。
  • IUserPhoneNumberStore
    IUserPhoneNumberStore<TUser> インターフェイスには、ユーザーの電話番号を格納するために実装するメソッドが定義されています。 電話番号と、電話番号が確認済みかどうかを取得および設定するためのメソッドが含まれています。
  • IUserEmailStore
    IUserEmailStore<TUser> インターフェイスには、ユーザーのメール アドレスを格納するために実装するメソッドが定義されています。 メール アドレスと、メールが確認済みかどうかを取得および設定するためのメソッドが含まれています。
  • IUserLockoutStore
    IUserLockoutStore<TUser> インターフェイスには、アカウントのロックに関する情報を格納するために実装するメソッドが定義されています。 失敗したアクセスの試行とロックアウトを追跡するためのメソッドが含まれています。
  • IQueryableUserStore
    IQueryableUserStore<TUser> インターフェイスには、クエリ可能なユーザー ストアを提供するために実装するメンバーが定義されています。

アプリで必要なインターフェイスだけを実装します。 次に例を示します。

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

IdentityUserClaim、IdentityUserLogin、IdentityUserRole

Microsoft.AspNet.Identity.EntityFramework 名前空間には、IdentityUserClaimIdentityUserLogin、および IdentityUserRole クラスが含まれています。 これらの機能を使用する場合は、これらのクラスの独自のバージョンを作成し、ご自身のアプリのプロパティを定義することができます。 ただし、基本操作 (ユーザーの要求の追加や削除など) を実行するときは、これらのエンティティをメモリに読み込まない方が効率的な場合もあります。 代わりに、バックエンド ストア クラスを使用して、データ ソースに対してこれらの操作を直接実行できます。 たとえば、UserStore.GetClaimsAsync メソッドで userClaimTable.FindByUserId(user.Id) メソッドを呼び出して、そのテーブルに対してクエリを直接実行し、要求の一覧を返すことができます。

ロール クラスのカスタマイズ

ロール記憶域プロバイダーを実装する場合は、カスタムのロールの種類を作成できます。 特定のインターフェイスを実装する必要はありませんが、Id は必要であり、通常は Name プロパティを使用します。

次に、ロール クラスの例を示します。

using System;

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

ロール ストアのカスタマイズ

ロールに対するすべてのデータ操作のメソッドを提供する RoleStore クラスを作成できます。 このクラスは、RoleStore<TRole> クラスと同等です。 RoleStore クラスには、IRoleStore<TRole> と、オプションで IQueryableRoleStore<TRole> インターフェイスを実装します。

  • IRoleStore<TRole>
    IRoleStore<TRole> インターフェイスには、ロール ストア クラスで実装するメソッドが定義されています。 ロールの作成、更新、削除、取得を行うためのメソッドが含まれています。
  • RoleStore<TRole>
    RoleStore をカスタマイズするには、IRoleStore<TRole> インターフェイスを実装するクラスを作成します。

新しい記憶域プロバイダーを使用するようにアプリを再構成する

記憶域プロバイダーを実装したら、それを使用するようにアプリを構成します。 アプリで既定のプロバイダーを使用している場合は、カスタム プロバイダーに置き換えます。

  1. Microsoft.AspNetCore.EntityFramework.Identity NuGet パッケージを削除します。
  2. 記憶域プロバイダーが別のプロジェクトまたはパッケージに存在する場合は、それへの参照を追加します。
  3. Microsoft.AspNetCore.EntityFramework.Identity へのすべての参照を、記憶域プロバイダーの名前空間の using ステートメントに置き換えます。
  4. カスタム型を使用するように AddIdentity メソッドを変更します。 この目的のために独自の拡張メソッドを作成できます。 例については、「IdentityServiceCollectionExtensions」を参照してください。
  5. ロールを使用する場合は、ご自身の RoleStore クラスを使用するように RoleManager を更新します。
  6. 接続文字列と資格情報をご自身のアプリの構成に更新します。

例:

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