ASP.NET Core Identity をゼロから構築する

こんにちは。 クラウド事業本部コンサルティング&テクノロジー部の茨木です。

前回 に引き続き、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


ネクストスケープ企業サイトへ

NEXTSCAPE

検索する

タグ

メタデータ

投稿のRSS