WebSurfer's Home

Filter by APML

MVC5 でのロールによるアクセス制限

by WebSurfer 12. October 2020 15:05

.NET Framework 版の ASP.NET MVC5 アプリで、ユーザーはログインしているが、アクセス権がないコントローラーまたはアクションメソッドにアクセスした場合、以下のようなメッセージを表示する方法を書きます。(Core 3.1 版でのデフォルトです)

AccessDenied

ASP.NET Identity ベースのユーザー認証に、先の記事「ASP.NET Identity のロール管理 (MVC)」のようにしてロールを実装していることが条件です。

.NET Framework 版の ASP.NET MVC5 アプリではコントローラーやアクションメソッドに AuthorizeAttribute 属性を付与し、名前付きパラメータ Roles プロパティにアクセスを許可するロール名を指定することにより、指定されたロールに属するユーザー以外のアクセスを制限することができます。

ただし、デフォルトでは、ユーザーがログイン済みでも指定されたロールに属さない場合、ロールで制限されたページにアクセスすると Login ページにリダイレクトされます。ログイン済なのに Login ページに飛ばされるというのがユーザーフレンドリーではない感じです。

Core 3.1 版の ASP.NET MVC アプリでは、ユーザーがログイン済みだが指定された ロールに属さない場合、Login ページではなくて、上の画像のようなアクセス権がないというメッセージを表示するページにリダイレクトされます。

Core 3.1 版の方がユーザーフレンドリーのように思いますので、.NET Framework 版の MVC5 でも同様なことができないかを考えてみました。方法は以下の 2 つがありそうです。

  1. AuthorizeAttribute クラスを継承したカスタム認証フィルターを定義し、OnAuthorization メソッドを override して使う。
  2. FilterAttribute, IAuthorizationFilter を継承したカスタム認証フィルターを自作してそれを利用する。  

後者の方法で作ったカスタム認証フィルターのコードを以下にアップしておきます。(AccessDenied アクションメソッドとビューの追加も必要ですので忘れないようにしてください。追加しないと 404 エラーになります)

using System;
using System.Web;
using System.Web.Mvc;

namespace Mvc5App2.Filters
{
    public class RoleAuthFilterAttribute : 
        FilterAttribute, IAuthorizationFilter
    {
        private string role;

        public RoleAuthFilterAttribute(string role)
        {
            this.role = role;
        }
        
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (!HttpContext.Current.Request.IsAuthenticated)
            {
                string path = "/Account/Login?returnUrl=" + 
                              HttpContext.Current.Request.RawUrl;
                filterContext.Result = new RedirectResult(path);
            }
            else 
            {
                if (!HttpContext.Current.User.IsInRole(this.role))
                {
                    filterContext.Result = 
                        new RedirectResult("/Account/AccessDenied");
                }
            }
        }
    }
}

ASP.NET Identity ベースのユーザー認証にロール管理を実装した場合、ユーザーが認証されているか否か、指定されたロールに属しているか否かはフレームワーク組み込みのプロパティ/メソッドを利用して判定することができます。上のコードの IsAuthenticated プロパティ、IsInRole メソッドがそれです。

独自認証の場合はそのあたりは自力で実装することになります。かなり大変かも。

上のコードの認証フィルターの使い方の例は下の画像の通りです。

認証フィルターの使い方

Visual Studio 2019 のテンプレートを使って作った Home/Contact アクションメソッドに認証フィルター属性を付与して Administrator 以外のアクセスを制限しています。

Tags: , ,

MVC

ASP.NET Core Identity 独自実装(その2)

by WebSurfer 6. September 2020 16:29

先の記事「ASP.NET Core Identity 独自実装(その1)」と「Register, Login, Logout 機能の実装」の続きです。

ロール管理画面

先の記事では ASP.NET Core Identity のユーザー認証に用いるデータソースとカスタムプロバイダを作成し、それを用いてユーザー認証機能を実装しました。今回、ロール機能を追加しましたので、その方法などを備忘録として書いておきます。

先の記事「ASP.NET Core Identity 独自実装(その1)」の一番上に表示した画像を見てください。前回、その画像の Data Source, Data Access Layer レイヤーおよび Identity Store レイヤーの中のカスタムプロバイダ UserStore までは作成し、期待通りの動作をすることは確認しました。

今回、ロール機能を実現するためのカスタムプロバイダ RoleStore を実装し、先に作成したカスタムプロバイダ UserStore にロールによるアクセス制限のための機能を追加します。

(1) RoleStore クラスの実装

先に骨組みだけ作っておきましたが、それに中身を実装します。作成済みの UserStore のメソッドを参考にしました。

上位レイヤーにある RoleInManager がこのクラスに定義したメソッドを使って、先に作成済みの Data Access Layer を介して SQL Server の Role テーブルにアクセスし、ロールデータの作成、取得、削除などの操作を行います。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MvcIdCustom.Models;

namespace MvcIdCustom.DAL
{
    public class RoleStore : IRoleStore<Role>, 
                             IQueryableRoleStore<Role>
    {
        private DataContext db;

        public RoleStore(DataContext db)
        {
            this.db = db;
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (db != null)
                {
                    db.Dispose();
                    db = null;
                }
            }
        }

        // IQueryableRoleStore<Role> のメンバーの Roles プロパティ
        public IQueryable<Role> Roles
        {
            get { return db.Roles.Select(r => r); }
        }

        // 同じロール名の二重登録の防止機能は上位レイヤーに組み込まれている
        public async Task<IdentityResult> CreateAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            db.Add(role);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{role.Name} 登録失敗" });
        }

        // 同じロール名の二重登録の防止機能は上位レイヤーに組み込まれている
        public async Task<IdentityResult> UpdateAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            db.Update(role);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{role.Name} 更新失敗" });
        }

        public async Task<IdentityResult> DeleteAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            db.Remove(role);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{role.Name} 削除失敗" });
        }

        public Task<string> GetRoleIdAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            return Task.FromResult(role.Id.ToString());
        }

        public Task<string> GetRoleNameAsync(Role role,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            return Task.FromResult(role.Name);
        }

        public Task SetRoleNameAsync(Role role, 
            string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            role.Name = roleName;

            return Task.CompletedTask;
        }

        public Task<string> GetNormalizedRoleNameAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            return Task.FromResult(role.Name);
        }

        public Task SetNormalizedRoleNameAsync(Role role, 
            string normalizedName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));
            if (string.IsNullOrEmpty(normalizedName))
                throw new ArgumentException("normalizedName");

            return Task.CompletedTask;
        }

        public async Task<Role> FindByIdAsync(string roleId, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (int.TryParse(roleId, out int id))
            {
                return await db.Roles.SingleOrDefaultAsync(r => r.Id == id, 
                    cancellationToken);
            }
            else
            {
                return await Task.FromResult<Role>(null);
            }
        }

        public async Task<Role> FindByNameAsync(string normalizedRoleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrEmpty(normalizedRoleName))
                throw new ArgumentException("normalizedRoleName");

            Role role = await db.Roles.SingleOrDefaultAsync(
                r => r.Name.Equals(normalizedRoleName.ToLower()), 
                cancellationToken);

            return role;
        }
    }
}

(2) UserStore クラスへの追加

ロールを使えるようにするには、先に作成した UserStore クラス に機能の追加を行う必要があります。

具体的には、以下のように IUserRoleStore<User> インターフェースを継承し、AddToRoleAsunc, GetRolesAsync, IsInRoleAsync, RemoveFromRoleAsync メソッドを実装します。

using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Threading;
using System.Threading.Tasks;

namespace MvcIdCustom.DAL
{
    public class UserStore : IUserStore<User>, 
                             IUserPasswordStore<User>, 
                             IQueryableUserStore<User>, 
                             IUserRoleStore<User>       // 追加
    {

        // ・・・中略・・・

        // Visual Studio のテンプレートで「個別のユーザーアカウント」を
        // 選んで自動生成される AspNetUserRoles テーブルは UserId と
        // RoleId の 2 つのフィールドのみが含まれ、それが連結主キーにな
        // っているので UserId と RoleId の組み合わせが重複することはない。

        // 一方、このプロジェクトで作った UserRole テーブルは Id, UserId,
        // RoleId の 3 つのフィールドを持ち Id が主キーになっている。なの
        // で UserId と RoleId の組み合わせが重複できてしまう。以下のコード
        // では重複設定できないように考えた(つもり)

        public async Task AddToRoleAsync(User user, string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // roleName に該当する Role オブジェクトを取得
            var role = await db.Roles.SingleOrDefaultAsync(
                             role => role.Name.Equals(roleName), 
                             cancellationToken);

            // roleName という名前のロールは存在しないので何もせず return
            if (role == null) return;

            // UserRole テーブルの UserId と RoleId が user.Id と role.Id
            // と一致する UserRole オブジェクトを取得
            var userRole = await db.UserRoles.SingleOrDefaultAsync(
                                 userRole => userRole.UserId.Equals(user.Id) && 
                                             userRole.RoleId.Equals(role.Id),
                                 cancellationToken);

            // user は roleName という名前のロールには既にアサイン済みなので
            // 何もせず return
            if (userRole != null) return;

            userRole = new UserRole
            {
                UserId = user.Id,
                RoleId = role.Id
            };

            db.UserRoles.Add(userRole);
            await db.SaveChangesAsync(cancellationToken);
        }

        public async Task RemoveFromRoleAsync(User user, string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // roleName に該当する Role オブジェクトを取得
            var role = await db.Roles.SingleOrDefaultAsync(
                             role => role.Name.Equals(roleName), 
                             cancellationToken);

            // roleName という名前のロールは存在しないので何もせず return
            if (role == null) return;

            // UserRole テーブルの UserId と RoleId が user.Id と role.Id
            // と一致する UserRole オブジェクトを取得
            var userRole = await db.UserRoles.SingleOrDefaultAsync(
                                userRole => userRole.UserId.Equals(user.Id) &&
                                            userRole.RoleId.Equals(role.Id), 
                                cancellationToken);

            // user は roleName という名前のロールにはアサインされてないので
            // 何もせず return
            if (userRole == null) return;

            db.UserRoles.Remove(userRole);
            await db.SaveChangesAsync(cancellationToken);
        }

        public async Task<IList<string>> GetRolesAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw 
                new ArgumentNullException(nameof(user));

            // UserRole テーブルの UserId フィールドが user.Id と一致する
            // UserRole オブジェクトを取得。Include を使ってナビゲーション
            // プロパティ経由 Role を紐づけているので Select メソッドで
            // ロール名のリストを取得できる
            var roleNames = await db.UserRoles
                .Include(userRole => userRole.Role)
                .Where(userRole => userRole.UserId.Equals(user.Id))
                .Select(userRole => userRole.Role.Name).ToListAsync();

            // 一件も見つからない時は null ではなく中身が空(Count がゼロ)
            // の List<string> オブジェクトを返す
            return roleNames;
        }

        public async Task<bool> IsInRoleAsync(User user, string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // UserRole テーブルの UserId フィールドが user.Id と一致する
            // UserRole オブジェクトを取得。Include を使ってナビゲーション
            // プロパティ経由 Role を紐づけているので Select メソッドで
            // ロール名のリストを取得できる
            var roleNames = await db.UserRoles
                .Include(userRole => userRole.Role)
                .Where(userRole => userRole.UserId.Equals(user.Id))
                .Select(userRole => userRole.Role.Name).ToListAsync();

            if (roleNames != null && roleNames.Count > 0)
            {
                // 引数 roleName は大文字に変換されて渡されるので ToUpper()
                // がないと true にならない。
                bool isInRole = roleNames.Any(
                                name => name.ToUpper() == roleName);
                return isInRole;
            }

            return false;
        }

        public async Task<IList<User>> GetUsersInRoleAsync(string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // roleName に該当する Role オブジェクトを取得
            var role = await db.Roles.SingleOrDefaultAsync(
                             role => role.Name.Equals(roleName),
                             cancellationToken);

            if (role != null)
            {
                var users = await db.UserRoles
                    .Include(userRole => userRole.User)
                    .Where(userRole => userRole.RoleId.Equals(role.Id))
                    .Select(userRole => userRole.User).ToListAsync();

                // 一件も見つからない時は null ではなく中身が空の
                // List<User> オブジェクトを返すはず
                return users;
            }

            return new List<User>();
        }
    }
}

(3) ロール管理画面の実装

先の記事「ASP.NET Identity のロール管理 (CORE)」と同等な方法で、ロールの表示・追加・変更・削除およびユーザーへのロールのアサイン・解除を行う機能を実装しました。

ただし、その記事に書いた「(1) ロールサービスの追加」は不要ですので注意してください。

実装した機能を使ってロールを作成し、ユーザーをロールにアサインした結果がこの記事の一番上にある画像です。

(4) /Account/AccessDenied の実装

コントローラーやアクションメソッドに [Authorize(Roles ="アドミ")] 属性を付与すると(ロール名は日本語 OK)、「アドミ」ロールを有してないユーザーがアクセスした場合は /Account/AccessDenied にリダイレクトされます。

/Account/AccessDenied を実装してないと、リダイレクトされた際 HTTP 404 Not Found エラーとなってしまいますので実装は必要です。

リダイレクトはフレームワークが自動的に行いますので、ユーザーにアクセス権限がないことを通知するだけの簡単なページを作れば OK です。


以上で完了です。

ロールによるアクセス制限は有効に働くようになります。ユーザーの持つロールに応じて /Account/Login および /Account/AccessDenied へのリダイレクト、許可されていればページの表示が期待通り動きます。

ユーザーがアサインされているロールの削除、ロールがアサインされているユーザーの削除も、内部で階層更新が行われているのか、問題なく実行でき、結果は期待通り SQL Server の当該テーブル反映されます。

その他、同じロール名の二重登録の防止機能が組み込まれており、例えば登録済みの Xxxxx というロール名を登録しようとすると Role name 'Xxxxx' is already taken. というエラーメッセージが出て登録に失敗します。(DB にユニーク制約は付けてないのですが・・・)

上記は上位レイヤーで処理されているようです。上記以外にもパスワードのハッシュや認証クッキーの発行など上位レイヤーの機能は多々あり、それを自力で実装するのは非常に難しいので、カスタム化する範囲は Identity Store レイヤー以下でないと手に負えないと思いました。

Tags: , , , ,

Authentication

Register, Login, Logout 機能の実装 (CORE)

by WebSurfer 5. September 2020 14:12

先の記事「ASP.NET Core Identity 独自実装(その1)」の続きです。

先の記事では ASP.NET Core Identity のユーザー認証に用いるデータソースとプロバイダを独自実装し、ユーザー情報の表示、追加、変更、削除が可能になるところまでは確認できました。この記事では Register, Login, Logout 機能を実装してみます。

Home/Index 画面

認証に「個別のユーザーアカウント」を選んでプロジェクトを作成した場合 Razor Class Library (RCL) として Login, Register 等の機能は提供され、ソースコードも入手することができます。そのコードを参考に Register, Login, Logout 用のコントローラーとビューを実装しました。

ユーザー認証の動きは、普通に ASP.NET Identity の「個別のユーザーアカウント」を選んで実装したときと同じで、以下のようになります。

初期画面では、上の画像のように、ユーザーがログインしてないときは Register, Login へのリンクを表示します。ページ右上の Login をクリックまたはアクセス制限がかかっているページを呼び出すとログイン画面に遷移します。

ログイン

有効なユーザー名とパスワードを入力して [Login] ボタンをクリックすると、以下の Fiddler の画像の通り認証クッキーが発行されます。

認証クッキー

この例では、アクションメソッドに [Authorize] を付与してアクセス制限がかかっている Home/Pricacy を要求したので、応答ヘッダの Location に /Home/Privacy が設定されています。

Location の設定により /Home/Privacy ページにリダイレクトされます。その際、認証クッキーも送られますのでユーザーは認証され、以下のように /Home/Privacy 画面が表示されます。

ログイン成功

どこでどのように実現しているのか分かりませんが、アクセス制限がかかっているページからログインページへの自動リダイレクトや認証クッキーの発行などは自力で実装しなくてもフレームワークが面倒を見てくれるようです。他には以下のことを自動的に行ってくれるのを確認しました。

  • 登録時のユーザー名とパスワードの検証は、ビューモデルのプロパティに付与したアノテーション属性に加えて、Microsoft のドキュメントの「Configure Identity services」のセクションのコードにある Password settings と User settings の設定が有効になります。
  • 同じユーザー名の二重登録の防止機能も組み込まれているようです。例えば、Register ページで既に登録済みの Surfer というユーザー名を登録しようとすると User name 'Surfer' is already taken. というエラーメッセージが出て登録に失敗します。(DB にユニーク制約は付けてないのですが・・・)
  • パスワードは自動的にハッシュされ、生のパスワードが DB に保存されることはありません。ハッシュのアルゴリズムは、参考にした記事の Customize ASP.NET Core Identity によると、PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations になるそうです。既存の DB にユーザー情報があるなどで、ハッシュのアルゴリズムが上記と違う場合は問題ですが、IPasswordHasher を継承したカスタム PasswordHasher を作成してサービスとして Startup.cs に登録することで対応できるようです。
  • 永続化機能も働きます。上のログイン画面で[このアカウントを記憶する]にチェックを入れると、応答ヘッダに含まれる認証クッキーの Set-Cookie: に expires 属性が追加されます。有効期限は 2 週間先になっていました。有効期限は Startup.cs の ConfigureServices メソッドに以下のような設定を追加することで変更できます。
services.ConfigureApplicationCookie(options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(5); // 5 分に設定
});

最後に、この記事で実装した Register, Login, Logout 関係のコードを以下に書いておきます。

_LoginPartial.cshtml

ページの右上にログイン状態を表示するためのパーシャルビュー _LoginPartial.cshtml を作成し、それを _Layout.schtml に配置します。コードは以下の通りです。「個別のユーザーアカウント」を選んで作ったプロジェクトのものをコピーして、それに手を加えました。

ログインすると表示されるユーザー名のリンク先は、「個別のユーザーアカウント」を選んで作ったプロジェクトの場合は管理用のページ Manage になるのですが、この記事では単にユーザー一覧が表示されるだけの Account/Index アクションメソッドにしています。

@using Microsoft.AspNetCore.Identity

@inject SignInManager<User> SignInManager
@inject UserManager<User> UserManager

<ul class="navbar-nav">
    @if (SignInManager.IsSignedIn(User))
    {
        <li class="nav-item">            
            <a id="manage" class="nav-link text-dark" asp-action="Index" 
                asp-controller="Account" title="Manage">
                Hello @UserManager.GetUserName(User) !
            </a>
        </li>
        <li class="nav-item">
            <form id="logoutForm" class="form-inline" asp-action="Logout" 
                  asp-controller="Account" 
                  asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
                <button id="logout" type="submit" class="nav-link btn btn-link text-dark">
                    Logout
                </button>
            </form>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" id="register" asp-action="Register" 
               asp-controller="Account">Register</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" id="login" asp-action="Login" 
               asp-controller="Account">Login</a>
        </li>
    }
</ul>

ビューモデル

Login と Register 用のビューモデルを作成します。MVC で言う Model で、コントローラとビューの間のデータのやり取りに使います。

using System.ComponentModel.DataAnnotations;

namespace MvcIdCustom.Models
{
    // AccountController の Login 用
    public class LoginViewModel
    {
        [Required(ErrorMessage = "{0}は必須です。")]
        [Display(Name = "ユーザー名")]
        [StringLength(100, ErrorMessage =
            "{0}は{2}から{1}文字の範囲で設定してください。",
            MinimumLength = 6)]
        public string UserName { get; set; }

        [Required(ErrorMessage = "{0}は必須です。")]
        [StringLength(100, ErrorMessage =
            "{0}は{2}から{1}文字の範囲で設定してください。",
            MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [Display(Name = "このアカウントを記憶する")]
        public bool RememberMe { get; set; }
    }

    // AccountController の Register 用
    public class RegisterViewModel
    {
        [Required(ErrorMessage = "{0}は必須です。")]
        [Display(Name = "ユーザー名")]
        [StringLength(100, ErrorMessage =
            "{0}は{2}から{1}文字の範囲で設定してください。",
            MinimumLength = 6)]
        public string UserName { get; set; }

        [Required(ErrorMessage = "{0}は必須です。")]
        [StringLength(100, ErrorMessage =
            "{0}は{2}から{1}文字の範囲で設定してください。",
            MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "パスワード確認")]
        [Compare("Password", 
            ErrorMessage = "確認パスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }
}

コントローラ / アクションメソッド

アクションメソッド Index へはログインするとページ右上に表示されるユーザー名からリンクが張られています。アクションメソッド Register, Login, Logout を、上に述べた _LoginPartial.cshtml のリンクから呼び出すことができます。

なお、Login ページについては、手動でリンクをクリックしなくとも、アクセス制限されているページを匿名ユーザーが要求した場合は自動的にリダイレクトされます。

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using MvcIdCustom.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;

namespace MvcIdCustom.Controllers
{
    public class AccountController : Controller
    {
        private readonly UserManager<User> _userManager;
        private readonly SignInManager<User> _signInManager;

        public AccountController(UserManager<User> userManager, 
                                 SignInManager<User> signInManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
        }

        public async Task<IActionResult> Index()
        {
            // using Microsoft.EntityFrameworkCore;
            var users = await _userManager.Users.
                              OrderBy(user => user.UserName).ToListAsync();
            return View(users);
        }

        // GET: Account/Login/ReturnUrl
        // Model は Models/UserViewModel.cs の LoginViewModel
        [AllowAnonymous]
        public IActionResult Login(string returnUrl)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        // POST: Account/Login/ReturnUrl
        [AllowAnonymous]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(string returnUrl, 
            [Bind("UserName,Password,RememberMe")] LoginViewModel model)
        {
            returnUrl = returnUrl ?? Url.Content("~/");

            if (ModelState.IsValid)
            {                
                var result = await _signInManager.
                    PasswordSignInAsync(model.UserName, 
                                        model.Password, 
                                        model.RememberMe, 
                                        lockoutOnFailure: false);
                if (result.Succeeded)
                {                    
                    return LocalRedirect(returnUrl);
                }
                else
                {
                    ModelState.AddModelError(string.Empty, "無効なログイン");
                    return View(model);
                }
            }

            return View(model);
        }

        // POST: Account/Logout
        [AllowAnonymous]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Logout(string returnUrl)
        {
            await _signInManager.SignOutAsync();

            // _LayoutPartial.cshtml の form 要素の
            // asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })
            // により returnUrl は "/" になる。
            if (returnUrl != null)
            {
                return LocalRedirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }

        // GET: Account/Register
        // Model は Models/UserViewModel.cs の RegisterViewModel
        [AllowAnonymous]
        public IActionResult Register(string returnUrl)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        // POST: Account/Register
        [AllowAnonymous]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Register(string returnUrl, 
            [Bind("UserName,Password,ConfirmPassword")] RegisterViewModel model)
        {
            returnUrl = returnUrl ?? Url.Content("~/");

            if (ModelState.IsValid)
            {
                var user = new User { UserName = model.UserName };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {                    
                    await _signInManager.SignInAsync(user, isPersistent: false);
                    return LocalRedirect(returnUrl);                    
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            return View(model);
        }
    }
}

ビュー (Login.cshtml)

Login アクションメソッド用のビューのコードのみ載せておきます。他はスキャフォールディング機能で生成したものをほぼそのまま使えますので省略します。

タグヘルパー form の asp-route-returnUrl 属性に "@ViewBag.ReturnUrl" を設定しているところに注目してください。アクセス制限がかかっているページにアクセスすると、そのページの URL が設定され、ログイン後 URL に設定されたページが表示されるようになっています。

@model MvcIdCustom.Models.LoginViewModel

@{
    ViewData["Title"] = "Login";
}

<h1>Login</h1>

<h4>LoginViewModel</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Login" asp-controller="Account" 
              asp-route-returnUrl="@ViewBag.ReturnUrl">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="UserName" class="control-label"></label>
                <input asp-for="UserName" class="form-control" />
                <span asp-validation-for="UserName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password" class="control-label"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group form-check">
                <label class="form-check-label">
                    <input class="form-check-input" asp-for="RememberMe" /> 
                    @Html.DisplayNameFor(model => model.RememberMe)
                </label>
            </div>
            <div class="form-group">
                <input type="submit" value="Login" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Tags: , , , ,

Authentication

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  March 2025  >>
MoTuWeThFrSaSu
242526272812
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar