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
を作成する。
自動で作成されたクラスIndex
にAuthorize
属性を追加する。
この属性によりログイン認証が行われていない場合、ログイン画面に飛ぶ。
[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;
}
}
}
実装したAppPasswordHasher
をAppUserManager
に登録する。
追加したAppUserManager
のCreate
に以下を追加する。
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メソッド内のログイン可能確認とサインイン処理はモデル側の実装かなと思う。