こんにちは。 クラウド事業本部コンサルティング&テクノロジー部の茨木です。
前回 に引き続き、ASP.NET Core Identity ネタでお送りします。
今回はテンプレートに頼らず、ゼロから ASP.NET Core Identity を構成してみます。
この記事は ネクストスケープ クラウド事業本部 Advent Calendar 2017 の22日目となります。
プロジェクト作成
プロジェクトテンプレートからプロジェクトを作成します。前回 とは異なり、ここで認証を設定しません。
ASP.NET Core Identity の組み込み
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<IdentityUser, IdentityRole>()
.AddUserStore<MyUserStore>()
.AddRoleStore<MyRoleStore>()
.AddDefaultTokenProviders();
//...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.UseAuthentication();
//...
}
AddIdentity がDI構成を行うために用意された拡張メソッドです。このメソッドひとつで、この様なデフォルト構造が構成されます。IdentityUser, IdentityRole は string をキー値とする標準部品です。実用では必要に応じて IdentityUser<Tkey>, IdentityRole<Tkey> を継承して拡張しますが、今回はこのまま使います。MyUserStore, MyRoleStore は今回実装するものです。(後述)
認証を使うので UseAuthentication もお忘れなく。
最小限の機能実装
前回、必要な実装は最小限の Interface を元にして行いました。今回も実装は最小限に留めますが、Interface ではなく標準で用意されている基底クラスである UserStoreBase, RoleStoreBase を継承して実装します。実際の開発においてもこの手法をとることになるでしょう。
MyUserStore.cs
ユーザー登録・ログインに必要な最小限です。やっていることは前回とほとんど変わりません。
class MyUserStore : UserStoreBase<
IdentityUser, string, IdentityUserClaim<string>,
IdentityUserLogin<string>, IdentityUserToken<string>>
{
private static readonly List<IdentityUser> InMemoryStore = new List<IdentityUser>();
private readonly IdentityErrorDescriber _describer;
public MyUserStore(IdentityErrorDescriber describer)
: base(describer)
{
_describer = describer;
}
public override async Task<IdentityResult> CreateAsync(
IdentityUser user, CancellationToken cancellationToken = new CancellationToken())
{
InMemoryStore.Add(user);
return IdentityResult.Success;
}
public override async Task<IdentityResult> UpdateAsync(
IdentityUser user, CancellationToken cancellationToken = new CancellationToken())
{
var index = InMemoryStore.FindIndex(a => a.Id == user.Id);
InMemoryStore[index] = user;
return IdentityResult.Success;
}
public override async Task<IdentityResult> DeleteAsync(
IdentityUser user, CancellationToken cancellationToken = new CancellationToken())
{
var index = InMemoryStore.FindIndex(a => a.Id == user.Id);
InMemoryStore.RemoveAt(index);
return IdentityResult.Success;
}
public override async Task<IdentityUser> FindByIdAsync(
string userId, CancellationToken cancellationToken = new CancellationToken())
=> InMemoryStore.FirstOrDefault(a => a.Id == userId);
public override async Task<IdentityUser> FindByNameAsync(string normalizedUserName,
CancellationToken cancellationToken = new CancellationToken())
=> InMemoryStore.FirstOrDefault(a => a.NormalizedUserName == normalizedUserName);
protected override async Task<IdentityUser> FindUserAsync(
string userId, CancellationToken cancellationToken)
=> InMemoryStore.FirstOrDefault(a => a.Id == userId);
protected override async Task<IdentityUserLogin<string>> FindUserLoginAsync(string userId,
string loginProvider, string providerKey, CancellationToken cancellationToken)
=> throw new NotImplementedException();
protected override async Task<IdentityUserLogin<string>> FindUserLoginAsync(
string loginProvider, string providerKey, CancellationToken cancellationToken)
=> throw new NotImplementedException();
public override async Task<IList<Claim>> GetClaimsAsync(
IdentityUser user, CancellationToken cancellationToken = new CancellationToken())
=> new Claim[0];
public override async Task AddClaimsAsync(IdentityUser user, IEnumerable<Claim> claims,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task RemoveClaimsAsync(IdentityUser user, IEnumerable<Claim> claims,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IList<IdentityUser>> GetUsersForClaimAsync(
Claim claim, CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
protected override async Task<IdentityUserToken<string>> FindTokenAsync(IdentityUser user,
string loginProvider, string name, CancellationToken cancellationToken)
=> throw new NotImplementedException();
protected override async Task AddUserTokenAsync(IdentityUserToken<string> token)
=> throw new NotImplementedException();
protected override async Task RemoveUserTokenAsync(IdentityUserToken<string> token)
=> throw new NotImplementedException();
public override IQueryable<IdentityUser> Users => throw new NotImplementedException();
public override async Task AddLoginAsync(IdentityUser user, UserLoginInfo login,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task RemoveLoginAsync(IdentityUser user, string loginProvider,
string providerKey,CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IList<UserLoginInfo>> GetLoginsAsync(IdentityUser user,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
}
MyRoleStore.cs
今回も Role は使用しないので、形式上存在するだけです。
class MyRoleStore : RoleStoreBase<
IdentityRole, string, IdentityUserRole<string>, IdentityRoleClaim<string>>
{
private readonly IdentityErrorDescriber _describer;
public MyRoleStore(IdentityErrorDescriber describer) : base(describer)
{
_describer = describer;
}
public override async Task<IdentityResult> CreateAsync(IdentityRole role,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IdentityResult> UpdateAsync(IdentityRole role,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IdentityResult> DeleteAsync(IdentityRole role,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IdentityRole> FindByIdAsync(string id,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IdentityRole> FindByNameAsync(string normalizedName,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task<IList<Claim>> GetClaimsAsync(IdentityRole role,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task AddClaimAsync(IdentityRole role, Claim claim,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override async Task RemoveClaimAsync(IdentityRole role, Claim claim,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();
public override IQueryable<IdentityRole> Roles
=> throw new NotImplementedException();
}
Controller
ここでは Controller.Action の一部だけを例示します。入力フォーム等は適当に用意してください。ユーザー登録やログインのテンプレートが生成されていないのでフォームの準備などは面倒ですが、前回 のように認証を設定してテンプレート生成したものからパクると便利かもです。
また、認証付きテンプレートの AccountController にはたくさんのサンプルが実装されており、ここを見ればほとんどの実装例が参照できると思います。
public class HomeController : Controller
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
// コンストラクタで各種ManagerをDIしてもらう
public HomeController(
SignInManager<IdentityUser> signInManager,
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager)
{
_signInManager = signInManager;
_userManager = userManager;
_roleManager = roleManager;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(
RegisterViewModel model, string returnUrl = null)
{
var createResult = await _userManager.CreateAsync(
new IdentityUser(model.Email)
{
Email = model.Email,
}, model.Password);
return this.Redirect(returnUrl);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(
LoginViewModel model, string returnUrl = null)
{
var signInResult = await _signInManager.PasswordSignInAsync(
model.Email, model.Password, model.RememberMe, false);
return this.Redirect(returnUrl);
}
[Authorize]
public IActionResult About()
{
var isSignedIn = _signInManager.IsSignedIn(this.User);
var userName = _userManager.GetUserName(this.User);
this.ViewData["Message"] = "Your application description page.";
this.ViewData["IsSignedIn"] = isSignedIn;
this.ViewData["UserName"] = userName;
return this.View();
}
}
動作実験
上記の例の実行結果です。
環境
Visual Studio 2017 (ver 15.5)
ASP.NET Core 2.0
おまけ
クラス図のソースを貼っておきます。
aspnet_core_identity_classes.vsdx