こんにちは。
クラウド事業本部コンサルティング&テクノロジー部の茨木です。
今回は、ASP.NET Core Identity です。
Visual Studio がテンプレート実装をしてくれますが、そのまま全て動かせる程に構成済み状態となっています。カスタマイズしようにも構成はDIで隠蔽されていて、初見ではどこから手を付けていいのか迷いますね。しかも SQL Server を前提に構成された上に、EntityFramework まで組み付けられてしまいますが、こんなの邪魔ですよね。ということで、テンプレートに手を加え、ASP.NET Core Identity に則りつつ、独自のデータストアで認証を実装してみたいと思います。
この記事はネクストスケープ クラウド事業本部 Advent Calendar 2017 の9日目となります。
プロジェクト作成
プロジェクトテンプレートからプロジェクトを作成します。ここで認証を設定しておくと、ASP.NET Core Identity が組み込まれたテンプレートが生成されます。
カスタマイズ
Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // Add application services. services.AddTransient<IEmailSender, EmailSender>(); services.AddMvc(); }
黄色の部分が要りません。消しちゃいましょう。
この状態で起動すると必要なクラスが欠損しているためDIの依存関係解決ができません。欠損している実装によって、概ね以下のパターンでエラーとなって失敗します。
IUserStore が解決できない
An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IUserStore`1[WebApplication1.Models.ApplicationUser]' while attempting to activate 'Microsoft.AspNetCore.Identity.AspNetUserManager`1[WebApplication1.Models.ApplicationUser]'.
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, ISet<Type> callSiteChain, ParameterInfo[] parameters, bool throwIfCallSiteNotFound)
IRoleStore が解決できない
An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IRoleStore`1[Microsoft.AspNetCore.Identity.IdentityRole]' while attempting to activate 'Microsoft.AspNetCore.Identity.AspNetRoleManager`1[Microsoft.AspNetCore.Identity.IdentityRole]'.
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, ISet<Type> callSiteChain, ParameterInfo[] parameters, bool throwIfCallSiteNotFound)
IUserStore の実装クラスが IUserPasswordStore を実装していない
An unhandled exception occurred while processing the request.
NotSupportedException: Store does not implement IUserPasswordStore<TUser>.
Microsoft.AspNetCore.Identity.UserManager.GetPasswordStore()
ここがテンプレート実装から削ってしまった部分なので、IUserStore, IUserPasswordStore, IRoleStore を実装して組み込む必要があります。
MyUserStore : IUserStore, IUserPasswordStore
独自のユーザー管理ストレージとして、ここでは簡易的にインメモリでユーザー管理をします。
class MyUserStore : IUserStore<ApplicationUser>, IUserPasswordStore<ApplicationUser> { private static readonly List<ApplicationUser> InMemoryStore = new List<ApplicationUser>(); public void Dispose() { } public async Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken) => user.Id; public async Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) => user.UserName; public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken) => user.UserName = userName; public async Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) => user.NormalizedUserName; public async Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken) => user.NormalizedUserName = normalizedName; public async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken) { InMemoryStore.Add(user); return IdentityResult.Success; } public async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken) { var index = InMemoryStore.FindIndex(a => a.Id == user.Id); InMemoryStore[index] = user; return IdentityResult.Success; } public async Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken) { var index = InMemoryStore.FindIndex(a => a.Id == user.Id); InMemoryStore.RemoveAt(index); return IdentityResult.Success; } public async Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken) => InMemoryStore.FirstOrDefault(a => a.Id == userId); public async Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) => InMemoryStore.FirstOrDefault(a => a.NormalizedUserName == normalizedUserName); public async Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken) => user.PasswordHash = passwordHash; public async Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken) => user.PasswordHash; public async Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken) => true; }
MyRoleStore : IRoleStore
今回Roleは使いませんが、インスタンスはDIできる必要があるのでクラス定義だけ用意します。
class MyRoleStore : IRoleStore<IdentityRole> { public void Dispose() { } public Task<IdentityResult> CreateAsync(IdentityRole role, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<IdentityResult> UpdateAsync(IdentityRole role, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<IdentityResult> DeleteAsync(IdentityRole role, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<string> GetRoleIdAsync(IdentityRole role, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<string> GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SetRoleNameAsync(IdentityRole role, string roleName, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<string> GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<IdentityRole> FindByIdAsync(string roleId, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task<IdentityRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) => throw new NotImplementedException(); }
Startup.cs
この2つのクラスをDIできるように登録しておきます。
public void ConfigureServices(IServiceCollection services) { services.AddIdentity<ApplicationUser, IdentityRole>() .AddUserStore<MyUserStore>() .AddRoleStore<MyRoleStore>() .AddDefaultTokenProviders(); // Add application services. services.AddTransient<IEmailSender, EmailSender>(); services.AddMvc(); }
※ApplicationUser は IdentityUser の派生クラスとして、テンプレートで生成されたものをそのまま利用します。 IdentityRole は SDK(Microsoft.AspNetCore.Identity) 内で定義済みのものをそのまま利用します。
ここまでくれば動きます
インメモリなので、実行を止めずに Register → Log out → Log in と操作してみましょう。 実装した MyUserStore にブレークポイントを仕掛けてみると、どの様に動いているかわかり易いと思います。
テンプレートの Controller の実装を見ると、たくさん例示されています。起点となる SignInManager, UserManager, RoleManager の3つのインスタンスをDIで取得すれば、必要な機能へアクセスできるはずです。
ASP.NET Core Identity の全体像
主要な構造を全体的に書き出してみました。そして、今回は水色で書いたあたりを扱いました。その他の部分は必要に応じて試せると思いますが、また次回、これを元にしたカスタマイズをやってみたいと思います。
環境
Visual Studio 2017 (ver 15.5)
ASP.NET Core 2.0
参考情報
マイクロソフトの公式ドキュメントはこちら
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/
弊社の配信事業本部でも Advent Calendar が行われております。
そちらにも ASP.NET Core Identity の記事がありますのでご紹介しておきます。
ASP.NET Core Identity ことはじめ 1