👌

ASP.NET MVC 認証

2022/10/26に公開約30,500字

はじめに

ASP.NET2.0以降に導入されたメンバーシップライブラリを用いて実装する

認証機能を実装する(固定文字列)

Here we go!!!!

Providerクラスの実装

まずは、Modelsに認証機能の大元となるCustomMembershipProvider.csを作成する。
次に、MembershipProviderを継承し、ValidateUserメソッドに認証機能に必要な実装を加えていく。ValidateUserメソッドはユーザからの入力内容の回答を用意するメソッドです。まずは、動作確認のために固定のユーザ名とパスワードで認証機能の実装を行う。usernameが"administrator"or"userかつpasswordが"password"であれば認証OK、それ以外は認証NGとなる。

Models/CustomMembershipProvider.cs
public override bool ValidateUser(string username, string password)
{
    if ("administrator".Equals(username) && "password".Equals(password))
    {
	return true;
    }
    if ("user".Equals(username) && "password".Equals(password))
    {
	return true;
    }
    return false;
}

次に、認証OKかNGかを判断するCustomRoleProvider.csを作成する
次に、RoleProviderを継承し、GetRolesForUserIsUserInRoleに認証機能に必要な実装を加えていく。

GetRolesForUserメソッドは指定されたユーザが所属するロールを配列で返すメソッドです。
引数で指定したusernameが"administrator"であれば、ロール名を文字列の配列"Administrator"として返す。それ以外はロール名を文字列の配列"Users"として返す

Models/CustomRoleProvider.cs
public override string[] GetRolesForUser(string username)
{
    if ("administrator".Equals(username))
    {
	return new string[] { "Administrator" };
    }
    return new string[] { "Users" };
}

IsUserInRoleメソッドは第1引数のusernameが第2引数のroleNameに所属するかを判定するメソッドです。

Models/CustomRoleProvider.cs
public override bool IsUserInRole(string username, string roleName)
{
    if("administrator".Equals(username) && "Administrators".Equals(roleName))
    {
	return true;
    }
    if("user".Equals(username) && "Users".Equals(roleName))
    {
	return true;
    }
    return false;
}

ログインコントローラの実装準備

入力フォームを格納するPOCOを作成します。
ModelsにLoginViewModelを作成します。
今回はログイン画面でユーザ名とパスワードを入力しますが、実際のDBには対応するテーブルは用意されていません。
そのため、以下の内容で実装が必要になります。
1.ログイン画面で入力したユーザ名とパスワードをLoginViewModelで保持する
2.これ以降に作成するLoginControllerに反映させるようにする

Models/LoginViewModel.cs
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace crud.Models
{
    public class LoginViewModel
    {
        [Required]
        [DisplayName("ユーザ名")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [DisplayName("パスワード")]
        public string Password { get; set; }

    }
}

ログインコントローラの実装

認証機能を提供するコントローラを作成します。

ユーザ認証を行う必要があるので、MembershipProviderを保持する
ASP.NETのFormsAuthenticationクラスのSetAuthCookieメソッドを用いてCookie認証を実装しログイン状態を保持させる。

Models/LoginViewModel.cs
using crud.Models;
using System.Web.Mvc;
using System.Web.Security;

namespace crud.Controllers
{
    //LoginControllerには認証なしの状態でのアクセスを可能にする
    [AllowAnonymous]
    public class LoginController : Controller
    {

        //ユーザ認証をimport
        //再度importする必要はないのでreadonlyを先頭に加える
        readonly CustomMembershipProvider membershipProvider = new CustomMembershipProvider();


        // GET: Login
        public ActionResult Index()
        {
            return View();
        }

        // POST: Login
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Index([Bind(Include = "UserName, Password")] LoginViewModel model) // POSTされたときはLoginViewModelを引数として持つ
        {
            //LoginViewModelに格納されたクライアントからの入力内容:ユーザ名とパスワードのバリデーションチェック
            if (ModelState.IsValid)
            {
                if (this.membershipProvider.ValidateUser(model.UserName, model.Password))
                {
                    //認証Cookieを設定することで、認証状態を保持させる
                    FormsAuthentication.SetAuthCookie(model.UserName, false);
                    //認証処理が終了すれば、TodoesコントローラのIndex.cshtmlに処理をリダイレクトするように指定
                    return RedirectToAction("Index", "Todoes");
                }
            }

            ViewBag.Message = "ログインに失敗しました";
            return View(model);

        }

    }
}

ログアウト機能の実装

Models/LoginViewModel.cs
// GET: Logout
public ActionResult SignOut()
{
    FormsAuthentication.SignOut();
    return RedirectToAction("Index");
}

ログイン画面の実装

スキャフォールディングでViews/Login/Index.cshtmlを作成する。
Indexを右クリックします。

LoginControllerで設定した、認証NGの場合のメッセージをViewに出力する

Views/Login/Index.cshtml
@model crud.Models.LoginViewModel

@{
    ViewBag.Title = "SignIn";
    Layout = "~/Views/_LayoutPage1.cshtml";
}

<h2>SignIn</h2>

<!--認証NG時のViewBag.Message-->
@if (!string.IsNullOrEmpty(ViewBag.Message))
{
    <p class="text-danger">@ViewBag.Message</p>
}

ActionLinkメソッドでログアウトリンクを生成する。
第1引く数にボタン名、第2引数にLoginControllerで生成したSignOutメソッドに対する処理を指定、第3引数にControllerを指定する。

_LayoutPage1.cshtml
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <!--ログイン画面にログアウトボタンを表示させる-->
    <div class="container">
        @if (Request.IsAuthenticated)
        {
            @Html.ActionLink("ログアウト", "SignOut", new { Controller = "Login" })
        }
    </div>
    <div class="container">
        @RenderBody()
    </div>
</body>
</html>

TodoesControllerの各ActionMethodには認証OKの状態でアクセス可能になる。

TodoesController.cs
namespace crud.Controllers
{
    //認証された状態でアクセス可能にする
    [Authorize]
    public class TodoesController : Controller
    {
        private TodoesContext db = new TodoesContext();

        // GET: Todoes
        public ActionResult Index()
        {
            return View(db.Todoes.ToList());
        }
	...

Web.configの設定

Login画面が一番最初に表示されるような設定を行う。

  <system.web>
    <compilation debug="true" targetFramework="4.7.2" />
    <httpRuntime targetFramework="4.7.2" />
	<authentication mode="Forms">
		<forms loginUrl ="/Login/Index"></forms>
	</authentication>
	<membership defaultProvider="CustomMembershipProvider">
		<providers>
			<clear/>
			<add name="CustomMembershipProvider" type="TodoApp.Models.CustomMembershipProvider"/>
		</providers>
	</membership>
	<roleManager enabled="true" defaultProvider="CustomRoleProvider">
		<providers>
			<clear/>
			<add name="CustomRoleProvider" type="TodoApp.Models.CustomRoleProvider"/>
		</providers>
	</roleManager>
  </system.web>

デバッグ実行

リダイレクト認証が走ることを確認します。

ユーザ名とパスワードを入力してログインします。

ログアウトして認証NGとなるユーザ名を入力します

きちんと認証処理が働くことを確認できました。

認証機能を実装する(EF対応)

ユーザとロールをDBで管理する。
Here we go!!!!

Modelの設定

まずは、POCOを作ります。
Modelsにユーザクラスとロールクラスを作成します。

ログイン画面で入力した内容と比較できるように重複しないユーザ名プロパティを用意する。
そして、文字数の長さを指定しないとテーブルエラーを吐かれるので設定。

UsermodelとRolemodelの関連を表すプロパティ;ナビゲーションプロパティにはviretualという修飾子を持つ必要があり、これによって一つのユーザが複数のロールを持つことが可能になる。

Models/User.cs
sing System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Web;

namespace crud.Models
{
    public class User
    {
        
        public int Id { get; set; } //主キー

        [Required]
        [Index(IsUnique = true)] //ユーザ名の重複を防ぐ
        [StringLength(256)] //900byte以下
        [DisplayName("ユーザ名")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [DisplayName("パスワード")]
        public string Password { get; set; }
	
	public virtual ICollection<Role> Roles { get; set; } //1つのユーザが複数のロールに所属できるようにする

    }
}

で、、、

Models/Role.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace crud.Models
{
    public class Role
    {
        public int Id { get; set; }
        public string RoleName { get; set; }

        public virtual ICollection<User> Users { get; set; } //1つのロールが複数のユーザに所属できるようにしたい
    }
}

ユーザとロールはお互いに1つに対して複数所属できる状態(N:M)であり、
このような場合、EFによってUserRoleテーブルが作成される。

TodoesContextの管理対象にUserとRoleを加える。
DbContextは各POCO(クラス)とのDB連携を行うためのクラスです。

Models/TodoesContext.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace crud.Models
{
    public class TodoesContext : DbContext
    {
       public DbSet<Todo> Todoes { get; set; }
 +     public DbSet<Role> Users { get; set; }
 +     public DbSet<Role> Roles { get; set; }
    }
}

Providerクラスのリファクタリング

以前までは、固定文字列で認可の是非を判断していましたが、
今後は、ログインフォームで入力した内容をEntity Frameworkを通じてデータベースに問い合わせた結果を返すようにリファクタリングしていきます。

[変更前のコード]

Models/CustomMembershipProvider.cs
public override bool ValidateUser(string username, string password)
{
    if ("administrator".Equals(username) && "password".Equals(password))
    {
	return true;
    }
    if ("user".Equals(username) && "password".Equals(password))
    {
	return true;
    }
    return false;
}

まずは、データベースにアクセスするためにDbContextを継承したTodoesContext()のインスタンスを生成します。using構文を使うと、そのブロック内で変数が有効になり、ブロックが終了するタイミングでdisposeメソッドが呼ばれるので終了処理が不要になる。

FirestorDefaultメソッドはWhereなどで取得したデータが複数存在している場合に、その中で一番先頭の値を返すまたはリストが空ならnullを返すメソッドです。

[変更後のコード]

Models/CustomMembershipProvider.cs
public override bool ValidateUser(string username, string password)
{
    using (var db = new TodoesContext())
    {
	var user = db.Users
	.Where(u => u.UserName == username && u.Password == password)
	.FirstOrDefault();
	if (user != null)
	{
	    return true;
	}
    }
    return false;
}

1つ目の修正
[変更前のコード]

Models/CustomRoleProvider.cs
public override string[] GetRolesForUser(string username)
{
    if ("administrator".Equals(username))
    {
	return new string[] { "Administrators" };
    }
    return new string[] { "Users" };
}

[変更後のコード]

Models/CustomRoleProvider.cs
public override string[] GetRolesForUser(string username) //引数で指定されたユーザが所属するロールを配列で返す
{
    using (var db = new TodoesContext())
    {
	var user = db.Users
	    .Where(u => u.UserName == username)
	    .FirstOrDefault();
	if (user != null)
	{
	    return user.Roles.Select(role => role.RoleName).ToArray(); ////ユーザが所属するロールの名前(roleNameの配列)を返す
	}
    }
	return new string[] {  }; //ユーザがヒットしなければ空の文字列を返す
}

2つ目の修正
[変更前のコード]

Models/CustomRoleProvider.cs
public override bool IsUserInRole(string username, string roleName)
{
    if("administrator".Equals(username) && "Administrators".Equals(roleName))
    {
	return true;
    }
    if("user".Equals(username) && "Users".Equals(roleName))
    {
	return true;
    }
    return false;
}

[変更後のコード]

Models/CustomRoleProvider.cs
public override bool IsUserInRole(string username, string roleName)
{
    string[] roles = this.GetRolesForUser(username); //GetRolesForUserを使う↑↑
    return roles.Contains(roleName);  //roles配列内に引数のroleNameが存在するか検証
}

ここまでで、ユーザとロールの情報をデータベースから取得し認証認可を実装できた。
まだデータベースにはUserやRoleの情報が登録されていない。

ここで、EFを用いてデータベースに初期データを放り込む作業を行う。

マイグレーションとは、モデルの変更に合わせてデータベースを作成・変更するための仕組みで、
モデル変更が行われれば自動的にデータベース操作を行ってくれます。神!!!!

まずは、マイグレーション機能をONNにします。
NuGetパッケージの管理から以下のコマンドを叩きデデータベースのチェックの後マイグレーション機能がONNになります、、、!

PM> Enable-Migrations -EnableAutomaticMigrations
Checking if the context targets an existing database...
Code First Migrations enabled for project crud.

ここで、Migrationsフォルダが作成されていることを確認し、以下の追加設定を行う

Todo, User, Roleなどのデータが削除されるような変更でも自動マイグレーション機能をONN
にするかどうかを設定する
AutomaticMigrationDataLossAllowed = true;

SeedメソッドはMigrationが終えた後ただちに実行されます。
ここにユーザとロールの初期データを登録します。

AddOrUpdateメソッド:追加する要素がなければ追加し、すでにDBに存在するならば更新します。
その時はIDをもとに追加や更新を行います。

Migrations/Configulation.cs
namespace crud.Migrations
{
    using crud.Models;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<crud.Models.TodoesContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true; //モデルのデータが削除されるような変更を許可するかを設定
            ContextKey = "crud.Models.TodoesContext";
        }

protected override void Seed(crud.Models.TodoesContext context)
        {
            User admin = new User()
            {
                Id = 1,
                UserName = "admin",
                Password = "password",
                Roles = new List<Role>()
            };


            //代表ロール
            Role administrators = new Role()
            {
                Id = 1,
                RoleName = "Administrators",
                Users = new List<User>()
            };

            //一般ロール
            Role users = new Role()
            {
                Id = 2,
                RoleName = "Users",
                Users = new List<User>()
            };

            admin.Roles.Add(administrators);
            administrators.Users.Add(admin);
            
            //引数のcontextにUSerとRoleを反映させる
            context.Users.AddOrUpdate(user => user.Id, new User[] { admin }); //複数ユーザが存在するので配列型のUser[]
            context.Roles.AddOrUpdate(role => role.Id, new Role[] { administrators, users });
        }
    }
}

crudアプリを起動した際にConfigulationクラスが実行されるようにGlobal.asaxに登録する。
Application_Start()がアプロ起動時に最初に呼ばれるメソッドです。

ここにEntity Frameworkの初期化処理(Configulationクラスの実行処理)をついかする。
これで、Seedメソッドがアプリケーション実行時に適切に行われる

crud/Global.asax.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using crud.Migrations;
using crud.Models;

namespace crud
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
+           Database.SetInitializer(new MigrateDatabaseToLatestVersion<TodoesContext, Configuration>());
        }
    }
}


サーバーエクスプローラーでテーブルの中身を確認する。




ユーザ管理画面

ここまででユーザとロールをデータベース管理することができた。
次に、ユーザ管理画面を追加し、CRUD機能を実装する。


ユーザ管理画面にはAdministratorの権限を持つユーザのみがアクセスできるようにする。

Controllers/UsersController.cs
[Authorize(Roles = "Administrators")]

各画面に遷移できるように、共通レイアウトにリンクを生成していく。
ActionLinkメソッドでリンクを生成し、引数にコントローラのActionMethodを渡すことで、
アクセス先を指定することができる。神

Views/_LayoutPage1.cshtml
<!--ログイン画面にログアウトボタンを表示させる-->
<div class="container">
@if (Request.IsAuthenticated)
{
    @Html.ActionLink("ログアウト", "SignOut", new { Controller = "Login" })

    //今ログインしているユーザ(Userオブジェクト)が引数に指定したロールに所属しているかをチェック
    if (User.IsInRole("Administrators"))
    {
	<span>|</span>
	//ユーザ管理というリンク先にUsersController.csのIndexメソッドを渡す
	@Html.ActionLink("ユーザ管理", "Index", new { Controller = "Users" })
    }
}
</div>

システム管理者であるadminでログインするとユーザ管理画面へのアクセス先が表示されます。

一般のユーザアカウントuserを作成します。

一度ログアウトし、userとしてログインしなおすとユーザ管理画面へのアクセスリンクはなく、実際にURLを叩いてもアクセスできないことが確認できました。

しかし、ロール(システム管理者なのか一般ユーザなのか)に関する表記がないためわかりにくいです。次に、ロールに関する設定を行います。

リストボックスで項目を追加する際SelectListItemが必要です。

Controllers/UsersController.cs
private void SetRoles()
{
    //DBからロールの情報を取り出す
    var list = db.Roles.Select(item => new SelectListItem()
    {
	//SelectListItemのTextとValueに値を設定する
	Text = item.RoleName,
	Value = item.Id.ToString(), //Valueは文字列型
    }).ToList();

    //Create,EditのViewにデータを渡す
    ViewBag.RoleIds = list;
}

次に、SetRolesの呼び出し元を設定していきます。
Editの場合、ユーザが一つ選ばれて、そのユーザが保持する値が反映された編集画面へと遷移します。
そのため、現在所属しているロールはすでに選択された状態である必要があります。
そのため、ここでSetRolesメソッドの引数にて、ロールのコレクションを取得できるようにします。

Controllers/UsersController.cs
// GET: Users/Create
public ActionResult Create()
{
    this.SetRoles(new List<Role>());
    return View();
}

// GET: Users/Edit/5
public ActionResult Edit(int? id)
{
    if (id == null)
    {
	return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    User user = db.Users.Find(id);
    if (user == null)
    {
	return HttpNotFound();
    }
+    this.SetRoles(user.Roles);
    return View(user);
}


//現在ユーザが所属しているロールのコレクションをセットする
+ private void SetRoles(ICollection<Role> userRoles)
{
    //roleコレクションよりもroleのIdのint配列のほうが管理しやすい
+     var roles = userRoles.Select(item => item.Id).ToArray();

    //DBからロールの情報を取り出す
    var list = db.Roles.Select(item => new SelectListItem()
    {
	//SelectListItemのTextとValueに値を設定する
	Text = item.RoleName,
	Value = item.Id.ToString(), //Valueは文字列型
+ 	Selected = roles.Contains(item.Id) //rolesのidに含まれるSelectListItemは初期状態で選択された状態にする
    }).ToList();

    //Create,EditのViewにデータを渡す
    ViewBag.RoleIds = list;
}

Createメソッドのように空のリストを返した場合はrolesが空になるので、SelectListItemが選択されていない状態でセットされます。

コントローラの修正はここまで、続いてユーザのモデルクラスを編集します。
今回Edit, Create画面にロールを選択する画面を用意します。
そのリストボックスで選択された状態を保持できるように設定を加える必要があります。
しかしDBに反映させる必要はありません。

モデルの修正はOK
次はビューです。

ListBoxメソッドの第1引数にViewbagで指定したキー名を与えると、自動的にリストボックス内でViewbagで設定したリストが項目として登録される。

Views/Views/Create, Edit.cshtml
<div class="form-group">
@Html.LabelFor(model => model.RoleIds, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
    @Html.ListBox("RoleIds", null, new { @class = "form-control" }) <!--ViewBagで設定した値-->
    @Html.ValidationMessageFor(model => model.RoleIds, "", new { @class = "text-danger" })
</div>
</div>

次に、CreateボタンのPOST時のメソッドを修正

Controllers/UsersController.cs
// GET: Users/Create
public ActionResult Create()
{
    this.SetRoles(new List<Role>()); //空のリストを返す
    return View();
}

// POST: Users/Create
// 過多ポスティング攻撃を防止するには、バインド先とする特定のプロパティを有効にしてください。
// 詳細については、https://go.microsoft.com/fwlink/?LinkId=317598 をご覧ください。
[HttpPost]
[ValidateAntiForgeryToken]
+ public ActionResult Create([Bind(Include = "Id,UserName,Password,RoleIds")] User user)
{
    //DBに格納されているRolesに、入力されたRoleIdsが含まれていればそれを取得してリストとして返す
    var roles = db.Roles.Where(role => user.RoleIds.Contains(role.Id)).ToList();

    if (ModelState.IsValid)
    {
	user.Roles = roles; //選択されたrolesをuser.Rolesにセットする

	db.Users.Add(user);
	db.SaveChanges();
	return RedirectToAction("Index");
    }

    + this.SetRoles(user.Roles); //入力内容が失われないようにセット

    return View(user);
}

EditボタンのPOST時のメソッドを修正

Controllers/UsersController.cs
public ActionResult Edit([Bind(Include = "Id,UserName,Password,RoleIds")] User user)
{
    var roles = db.Roles.Where(role => user.RoleIds.Contains(role.Id)).ToList(); //画面で選択されたrolesをDBから取得します。

    if (ModelState.IsValid)
    {

	var dbUser = db.Users.Find(user.Id); //dbから情報取得(引数のuserのIDをもとに該当のユーザを取得)

	if (dbUser == null)
	{
	    return HttpNotFound();
	}
	dbUser.UserName = user.UserName; //データベースから取得したユーザ(dbUser)に画面入力内容を反映させる
	dbUser.Password = user.Password;
	dbUser.Roles.Clear(); //以前選択していたロールをクリアします

	foreach (var role in roles) //dbUserのrolesに画面で選択されたroleを追加
	{
	    dbUser.Roles.Add(role);
	}

	db.Entry(user).State = EntityState.Modified;
	db.SaveChanges();
	return RedirectToAction("Index");
    }
    this.SetRoles(roles); //入力内容に不備があるとModelState.IsValid==falseになるので入力内容が消し飛ばないようにする
    return View(user);
}

コントローラの実装が完了。
動作確認したところ作成されたユーザのロールが正しく選択されていないエラーが起きが語気が原因でした。

Controllers/UsersController.cs
// GET: Users/Edit/5
public ActionResult Edit(int? id)
{
    if (id == null)
    {
	return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    User user = db.Users.Find(id);
    if (user == null)
    {
	return HttpNotFound();
    }
-   this.SetRoles(new List<Role>());
+   this.SetRoles(user.Roles);
    return View(user);
}

OK

パスワードハッシュ化

PBKDF2=Password Based Key Deriveation Function 2

  • ハッシュ化
    ハッシュ関数を用いて文字列を一定の長さに変換⇒ハッシュ値
    不可逆性が特徴(SHA256などが代表例)

しかし、元の文字列が短すぎると元の文字列からハッシュ値を割り出すことが現実的な時間内で可能になります⇒レインボーテーブル攻撃。

そこで、以下の対応を行う。

  • salt
    ユーザーごとにことなる20桁以上のかさましの文字列をもとの文字列(パスワード)に付加する。

  • ストレッチング
    ハッシュ値 = ハッシュ関数(salt + password)

string hash = password;
for (int i=0; i<10000; i++)
{
  hash = hash-method(hash + salt + password)
}

ハッシュ化をコードに記載していく

Models/CustomMembershipProvider.cs
using System.Security.Cryptography;

//第1引数のユーザに第二引数のパスワードをハッシュ化した値を返す
public string GeneratePasswordHash(string username, string password)
{
    //第1引数のユーザ名は最初で1文字の設定なので先頭にsecretという文字列(ソルト)を足す
    string rawSalt = $"secret_{username}";
    //ハッシュ関数の取得
    var sha256 = new SHA256CryptoServiceProvider();
    //ComputeHashは引数をバイト配列なので、文字列をバイト配列に変換する
    var salt = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawSalt));
    //引数とストレッチング回数
    var pdkf2 = new Rfc2898DeriveBytes(password, salt, 100000);
    //インスタンス生成後にhash値を取得
    var hash = pdkf2.GetBytes(32);
    //配列を文字列に変換
    return Convert.ToBase64String(hash);

}

ValidateUserメソッド内にて、ユーザ名とパスワードを用いてGeneratePasswordHashでハッシュ値を取得します。

取得したハッシュ値とDBに格納されているハッシュ値を比較します。

Models/CustomMembershipProvider.cs
public override bool ValidateUser(string username, string password)
{
    using (var db = new TodoesContext())
    {
	string hash = this.GeneratePasswordHash(username, password);
	var user = db.Users
	.Where(u => u.UserName == username && u.Password == hash)
	.FirstOrDefault();
	if (user != null)
	{
	    return true;
	}
    }
    return false;
}

続いてコントローラの修正を行います。
先ほど修正したCustomMembershipProvider.csのインスタンスを保持するように修正します。

Controllers/UsersController.cs
readonly private CustomMembershipProvider membershipProvider = new CustomMembershipProvider();

次に、CreateのPOST時のアクションメソッドを修正します。
DBにユーザを追加する前にパスワードをハッシュ化します。

Controllers/UsersController.cs
    if (ModelState.IsValid)
    {
	user.Roles = roles; //画面で選択されたrolesをuser.Rolesにセットする

	//取得したRoleをセットする
	user.Roles = roles;
	//DB格納前にパスワードをハッシュ化
	user.Password = this.membershipProvider.GeneratePasswordHash(user.UserName, user.Password);
	db.SaveChanges();
	return RedirectToAction("Index");
    }

EditのPOST時のアクションメソッドを修正します。
[修正前]

Controllers/UsersController.cs
if (ModelState.IsValid)
    {

	var dbUser = db.Users.Find(user.Id); //dbから情報取得(引数のuserのIDをもとに該当のユーザを取得)

	if (dbUser == null)
	{
	    return HttpNotFound();
	}
	//データベースから取得したユーザ(dbUser)に画面入力内容を反映させる
	dbUser.UserName = user.UserName;
	dbUser.Password = user.Password;
	//直前までに選択していたロールをクリアします
	dbUser.Roles.Clear();

	foreach (var role in roles) //dbUserのrolesに画面で選択されたroleを追加
	{
	    dbUser.Roles.Add(role);
	}

	db.Entry(user).State = EntityState.Modified;
	db.SaveChanges();
	return RedirectToAction("Index");
    }

[修正後]
編集画面でパスワードを変更してPOSTした場合、変更せずにPOSTした場合があります。
変更されていた場合は入力されたパスワードをハッシュ化します。
つまり、入力されたパスワードとDBに格納されているパスワードが異なる場合です。
this.membershipProvider.GeneratePasswordHash(dbUser.UserName, user.Password);のように書けばユーザに対応するハッシュ化されたパスワードを生成できます。

Controllers/UsersController.cs
if (ModelState.IsValid)
    {

	var dbUser = db.Users.Find(user.Id); //dbから情報取得(引数のuserのIDをもとに該当のユーザを取得)

	if (dbUser == null)
	{
	    return HttpNotFound();
	}
	dbUser.UserName = user.UserName;
	+ if (!String.IsNullOrEmpty(user.Password) && !dbUser.Password.Equals(user.Password))
	{
	    dbUser.Password = this.membershipProvider.GeneratePasswordHash(dbUser.UserName, user.Password);
	}
	- dbUser.Password = user.Password;
	dbUser.Roles.Clear(); //以前選択していたロールをクリアします

	foreach (var role in roles) //dbUserのrolesに画面で選択されたroleを追加
	{
	    dbUser.Roles.Add(role);
	}

	db.Entry(user).State = EntityState.Modified;
	db.SaveChanges();
	return RedirectToAction("Index");
    }

続いてSeedメソッドを修正します。

Migrations/Configulation.cs
var membershipProvider = new CustomMembershipProvider();
admin.Password = membershipProvider.GeneratePasswordHash(admin.UserName, admin.Password);

デバック実行します。

パスワード編集後POSTするとエラーが出ました。

'crud.Models.User' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.'

TodoAppのコード貼り付けて解決wwwww
まあいいでしょう。

入力したパスワードの文字列が同じでも、ユーザ名と紐づけてハッシュ化を行うので、
ハッシュ値は重複しない。

下記のテストコード(ログインできなくなった際の逃げ道)を削除

Models/CustomMembershipProvider.cs
//試験用のコード
if("admin".Equals(username) && "password".Equals(password))
{
return true;
}

ユーザ固有のTodo管理を行う

Userプロパティを追加し、TodoクラスとUserクラスを紐づけます。
ナビゲーションプロパティによってユーザ対Todoは1対Nの構造になります。
この場合、EFによってTodoesテーブルにUSerIdというFKが作成されます。

Models/Todo.cs
//Todoを登録したユーザを保持する
public virtual User User { get; set; }
Models/User.cs
//一人のユーザは複数のtodoを持つのでユーザクラスはTodoのコレクションを持つ
public virtual ICollection<Todo> Todoes { get; set; }

GET時に、今の書き方だとすべてのTodoを返している

TodoesController.cs
// GET: Todoes
public ActionResult Index()
{
    return View(db.Todoes.ToList());
}

Identityオブジェクト内のNameに一致する場合に、そのUserのTodoを返す。
もしユーザが取得できなければ、return View(new List<Todo>());として、空のリストを返す。

TodoesController.cs
public ActionResult Index()
{
    //ログインしたユーザのみTodoを返す
    var user = db.Users.Where(item => item.UserName == User.Identity.Name).FirstOrDefault();
    if (user != null)
    {
	return View(user.Todoes);
    }
    return View(new List<Todo>());
}

POST時に、ログインユーザのTodoを作成するようにしたい。
userが存在するなら、todo.Userに取得したuserをセットします。

TodoesController.cs
public ActionResult Create([Bind(Include = "Id,Summary,Detail,Limit,Done")] Todo todo)
{
    if (ModelState.IsValid)
    {
	//ログインしているユーザのオブジェクトを取得
	var user = db.Users.Where(item => item.UserName == User.Identity.Name).FirstOrDefault();

	if (user != null)
	{
	    todo.User = user;

	    //DB登録
	    db.Todoes.Add(todo);
	    db.SaveChanges();
	    //RedirectToActionは指定されたアクションに処理を転送するヘルパーメソッド
	    //Indexが指定されているので、登録を完了すると処理が一覧画面に戻る
	    return RedirectToAction("Index");
	}
    }

    return View(todo);
}


サーバエクスプローラにて、TodoesテーブルでUser_Idが作成されている。
このIDごとにTodoを画面表示するよう実装した。

エラー対応

ユーザ変更画面にてパスワード変更を任意とします。
未入力の場合はパスワード変更なしとします。

ユーザ名の変更をできないように、読み取り専用にする。
@readonly = "readonly"

Views/Users/Edit.cshtml
<div class="form-group">
@Html.LabelFor(model => model.UserName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
    @Html.EditorFor(model => model.UserName, new { htmlAttributes = new { @class = "form-control", @readonly = "readonly" } })
    @Html.ValidationMessageFor(model => model.UserName, "", new { @class = "text-danger" })
</div>
</div>

ユーザ名の変更を行わないようにする。

Controllers/UsersController.cs
//dbUser.UserName = user.UserName;

編集画面用に、ViewModelを作成します。

Models/UserEditViewModel.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace crud.Models
{
    public class UserEditViewModel
    {
        public int Id { get; set; }

        [Required]
        [StringLength(256)]
        [DisplayName("ユーザー名")]
        public string UserName { get; set; }

        [DataType(DataType.Password)]
        [DisplayName("新しいパスワード")]
        public string Password { get; set; }

        [DisplayName("ロール")]
        public List<int> RoleIds { get; set; }
    }
}

GET時にDBから取得したUserをもとに、UserEditViewモデルを生成し、Viewに返す

Model/UserEditView.cs
var model = new UserEditViewModel()
{
Id = user.Id,
UserName = user.UserName
};

POST時には、UserEditviewModelを受け取るように修正し、
受け取ったViewModelのパスワードが空の場合、パスワードは変更せず、
GeneratePasswordHashの引数はDBから取得したdbUser.UserNameとします。

Controllers/UsersController.cs
public ActionResult Edit([Bind(Include = "Id,UserName,Password,RoleIds")] UserEditViewModel user)
        {
            var roles = db.Roles.Where(role => user.RoleIds.Contains(role.Id)).ToList();
            if (ModelState.IsValid)
            {

                //dbから情報取得
                var dbUser = db.Users.Find(user.Id);
                if (dbUser == null)
                {
                    return HttpNotFound();
                };
                //入力内容を反映させる(編集なので)
                //dbUser.UserName = user.UserName;

                //Db格納と入力されたpasswordが同地でないならハッシュ化されたものに置換する

                //UserEditViewモデルを受け取る

                if (!String.IsNullOrEmpty(user.Password) && !dbUser.Password.Equals(user.Password))
                {
                    dbUser.Password = this.membershipProvider.GeneratePasswordHash(dbUser.UserName, user.Password);
                }
                //いったんは以前のロールを削除する
                dbUser.Roles.Clear();
                //取得したRolesの配列を一つずつ確認
                foreach (var role in roles)
                {
                    dbUser.Roles.Add(role);
                }

                dbUser.RoleIds = user.RoleIds;

                db.Entry(dbUser).State = EntityState.Modified;
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            this.SetRoles(roles);
            return View(user);
        }

最後に、大事な設定!

Users/Edit.cshtml
- @model crud.Models.User
+ @model crud.Models.UserEditViewModel

Discussion

ログインするとコメントできます