カテゴリー
Microsoft

【ASP.NET MVC5】Identity 2 をベースにした、ユーザー情報を DB に持つ独自の認証プロジェクトのチュートリアル

2016年8月18日11時12分追記: ソリューション全体をアップロードしました。

Identity をカスタマイズした独自の認証を持ったサンプルのアプリを作ってみました。Identity の拡張方法の習得を目標に、それ以外はシンプルに、あまりこだわらないようしております。

構築(勉強)の仕方

Visual Studio 2015 でプロジェクトを作成するときに認証処理を追加するように選択することができます。そうしますと、Identity を使用した認証ロジックと、ユーザーやロールを管理するデータベーステーブルが予め作られます。

このファイルやコードが大変参考になります。

そこで、ファイルの構成はプロジェクト生成時に自動作成される認証のあり方に近づけます。

コードの振る舞いや、内容の理解、実際に書くコードの内容は、参考ウェブページをお手本にいたします。

仕様

認証方法

  • ユーザ名とパスワードでログイン

データベースのテーブル

ロールなどのテーブルは作りません。ユーザー名、パスワードと他の属性情報を 1 つだけもつ、シンプルなテーブルのみを作成いたします。

Users テーブル

ページ一覧

  • /Home/Index ← ログイン必要
  • /Home/About ← ログイン必要
  • /Home/Contact ← ログイン必要
  • /Users/Login : ログイン
  • /Users/Index : ユーザ一覧
  • /Users/Detail : ユーザ詳細
  • /Users/Create : ユーザ作成
  • /Users/Edit : ユーザ編集
  • /Users/ChangePasswrod : パスワード変更 ← ログイン必要
  • /Users/Delete : ユーザ削除

ページ遷移の補足

  • ログオフ時は Home コントローラーページにアクセスできず、Users/Login ページにリダイレクトする。
  • ログインページで認証すると、Home/Index へリダイレクトする。
  • ユーザー作成と同時にログイン完了状態にする。
  • ユーザー情報を更新してもログイン状態を継続する。
  • パスワード変更は、専用のページを用意する。モデルビューも用意する(他のページは Users モデルを使い回す)。
  • パスワード変更は、認証を必要とする。どのリンクから辿ったとしても、ログイン中ユーザのパスワードを変更する。
  • ログインしているユーザーが自分自身を削除した場合は、ログオフする。

それではいよいよ、実際に作ってまいります!

1. プロジェクト作成

  • ソリューション名・プロジェクト名 : Identity4
  • ASP.NET 4.6 の ASP.NET Web アプリケーション
  • ASP.NET 4.6 テンプレート : MVC
  • 認証なし

プロジェクト名が Identity「4」となっているのに深い理由はございません。何度か試して、ようやく形になった、というだけでございます。

認証は自分自身でくっつけてまいりますので、「なし」でのスタートです♪

2. NuGet パッケージインストール

NuGet パッケージマネージャーからインストールいたします。

または、パッケージマネージャーコンソールを使う場合は次のコマンドを打ってくださいまし。わたくしたちはこちらのほうが好みですの♪

Install-Package Microsoft.AspNet.Identity.Owin.ja
Install-Package Microsoft.Owin.Host.SystemWeb.ja
Install-Package EntityFramework.ja

3. アプリ起動時の認証に関する設定を作成

3-1. Startup.cs

  • ファイル新規作成
  • startup.auth.cs と内容を分けるメリットが分からないが、お手本 (自動生成される認証コード) に従っておく
  • assembly の登録 ( ← 言い回し、理解が正しいかはナゾ)
  • 認証設定クラスの呼び出し。
  • public class Startup → public partial class Startup
using Microsoft.Owin;
using Owin;

[assembly: OwinStartupAttribute(typeof(Identity4.Startup))]
namespace Identity4
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

3-2. Startup.Auth.cs

  • ファイル新規作成
  • クッキーを使用すること、ログインページのパスの設定を行う。
  • namespace Identity4.App_Start → namespace Identity4 に変更
  • public class Startup → public partial class Startup に変更
  • また後で、1 要求につき 1 インスタンスのみを使用するように DB コンテキスト、ユーザー マネージャー、サインイン マネージャーを構成するコードを追加する。
using Identity4.Models;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;

namespace Identity4
{
    public partial class Startup
	{
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Users/Login")
            });
        }
	}
}

4. データベースモデルの作成

4-1. User モデルクラス

  • IUser インターフェースを実装して作成
  • IUser の string は Id の型
  • Id と UserName は Identity を使うために必須
  • プライマリキーとなる Id は String のため、[Key] とアノテーションを明示的につける。これによってマイグレーションでエラーとならずに実行できる。 ← 誤り。
  • Id には、get だけを設定していたが、それではマイグレーション時にエラーとなる。そのため set も設定した。
  • UserName と Password に [Required] をつけた。つけなくとも UserStore のメソッドを利用すれば、たとえばパスワード抜きで登録しようとしても警告を発してくれる。しかし、データベーステーブルとしても、Null を許容したくなかったので、明示的に指定した。
using Microsoft.AspNet.Identity;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Identity4.Models
{
    public class User : IUser<string>
    {
        public string Id { get; set; }

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

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

        [DisplayName("メモ")]
        public string Memo { get; set; }
    }
}

4-2. ApplicationDbContext クラス

  • DbContext を継承。これが自動生成認証だと、IdentityDbContext なのだが、これとは異なる点に注意
  • コンストラクタで親クラスに接続先のデータベースの名前を渡す。
  • プロパティとして、DB と関連付けるモデルを設定
  • DbContext ではなく、IdentityDbContext を継承すれば、UserStore クラスで context.saveChanges() とかやらなくて良いみたい。
    しかし、IdentityDbContext の継承は無理。なぜなら、User が IdentityUser ではなく、IUser を継承しているから。
    そして、User が IUser を継承しているのはプロパティを独自に設定したいため。
using System.Data.Entity;

namespace Identity4.Models
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext() : base("name=ApplicationDbContext")
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }

        public DbSet<User> Users { get; set; }
    }
}

4-3. Web.config

  • 接続データベースを指定する、connectionStrings タグ内容を設定
  • name の値を ApplicationDbContext クラスのコンストラクタで指定した、接続先のデータベースの名前と合わせる。
  • Initial Catalog、AttachDbFilename にデータベース名を設定する。

configuration タグ内の、該当部分のみ掲載

<connectionStrings>
  <add name="ApplicationDbContext" connectionString="Data Source=(localdb)\MSSQLLocalDB; Initial Catalog=Identity4Db; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|Identity4Db.mdf" providerName="System.Data.SqlClient" />
</connectionStrings>

5. 認証処理クラスの作成

5-1. IdentityConfig.cs ファイルを作って、UserManager と SignInManagerの実装

  • IdentityConfig.cs の namespace はプロジェクト直下の Identity4 としておく。お手本に合わせた。
  • 内容としては、UserManager、SignInManager を継承したのみで具体的なコードは書いていない。
  • しかし、コントローラーからなど認証系の関数を呼ぶときは、UserManager や SignInManager の関数を呼ぶことになる。
using Identity4.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;

namespace Identity4
{
    public class ApplicationUserManager : UserManager<User>
    {
        public ApplicationUserManager(IUserStore<User> store) : base(store)
        {
        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
        {
            return new ApplicationUserManager(new UserStore(context.Get<ApplicationDbContext>()));
        }
    }

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

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

5-2. UserStore の作成

  • 実際のデータベースアクセスなどを行うコードが書かれるクラス
  • どのフォルダに入れるのが定石なのか、不明。Models に配置した。
  • IUserStore、IUserPasswordStore インターフェースを実装する。ロールなど扱いたい場合は、対応するインターフェースをさらに実装する。
  • UserManager や SignInManager の関数から必要に応じて UserStore に実装した関数が呼ばれる。
using Microsoft.AspNet.Identity;
using System.Linq;
using System.Threading.Tasks;

namespace Identity4.Models
{
    public class UserStore :
        IUserStore<User>,
        IUserStore<User, string>,
        IUserPasswordStore<User, string>
    {
        private ApplicationDbContext db;

        public UserStore(ApplicationDbContext applicationDbContext)
        {
            db = applicationDbContext;
        }

        /// <summary>
        /// ユーザーを作成します。
        /// </summary>
        /// <param name="user">ユーザーオブジェクト</param>
        /// <returns>IdentityResult オブジェクト</returns>
        public Task CreateAsync(User user)
        {
            // この時点で ID が null だと例外
            // 1 つ以上のエンティティで検証が失敗しました。詳細については 'EntityValidationErrors' プロパティを参照してください。
            // ユーザー名重複チェックが自動的になされ、OK の場合のみここに来る。
            // パスワードは自動的に暗号化済みの状態でここに来る。
            db.Users.Add(user);
            db.SaveChanges();
            return Task.FromResult(default(object));
        }

        /// <summary>
        /// ユーザーを削除します。
        /// </summary>
        /// <param name="user">ユーザーオブジェクト</param>
        /// <returns>IdentityResult オブジェクト</returns>
        public Task DeleteAsync(User user)
        {
            User deletedTargetUser = db.Users.Find(user.Id);
            db.Users.Remove(deletedTargetUser);
            db.SaveChanges();
            return Task.FromResult(default(object));
        }

        /// <summary>
        /// ユーザーを Id を指定して取得します。
        /// </summary>
        /// <param name="userId">ユーザー ID</param>
        /// <returns>ユーザーオブジェクト</returns>
        public Task<User> FindByIdAsync(string userId)
        {
            var result = db.Users.Find(userId);
            return Task.FromResult(result);
        }

        /// <summary>
        /// ユーザーをユーザー名を指定して取得します。
        /// </summary>
        /// <param name="userName">ユーザー名</param>
        /// <returns>ユーザーオブジェクト</returns>
        public Task<User> FindByNameAsync(string userName)
        {
            var result = db.Users.FirstOrDefault(x => x.UserName == userName);
            return Task.FromResult(result);
        }

        /// <summary>
        /// ユーザー情報を更新します。
        /// </summary>
        /// <param name="user">ユーザーオブジェクト</param>
        /// <returns>IdentityResult オブジェクト</returns>
        public Task UpdateAsync(User user)
        {
            // コントローラーと同じ、db.Entry(user).State = EntityState.Modified; で更新しようとすると、下記のエラー
            // 例外の詳細: System.InvalidOperationException: 同じ型の別のエンティティに同じ主キー値が既に設定されているため、
            // 型 'Identity3.Models.ApplicationUser' のエンティティをアタッチできませんでした。
            // この状況は、グラフ内のエンティティでキー値が競合している場合に 'Attach' メソッドを使用するか、
            // エンティティの状態を 'Unchanged' または 'Modified' に設定すると発生する可能性があります。
            // これは、一部のエンティティが新しく、まだデータベースによって生成されたキー値を受け取っていないことが原因である場合があります。
            // この場合は、'Add' メソッドまたは 'Added' エンティティ状態を使用してグラフを追跡してから、
            // 必要に応じて、既存のエンティティの状態を 'Unchanged' または 'Modified' に設定してください。

            var updatedTargetUser = db.Users.Find(user.Id);
            updatedTargetUser.UserName = user.UserName;
            updatedTargetUser.Memo = user.Memo;
            // Q : 特別な処理無しで、パスワードはハッシュ化されて更新されるのか?
            // A: されない。よって、UpdateAsync ではパスワードを変更しない。
            //updatedTargetUser.Password = user.Password;
            db.SaveChanges();
            return Task.FromResult(default(object));
        }

        /// <summary>
        /// ユーザーにハッシュ化されたパスワードを設定します。
        /// </summary>
        /// <param name="user">ユーザーオブジェクト</param>
        /// <param name="passwordHash">パスワード文字列(未暗号化?)</param>
        /// <returns>IdentityResult オブジェクト</returns>
        public Task SetPasswordHashAsync(User user, string passwordHash)
        {
            user.Password = passwordHash;
            return Task.FromResult(default(object));
        }

        /// <summary>
        /// ユーザーからパスワードのハッシュを取得する
        /// </summary>
        /// <param name="user">ユーザーオブジェクト</param>
        /// <returns>パスワードハッシュ文字列</returns>
        public Task<string> GetPasswordHashAsync(User user)
        {
            var passwordHash = db.Users.Find(user.Id).Password;
            return Task.FromResult(passwordHash);
        }

        /// <summary>
        /// パスワードが設定されている場合に true を返却します。
        /// </summary>
        /// <param name="user">ユーザーオブジェクト</param>
        /// <returns>パスワードが設定されている場合は true、それ以外の場合は false</returns>
        public Task<bool> HasPasswordAsync(User user)
        {
            return Task.FromResult(user.Password != null);
        }

        public void Dispose()
        {
            // プロパティをここで明示的に破棄
            // 参考 → https://aspnet.codeplex.com/SourceControl/latest#Samples/Identity/AspNet.Identity.MySQL/RoleStore.cs
            if (db != null)
            {
                db.Dispose();
                db = null;
            }
        }
    }
}

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

  • ConfigureAuth メソッドでアプリが起動する最初の1回のみ実行され、そこで設定を行っている。(正確かどうか自信なし)
  • ApplicationDbContext、ApplicationUserManager、ApplicationSignInManager、を使用するアクションで実際に使用するときに、それぞれを Create メソッドでインスタンス化するようにする。
using Identity4.Models;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;

namespace Identity4
{
    public partial class Startup
	{
        public void ConfigureAuth(IAppBuilder app)
        {
            // 1 要求につき 1 インスタンスのみを使用するように DB コンテキスト、ユーザー マネージャー、サインイン マネージャーを構成します。
            app.CreatePerOwinContext(ApplicationDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
            app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Users/Login")
            });
        }
	}
}

以上で認証処理の実装は完了です!後は認証処理を使うデータベースや、コントローラー、ビューを作ってまいります♪

6. マイグレーション

マイグレーションを有効にし、マイグレーションに追加、そしてデータベースへ反映しました。

Enable-Migrations
Add-Migration Initial
Update-Database -Verbose

7. 動作確認用のコントローラー、ビューを作成

スキャフォールディングで、「Entity Framework を使用した、ビューがある MVC 5 コントローラー」から UsersController を作成いたしました。

自動で作成されたアクションのデータベース・アクセス部分を、UserManager、SignInManager を使用したものに修正していきます。

こうすることによって、ビューから渡ってきたデータの扱いをすべてカスタマイズして実装した Identity にまかせるようにします。

Login、ChangePassword はスキャフォールディングでは作成されませんので自作いたしました。

また、上部メニュー部に表示される「ようこそ」は部分ビューを使用し、ログイン、未ログイン状態で表示を変えるようにいたしました。これは自動生成される認証を参考にしております。

シンプルに作るために、Edit や Detail の Get 時に、URL や HTML ソースにユーザーの ID が表示されるようになっております。詳しくはありませんけれども、本当は ID は一切表に出さないようにしたほうがセキュリティ上安心かもしれませんわ。

7-1. HomeController.cs

  • Authorize 属性を付けて、ログインしなければ表示できないようにした。

該当部分のみ掲載

[Authorize]
public class HomeController : Controller
... 略 ...

7-2. UsersController.cs

  • ApplicationDbContext のように、ApplicationUserManager と ApplicationSignInManager もクラス変数として宣言しようとしたが、不可能だった。そのため、自動生成される認証を参考にして get と set を定義している。
  • Index アクションでは、ユーザーをすべて取得する。このとき、パスワードは含めないようにしたかったが、うまいこと LINQ を書くことができなかった。さらに、本来なら、コントローラーではなく UserStore に全ユーザー取得の処理を関数として書きたかった。
  • using Microsoft.AspNet.Identity.Owin; は Ctrl + . では追加できなかった。手動で追加することになるが、追加しないと SignInManager、UserManager の get 部分でエラーが発生した。
using Identity4.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

namespace Identity4.Controllers
{
    public class UsersController : Controller
    {
        private ApplicationDbContext db = new ApplicationDbContext();

        private ApplicationUserManager _userManager;
        private ApplicationSignInManager _signInManager;

        public UsersController()
        {
        }

        public UsersController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
        {
            UserManager = userManager;
            SignInManager = signInManager;
        }

        public ApplicationSignInManager SignInManager
        {
            get
            {
                return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
            }
            private set
            {
                _signInManager = value;
            }
        }

        public ApplicationUserManager UserManager
        {
            get
            {
                return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
            }
            private set
            {
                _userManager = value;
            }
        }

        // GET: Users
        public ActionResult Index()
        {
            // TODO これだと例外。なぜ。。。
            //var users = db.Users.Select(a => new User
            //{
            //    Id = a.Id,
            //    UserName = a.UserName,
            //    Memo = a.Memo
            //}).ToList<User>();

            var users = db.Users.ToList();
            return View(users);
        }

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

        [AllowAnonymous]
        [HttpPost]
        public async Task<ActionResult> Login(User user, string returnUrl)
        {
            var userForLogin = await UserManager.FindAsync(user.UserName, user.Password);
            if (userForLogin == null)
            {
                return View(user);
            }

            await SignInManager.SignInAsync(userForLogin, false, false);

            return RedirectToLocal(returnUrl);
        }

        // POST: /Home/LogOff
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
            return RedirectToAction("Login", "Users");
        }

        // GET: Users/Details/5
        public async Task<ActionResult> Details(string id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            var user = await UserManager.FindByIdAsync(id);
            if (user == null)
            {
                return HttpNotFound();
            }
            return View(user);
        }

        // GET: Users/Create
        public ActionResult Create()
        {
            return View();
        }

        // POST: Users/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Create([Bind(Include = "UserName,Password,Memo")] User user)
        {
            if (ModelState.IsValid)
            {
                // UserStore に定義した CreateAsync(user) を呼び出してはダメ。
                // ↓だとパスワードがハッシュ化されないため NG。
                //var result = await userManager.CreateAsync(user);
                // CreateAsync(user, applicationUser.Password) を呼び出すこと!
                // インターフェースを実装したメソッドを呼び出すのがダメだったので、どのメソッドを使うべきなのかわからなくて辛い。

                var userForCreate = new User
                {
                    Id = Guid.NewGuid().ToString(), UserName = user.UserName, Memo = user.Memo
                };
                var result = await UserManager.CreateAsync(userForCreate, user.Password);
                if (result.Succeeded)
                {
                    // 作成したユーザで即ログインする。
                    var signInUser = await UserManager.FindByNameAsync(userForCreate.UserName);
                    if (signInUser == null)
                    {
                        return View(user);
                    }
                    await SignInManager.SignInAsync(signInUser, isPersistent: false, rememberBrowser: false);

                    return RedirectToAction("Index");
                }
                AddErrors(result);
            }
            return View(user);
        }

        // GET: Users/Edit/5
        public async Task<ActionResult> Edit(string id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            var user = await UserManager.FindByIdAsync(id);
            if (user == null)
            {
                return HttpNotFound();
            }
            return View(user);
        }

        // POST: Users/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Edit([Bind(Include = "Id,UserName,Memo")] User user)
        {
            if (ModelState.IsValid)
            {
                var result = await UserManager.UpdateAsync(user);
                if (result.Succeeded)
                {
                    return RedirectToAction("Index");
                }
                AddErrors(result);
            }
            return View(user);
        }

        [Authorize]
        // GET: /Manage/ChangePassword
        public ActionResult ChangePassword()
        {
            return View();
        }

        //
        // POST: /Manage/ChangePassword
        [Authorize]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> ChangePassword(ChangePasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword);
            if (result.Succeeded)
            {
                var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
                if (user != null)
                {
                    await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
                }
                return RedirectToAction("Index");
            }
            AddErrors(result);
            return View(model);
        }

        // GET: Users/Delete/5
        public async Task<ActionResult> Delete(string id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            var user = await UserManager.FindByIdAsync(id);
            if (user == null)
            {
                return HttpNotFound();
            }
            return View(user);
        }

        // POST: Users/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> DeleteConfirmed(string id)
        {
            var user = await UserManager.FindByIdAsync(id);
            if (user == null)
            {
                return HttpNotFound();
            }
            var result = await UserManager.DeleteAsync(user);
            if (result.Succeeded)
            {
                // 現在ログイン中のユーザを削除していたらログアウトする。
                var signInUser = await UserManager.FindByIdAsync(User.Identity.GetUserId());
                if (signInUser == null)
                {
                    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
                    return RedirectToAction("Index");
                }
                await SignInManager.SignInAsync(signInUser, false, false);

                return RedirectToAction("Index");
            }
            AddErrors(result);
            return HttpNotFound();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }

        #region ヘルパー
        private IAuthenticationManager AuthenticationManager
        {
            get
            {
                return HttpContext.GetOwinContext().Authentication;
            }
        }

        private void AddErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }

        private ActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            return RedirectToAction("Index", "Home");
        }
        #endregion
    }
}

7-3. _Layout.cshtml

  • 上部メニューに <li>@Html.ActionLink("ユーザー", "Index", "Users")</li> を追加してすぐにアクセスできるようにした。
  • ログイン状態がわかるように、@Html.Partial("_LoginPartial") を追加した。

該当部分 (上部メニュー部分) のみ掲載

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("ホーム", "Index", "Home")</li>
        <li>@Html.ActionLink("詳細", "About", "Home")</li>
        <li>@Html.ActionLink("連絡先", "Contact", "Home")</li>
        <li>@Html.ActionLink("ユーザー", "Index", "Users")</li>
    </ul>
    @Html.Partial("_LoginPartial")
</div>

7-4. _LoginPartial.cshtml

  • ログインしていれば、ユーザー名と Details アクションへのリンク、ログオフリンクを表示。
  • 未ログイン状態ならば、Login アクションへのリンクを表示。
  • 自動生成される認証の同じ部分をほぼそのまま使用。
@using Microsoft.AspNet.Identity
@if (Request.IsAuthenticated)
{
    using (Html.BeginForm("LogOff", "Users", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
    {
        @Html.AntiForgeryToken()

        <ul class="nav navbar-nav navbar-right">
            <li>
                @Html.ActionLink("こんにちは、" + User.Identity.GetUserName() + "さん", "Details", "Users", routeValues: new { id = User.Identity.GetUserId() }, htmlAttributes: null)
            </li>
            <li><a href="javascript:document.getElementById('logoutForm').submit()">ログオフ</a></li>
        </ul>
    }
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("ログイン", "Login", "Users", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

7-5. Index.cshtml

  • パスワードは表示しないようにした。
@model IEnumerable<Identity4.Models.User>

@{
    ViewBag.Title = "ユーザ一覧";
}

<h2>@ViewBag.Title</h2>

<p>
    @Html.ActionLink("新規作成", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.UserName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Memo)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.UserName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Memo)
        </td>
        <td>
            @Html.ActionLink("編集", "Edit", new { id=item.Id }) |
            @Html.ActionLink("詳細", "Details", new { id = item.Id }) |
            @Html.ActionLink("削除", "Delete", new { id=item.Id })
        </td>
    </tr>
}

</table>

7-6. Details.cshtml

  • これもパスワードは表示しないようにした。
@model Identity4.Models.User

@{
    ViewBag.Title = "ユーザ詳細";
}

<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.UserName)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.UserName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Memo)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Memo)
        </dd>

    </dl>
</div>
<p>
    @Html.ActionLink("編集", "Edit", new { id = Model.Id }) |
    @Html.ActionLink("一覧へ戻る", "Index")
</p>

7-7. Create.cshtml

@model Identity4.Models.User

@{
    ViewBag.Title = "ユーザ新規作成";
}

<h2>@ViewBag.Title</h2>


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <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" } })
                @Html.ValidationMessageFor(model => model.UserName, "", new { @class = "text-danger" })
            </div>
        </div>

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

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

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="新規作成" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("一覧へ戻る", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

7-8. Edit.cshtml

  • パスワードはこのページではなく、別のページで変更させる。
  • そのため、パスワード変更用のアクションへのリンクを設置する。
@model Identity4.Models.User

@{
    ViewBag.Title = "ユーザ編集";
}

<h2>@ViewBag.Title</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Id)

        <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" } })
                @Html.ValidationMessageFor(model => model.UserName, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <p class="form-control-static">@Html.ActionLink("パスワードの変更", "ChangePassword")</p>
            </div>
        </div>

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

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="保存" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("一覧へ戻る", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

7-9. Delete.cshtml

  • これもパスワードは表示しないようにした。
@model Identity4.Models.User

@{
    ViewBag.Title = "ユーザ削除";
}

<h2>@ViewBag.Title</h2>

<h3>ユーザを削除してもよろしいですか?</h3>
<div>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.UserName)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.UserName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Memo)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Memo)
        </dd>

    </dl>

    @using (Html.BeginForm()) {
        @Html.AntiForgeryToken()

        <div class="form-actions no-color">
            <input type="submit" value="削除" class="btn btn-default" /> |
            @Html.ActionLink("一覧へ戻る", "Index")
        </div>
    }
</div>

7-10. Login.cshtml

  • @Html.TextBoxFor でユーザー名フォームを生成
  • @Html.PasswordFor でパスワードフォームを生成
@model Identity4.Models.User

@{
    ViewBag.Title = "ログイン";
}

<h2>ログイン</h2>


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>User</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.UserName, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextBoxFor(model => model.UserName, new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.UserName, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.PasswordFor(model => model.Password, new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="ログイン" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

7-11. ChangePassword

  • User モデルではパスワードを複数扱えないため、このページ用のビューモデルを用意した。
  • 自動生成される認証の ChangePassword をほぼそのまま使用した。
using System.ComponentModel.DataAnnotations;

namespace Identity4.Models
{
    public class ChangePasswordViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "現在のパスワード")]
        public string OldPassword { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "新しいパスワード")]
        public string NewPassword { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "新しいパスワードの確認入力")]
        [Compare("NewPassword", ErrorMessage = "新しいパスワードと確認のパスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }
}
@model Identity4.Models.ChangePasswordViewModel
@{
    ViewBag.Title = "パスワードの変更";
}

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm("ChangePassword", "Users", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>パスワード変更フォーム</h4>
    <p>現在ログイン中のユーザパスワードを変更します。</p>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.OldPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="パスワードの変更" class="btn btn-default" />
        </div>
    </div>
}
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

これで、完成です!Ctrl + F5 で実際に確認いたしましょう♪

おわりに

認証の勉強は具体的なコードのある ASP.NET Identityで独自データストアからデータを取得する(ログインからロールまで) – かずきのBlog@hatena ページで進めておりました。

勉強にもなりますし、これをそのまま使いたいと思いましたわ。ですけれども、ユーザーの保存先がメモリでしたのでデータベースへと変更したかったのですの。

全然わかりませんでしたわ><。

そこで、Overview of Custom Storage Providers for ASP.NET Identity | The ASP.NET Site で概要を少しでもつかみ、なんとか本投稿のサンプルを作れるまで理解することができました♪

以上です。

コメントを残す