かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ASP.NET Identityで独自データストアからデータを取得する(ログインからロールまで)

okazuki.hatenablog.com

上記記事でASP.NET Identityで完全に独自のデータストアからユーザーのデータを取ってくる方法を紹介しました。今回は、ちょっとしたシステムなら必要になってくるロールの機能をカスタマイズする方法を紹介したいと思います。

自分が思い出すのも兼ねて、ASP.NET Identityを使ってログイン処理もあわせて作っていこうと思います。

ASP.NET Webアプリケーションの作成と参照の設定

まず、ASP.NET Web アプリケーションを新規作成します。ASP.NET 5は、まだちょっと時期的にはやそうなので、今回はASP.NET 4.6のEmptyを選択します。フォルダー及びコア機能を追加するでMVCにチェックを入れてプロジェクトを作成します。

ASP.NET Identity関連のNuGetパッケージを追加します。以下の2つのパッケージを追加します。

  • Microsoft.AspNet.Identity.Owin
  • Microsoft.Owin.Host.SystemWeb

スタートアップの作成

次に、起動時の処理を作成します。「OWIN Startup クラス」をStartupという名前で作成します。UseCoolieAuthenticationを呼び出して認証をCookieを使ってやるということと、ログイン画面のパスを指定します。

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;

[assembly: OwinStartup(typeof(SampleApp.Startup))]

namespace SampleApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Home/Login")
            });
        }
    }
}

認証処理の作成

ASP.NET Identityのクラスやインターフェースを拡張して認証処理を作っていきます。

ユーザーの作成

まずは、ユーザーを作成します。ユーザーはIUser>TKey<インターフェースを実装して作成します。IDとUserNameだけをもったシンプルなインターフェースです。これに今回はPasswordプロパティを追加しています。

public class ApplicationUser : IUser<string>
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string UserName { get; set; }
    public string Password { get; set; }
}

UserManagerの実装

次に、UserManagerを実装します。こいつを使ってログイン処理とかを行います。

public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(IUserStore<ApplicationUser> store) : base(store)
    {
    }
}

ロールの作成

次に、ロールのクラスを作成します。ロールはIRole>TKey<インターフェースを実装します。こいつもIUserと同じくらいシンプルで、IdとNameプロパティを持っているだけです。今回は、それをさくっと実装します。

public class ApplicationRole : IRole<string>
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    public string Name { get; set; }
}

RoleManagerの実装

次にRoleManagerを実装します。Userに対するUserManagerのような関係のものになります。

public class ApplicationRoleManager : RoleManager<ApplicationRole>
{
    public ApplicationRoleManager(IRoleStore<ApplicationRole, string> store) : base(store)
    {
    }
}

SignInManagerの実装(オプション)

こいつは最小構成では実装しなくてもいいのですが、まぁあると便利なので実装しておきます。サインイン処理とかをやってくれるものになります。

public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
    public ApplicationSignInManager(
        UserManager<ApplicationUser, string> userManager,
        IAuthenticationManager authenticationManager) : base(userManager, authenticationManager)
    {
    }
}

UserStoreの作成

これまでは、クラスを継承してコンストラクタを作っておしまいという簡単な決まり文句の実装でした。これから作るUserStoreが実際のデータアクセスなどを行うクラスになります。

ユーザー管理を行うIUserStoreインターフェースにパスワードの管理を行うIUserPasswordStoreインターフェースにロールの管理を行うIRoleStoreインターフェースにユーザーとロールの紐づけを管理するIRoleSoreインターフェースを実装します。それぞれ、基本的に単純なCRUDの処理になります。ここで、各々のデータストアに対して処理を行うといいでしょう。

ここではインメモリのリストに対して処理を行っています。

public class UserStore :
    IUserStore<ApplicationUser>,
    IUserStore<ApplicationUser, string>,
    IUserPasswordStore<ApplicationUser, string>,
    IUserRoleStore<ApplicationUser, string>,
    IRoleStore<ApplicationRole, string>
{
    /// <summary>
    /// ユーザー保存先
    /// </summary>
    private static List<ApplicationUser> Users { get; } = new List<ApplicationUser>();
    /// <summary>
    /// ロールの保存先
    /// </summary>
    private static List<ApplicationRole> Roles { get; } = new List<ApplicationRole>();
    /// <summary>
    /// ユーザーとロールのリレーション
    /// </summary>
    private static List<Tuple<string, string>> UserRoleMap { get; } = new List<Tuple<string, string>>();

    /// <summary>
    /// ユーザーをロールに追加する
    /// </summary>
    public Task AddToRoleAsync(ApplicationUser user, string roleName)
    {
        Debug.WriteLine(nameof(AddToRoleAsync));
        var role = Roles.FirstOrDefault(x => x.Name == roleName);
        if (role == null) { throw new InvalidOperationException(); }

        var userRoleMap = UserRoleMap.FirstOrDefault(x => x.Item1 == user.Id && x.Item2 == role.Id);
        if (userRoleMap == null)
        {
            UserRoleMap.Add(Tuple.Create(user.Id, role.Id));
        }

        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ユーザーを作成する
    /// </summary>
    public Task CreateAsync(ApplicationUser user)
    {
        Debug.WriteLine(nameof(CreateAsync));
        Users.Add(user);
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ユーザーを削除する
    /// </summary>
    public Task DeleteAsync(ApplicationUser user)
    {
        Debug.WriteLine(nameof(DeleteAsync));
        Users.Remove(Users.First(x => x.Id == user.Id));
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// 何か後始末(DbContextとかDBのコネクションとか作ってたら後始末をする)
    /// </summary>
    public void Dispose()
    {
        Debug.WriteLine(nameof(Dispose));
    }

    /// <summary>
    /// ユーザーをId指定で取得する
    /// </summary>
    public Task<ApplicationUser> FindByIdAsync(string userId)
    {
        Debug.WriteLine(nameof(FindByIdAsync));
        var result = Users.FirstOrDefault(x => x.Id == userId);
        return Task.FromResult(result);
    }

    /// <summary>
    /// ユーザーをユーザー名で取得する
    /// </summary>
    /// <param name="userName"></param>
    /// <returns></returns>
    public Task<ApplicationUser> FindByNameAsync(string userName)
    {
        Debug.WriteLine(nameof(FindByNameAsync));
        var result = Users.FirstOrDefault(x => x.UserName == userName);
        return Task.FromResult(result);
    }

    /// <summary>
    /// ユーザーからパスワードのハッシュを取得する
    /// </summary>
    public Task<string> GetPasswordHashAsync(ApplicationUser user)
    {
        Debug.WriteLine(nameof(GetPasswordHashAsync));
        return Task.FromResult(user.Password);
    }

    /// <summary>
    /// ユーザーのロールを取得する
    /// </summary>
    public Task<IList<string>> GetRolesAsync(ApplicationUser user)
    {
        Debug.WriteLine(nameof(GetRolesAsync));
        IList<string> roleNames = UserRoleMap.Where(x => x.Item1 == user.Id).Select(x => x.Item2)
            .Select(x => Roles.First(y => y.Id == x))
            .Select(x => x.Name)
            .ToList();
        return Task.FromResult(roleNames);
    }


    /// <summary>
    /// パスワードを持ってるか
    /// </summary>
    public Task<bool> HasPasswordAsync(ApplicationUser user)
    {
        Debug.WriteLine(nameof(HasPasswordAsync));
        return Task.FromResult(user.Password != null);
    }

    /// <summary>
    /// ユーザーがロールに所属するか
    /// </summary>
    public async Task<bool> IsInRoleAsync(ApplicationUser user, string roleName)
    {
        Debug.WriteLine(nameof(IsInRoleAsync));
        var roles = await this.GetRolesAsync(user);
        return roles.FirstOrDefault(x => x.ToUpper() == roleName.ToUpper()) != null;
    }

    /// <summary>
    /// ユーザーをロールから削除する
    /// </summary>
    public Task RemoveFromRoleAsync(ApplicationUser user, string roleName)
    {
        Debug.WriteLine(nameof(RemoveFromRoleAsync));
        var role = Roles.FirstOrDefault(x => x.Name == roleName);
        if (role == null) { return Task.FromResult(default(object)); }
        var userRoleMap = UserRoleMap.FirstOrDefault(x => x.Item1 == user.Id && x.Item2 == role.Id);
        if (userRoleMap != null)
        {
            UserRoleMap.Remove(userRoleMap);
        }
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ユーザーにハッシュ化されたパスワードを設定する
    /// </summary>
    public Task SetPasswordHashAsync(ApplicationUser user, string passwordHash)
    {
        Debug.WriteLine(nameof(SetPasswordHashAsync));
        user.Password = passwordHash;
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ユーザー情報を更新する
    /// </summary>
    public Task UpdateAsync(ApplicationUser user)
    {
        Debug.WriteLine(nameof(UpdateAsync));
        var r = Users.FirstOrDefault(x => x.Id == user.Id);
        if (r == null) { return Task.FromResult(default(object)); }
        r.UserName = user.UserName;
        r.Password = user.Password;
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ロールを作成します。
    /// </summary>
    public Task CreateAsync(ApplicationRole role)
    {
        Debug.WriteLine(nameof(CreateAsync) + " role");
        Roles.Add(role);
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ロールを更新します
    /// </summary>
    public Task UpdateAsync(ApplicationRole role)
    {
        Debug.WriteLine(nameof(UpdateAsync) + " role");
        var r = Roles.FirstOrDefault(x => x.Id == role.Id);
        if (r == null) { return Task.FromResult(default(object)); }
        r.Name = role.Name;
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ロールを削除します
    /// </summary>
    public Task DeleteAsync(ApplicationRole role)
    {
        Debug.WriteLine(nameof(DeleteAsync) + " role");
        var r = Roles.FirstOrDefault(x => x.Id == role.Id);
        if (r == null) { return Task.FromResult(default(object)); }
        Roles.Remove(r);
        return Task.FromResult(default(object));
    }

    /// <summary>
    /// ロールをIDで取得します。
    /// </summary>
    Task<ApplicationRole> IRoleStore<ApplicationRole, string>.FindByIdAsync(string roleId)
    {
        Debug.WriteLine(nameof(FindByIdAsync) + " role");
        var r = Roles.FirstOrDefault(x => x.Id == roleId);
        return Task.FromResult(r);
    }

    /// <summary>
    /// ロールを名前で取得します。
    /// </summary>
    Task<ApplicationRole> IRoleStore<ApplicationRole, string>.FindByNameAsync(string roleName)
    {
        Debug.WriteLine(nameof(FindByNameAsync) + " role");
        var r = Roles.FirstOrDefault(x => x.Name == roleName);
        return Task.FromResult(r);
    }
}

Owinでリクエスト毎に1インスタンス作成するように設定する

StartupにCreatePerOwinContextメソッドでリクエスト毎にインスタンスを作成するように設定します。

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using SampleApp.Models;

[assembly: OwinStartup(typeof(SampleApp.Startup))]

namespace SampleApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Home/Login")
            });

            app.CreatePerOwinContext<UserStore>(() => new UserStore());
            app.CreatePerOwinContext<ApplicationUserManager>((options, context) => new ApplicationUserManager(context.Get<UserStore>()));
            app.CreatePerOwinContext<ApplicationRoleManager>((options, context) => new ApplicationRoleManager(context.Get<UserStore>()));
            app.CreatePerOwinContext<ApplicationSignInManager>((options, context) => 
                new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication));
        }
    }
}

ここらへんMVCのテンプレートなどでは、各クラスのCreateという静的メソッドを作成して、そいつへの参照を渡す形になっています。ここでは処理としてたいしたことないのでラムダ式の中に書いてしまっています。

コントローラなどの実装

あとは動作確認です。コントローラを作成します。

初期データの作成

今回はインメモリにデータを持ってるのでアプリの起動時にダミーデータを作成します。

using SampleApp.Models;
using System.Web.Mvc;
using System.Web.Routing;

namespace SampleApp
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected async void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            // ユーザーとロールの初期化
            // ロールの作成
            var roleManager = new ApplicationRoleManager(new UserStore());
            await roleManager.CreateAsync(new ApplicationRole { Name = "admin" });
            await roleManager.CreateAsync(new ApplicationRole { Name = "users" });

            var userManager = new ApplicationUserManager(new UserStore());
            // 一般ユーザーの作成
            await userManager.CreateAsync(new ApplicationUser { UserName = "tanaka" }, "p@ssw0rd");
            await userManager.AddToRoleAsync(
                (await userManager.FindByNameAsync("tanaka")).Id,
                "users");
            // 管理者の作成
            await userManager.CreateAsync(new ApplicationUser { UserName = "super_tanaka" }, "p@ssw0rd");
            await userManager.AddToRoleAsync(
                (await userManager.FindByNameAsync("super_tanaka")).Id,
                "users");
            await userManager.AddToRoleAsync(
                (await userManager.FindByNameAsync("super_tanaka")).Id,
                "admin");
        }
    }
}

tanakaが一般ユーザーでsuper-tanakaが管理者ユーザーになります。

コントローラの作成

適当にコントローラをでっちあげます。UserManagerを使いユーザー名とパスワードからユーザーを探して見つかったらSignInManagerでサインインをしています。

using Microsoft.AspNet.Identity.Owin;
using SampleApp.Models;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

namespace SampleApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }

        [AllowAnonymous]
        public ActionResult Login()
        {
            return View();
        }

        [AllowAnonymous]
        [HttpPost]
        public async Task<ActionResult> Login(ApplicationUser parameter, string returnUrl)
        {
            var userManager = this.HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
            var user = await userManager.FindAsync(parameter.UserName, parameter.Password);
            if (user == null)
            {
                return View(parameter);
            }

            var signInManager = this.HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
            await signInManager.SignInAsync(user, false, false);

            return Redirect(returnUrl);
        }
    }
}

SignInAsyncの引数は、ブラウザ閉じても覚えておくかどうかとかです。

View

Viewは適当です。Login.cshtmlは以下のようにしてます。

@model SampleApp.Models.ApplicationUser
@{
    ViewBag.Title = "Login";
}

<h2>Login</h2>

@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.UserName)<br/>
    @Html.PasswordFor(x => x.Password)<br/>
    <input type="submit" />
}

重要なログイン後の画面は、IsInRoleとかを使って管理者のときだけメッセージを出したりしています。

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

@if (User.IsInRole("admin"))
{
    <p>ようこそ管理者様</p>
}

<p>@User.Identity.Name</p>

実行して動作確認

では、実行してみます。ログイン画面が表示されます。

f:id:okazuki:20150820214023p:plain

普通のユーザーでログインすると、以下のように表示されます。

f:id:okazuki:20150820214113p:plain

adminのロールに所属するsuper_tanakaでログインすると管理者向けメッセージが表示されることが確認できます。

f:id:okazuki:20150820214207p:plain

ソースコード

ソースコードの全体は以下のGitHubリポジトリからダウンロードできます。

github.com