ASP.NET
identity

ASP.NET identityの実装 ログイン機能

ASP.NET Identiyのログイン機能実装

データベースから生成したADO.NET Entity Data Model を使用するにはどうしたら良いんだろうと思ったので実装してみた。
したいことは出来たのでメモ。

  • ASP.NET Identiyを使用した認証。
  • 認証用のユーザテーブルはすでにあるものとする。
  • ビューの生成にレイアウトを使用しない。
  • 実行しながら確認したいので画面側から作成。
  • プロジェクト名は"IdenditySample"。

環境

  • ASP.NET MVC 4.5
  • ASP.NET Identity 2.2.1
  • EntityFramework 6.2

作成する認証のクラス

  • AppUser : IUser ユーザー情報の定義クラス。
  • AppUserStore : IUserStore,IUserPasswordStore ユーザー、パスワードのCRUD処理を行う。
  • AppUserManager : UserManager 自動的に AppUserStore に変更を保存する api に関連するユーザーを公開する。
  • AppSignInManager : SignInManager ログイン情報の管理を行う。
  • AppPasswordHasher : IPasswordHasher FindAsyncでパスワード比較を行うPasswordHasherの入れ替えをする。https://www.cat-ears.net/?p=31697

ログイン後の画面を作成

まずはログインされたら表示される画面を用意。
RouteConfigの設定に従って/Home/Indexを作成する。

Controllers/HomeController.csを作成する。
自動で作成されたクラスIndexAuthorize属性を追加する。
この属性によりログイン認証が行われていない場合、ログイン画面に飛ぶ。

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

ビュー側を作成する。
こちらは"Hello world."とログインユーザー名(@User.Identity.Name)を記載した。

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div> 
        <div>Hello world.</div>
        <div>@User.Identity.Name</div>
    </div>
</body>
</html>

現段階で実行すると認証されていないのでhttp 401になる。

参照の追加

ASP.NET Identityを使用するため
NuGetで以下のパッケージを追加する。

  • Microsoft.AspNet.Identity.Owin
  • Microsoft.Owin.Host.SystemWeb
    IISでOWINを動かすのに必要。

.jaが付いてる日本語リソースでもOK。

ビルドすると以下のエラーになる。

OwinStartupAttribute を含むアセンブリが見つかりませんでした。
Startup クラスまたは [AssemblyName].Startup クラスを含むアセンブリが見つかりませんでした。

初期化コードの追加

エラー箇所の/Startup.csを作成する。

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            //クッキーベースの認証
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            //ログイン時のパスを設定
            LoginPath = new PathString("/Account/Login")
        });
    }
}

これで実行するとStartupに起因するエラーは解消されるが、/Account/Loginがないのでhttp 404になる。

ログイン画面の作成。

Startupで設定した/Account/Loginを作成する。
Controllers/AccountController.csを作成してユーザーとパスワードのフォームを作成する。

[AllowAnonymous]
public class AccountController : Controller
{
    // GET: Account
    public ActionResult Login()
    {
        return View(new LoginViewModel());
    }

    // POST
    [HttpPost]
    public ActionResult Login(LoginViewModel m)
    {
        // ログインしたらルートへ戻る。
        return Redirect("/");
    }
}

/// <summary>
/// ViewModel
/// </summary>
public class LoginViewModel
{
    [Display(Name = "ユーザー名")]
    public string UserName { get; set; }

    [Display(Name = "パスワード")]
    public string Password { get; set; }
}

ViewModelはきちんとディレクトリ分けしたほうが良いが、めんどくさいので一緒に記載。
また、きちんとルーティングするならReturnURLを受け付けてRedirect()に与える方がいい。
次に画面側。

@model IdenditySample.Controllers.LoginViewModel

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Login</title>
</head>
<body>
    <div> 
        @using (Html.BeginForm())
        {
            <div>
                @Html.LabelFor(h => h.UserName)
                @Html.TextBoxFor(h => h.UserName)
            </div>
            <div>
                @Html.LabelFor(h => h.Password)
                @Html.PasswordFor(h => h.Password)
            </div>
            <input type="submit" value="Login" />
        }
    </div>
</body>
</html>

これで実行してもエラーにならなくなる。
ログイン画面が表示されてボタンを押すと、認証されていないのでやっぱりログイン画面に戻る。
次に認証処理を実装する。

ユーザー情報の定義作成

/Models/AuthModels.csを作成

public class AppUser : IUser<string>
{
    public string Id { get; set; }
    public string UserName { get; set; }
    // Identityはハッシュ化したパスワードを使用するらしい
    public string HashPassword { get; set; }
}

ユーザ情報のCRUD処理を実装

ここではユーザー情報を返す処理だけ実装する。
SignInManager,UserManagerで管理させるため、IUserStore,IUserPasswordStoreのインターフェースを実装する。

以下を/Models/AuthModels.csに追加。

public class AppUserStore : IUserStore<AppUser>, IUserStore<AppUser, string>, IUserPasswordStore<AppUser, string>
{
・・・省略
    public void Dispose() { }

    public Task<AppUser> FindByNameAsync(string userName)
    {
        //ここでログインユーザー情報を見つけて返す処理。
        if ("user" == userName)
        {
            var user = new AppUser
            {
                Id = "1",
                UserName = "user",
                HashPassword = "user"
            };
            return Task.FromResult(user);
        }
        AppUser result = null;
        return Task.FromResult(result);
    }

    public Task<string> GetPasswordHashAsync(AppUser user)
    {
        // 単純にパスワードを返す。
        return Task.FromResult(user.HashPassword);
    }
・・・省略
}

FindByNameAsyncは入力されたユーザ名が"user"であるときユーザー情報を返すように実装。
実際には、DB等から検索してユーザー情報を返すなどの実装をする。
実装例->ASP.NET Identityの実装 DBからユーザ情報を取得する
作成、削除、変更などの機能は今回使用しないのでスルーする状態になっている。

AppUserStoreを管理するクラスの実装

UserManager,SignInManagerを実装する。
AppUserStoreと同じ/Models/AuthModels.csに追加。

public class AppUserManager : UserManager<AppUser>
{
    public AppUserManager(IUserStore<AppUser> store) : base(store)
    {
    }

    public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
    {
        var manager = new AppUserManager(new AppUserStore());
        return manager;
    }
}

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

    public static AppSignInManager Create(IdentityFactoryOptions<AppSignInManager> options, IOwinContext context)
    {
        return new AppSignInManager(context.GetUserManager<AppUserManager>(), context.Authentication);
    }
}

ユーザー管理クラスのインスタンス登録

作成したAppSignInManager,AppUserManager,を連携させるため登録をする。
以下をStartup.csに追加する。

public void Configuration(IAppBuilder app)
{
    app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
    app.CreatePerOwinContext<AppSignInManager>(AppSignInManager.Create);

    app.UseCookieAuthentication(new CookieAuthenticationOptions
・・・省略
}

これでASP.NET Identityを使用するためのロジックを実装出来た。

Controllerにログイン処理を実装

作成した認証処理を使用するための実装を行う。
/Controller/AccountController.csのHTTP POST処理を変更。

// POST
[HttpPost]
public async Task<ActionResult> Login(LoginViewModel m)
{
    if (m.UserName == null || m.Password == null) return View();
    // UserManagerを使用してログイン可能か判断
    var manager = this.HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
    var user = await manager.FindAsync(m.UserName, m.Password);
    if (user == null) return View(m);

    // ユーザーの識別情報を作成し、ログインする
    var identity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
    var authenticationManager = HttpContext.GetOwinContext().Authentication;
    authenticationManager.SignIn(identity);

    // ログインしたらルートへ戻る。
    return Redirect("/");
}

確認のため実行するとFindByNameAsyncで設定した"user"を入力してもvar user = await manager.FindAsync(m.UserName, m.Password);でnullになることがわかる。

これはFindAsync内でパスワードの比較をHashに変換して比較しているからで、IPasswordHasherを使用してパスワード比較の方法を変更する。

パスワード比較方法の変更

IPasswordHasherのインターフェイスを実装する。

public class AppPasswordHasher : IPasswordHasher
{
    public string HashPassword(string password)
    {
        // ハッシュ化しないでそのまま返す
        return password;
    }

    public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
    {
        // 単純に文字列を比較
        if (hashedPassword == HashPassword(providedPassword))
        {
            return PasswordVerificationResult.Success;
        }
        else
        {
            return PasswordVerificationResult.Failed;
        }
    }
}

実装したAppPasswordHasherAppUserManagerに登録する。
追加したAppUserManagerCreateに以下を追加する。

public class AppUserManager : UserManager<AppUser>
{
    public AppUserManager(IUserStore<AppUser> store) : base(store)
    {
    }

    public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
    {
        var manager = new AppUserManager(new AppUserStore());
        // デフォルトのパスワード管理はパスワードをハッシュ化して扱う。
        // 平文のパスワードを扱うためにPasswordHasherを変更。
        manager.PasswordHasher = new AppPasswordHasher();
        return manager;
    }
}

確認

/Account/Login

ログイン -> 失敗 -> /Account/Login

成功

/Home/Index

ログイン画面からログイン後Home/Indexに移動することが確認できる。
また、Home/Indexに直接URL指定してもログインしていなければログイン画面に移動する。

まとめ

AppUserManagerのにIPasswordHasherを実装しなおしているが、
FindAsyncをオーバーライドしてパスワードを比較するようにすれば良いのかも。

UserManager.FindAsync to use custom Userstore's FindByIdAsync method instead of FindByNameAsync

あと、AccountControllerのPOSTメソッド内のログイン可能確認とサインイン処理はモデル側の実装かなと思う。

参考