WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Identity のユーザー管理 (CORE)

by WebSurfer 2020年5月31日 11:45

ASP.NET Core Identity ベースのユーザー認証を実装した ASP.NET Core 3.1 MVC アプリで、管理者がユーザー情報の追加、変更、削除を行う機能を実装する例を書きます。

注: 下に出てくる「ハンドル名 HandleName」はプロファイル情報として後から追加したものでデフォルトでは含まれないので注意してください。

ユーザー管理画面

(登録ユーザーが多くなってくると、上の画面のような全員の一覧表示では管理が難しく、何らかの絞り込み機能が必要と思いますが、それは次の課題ということで・・・  手抜きですみません)

先の記事「ASP.NET Identity のユーザー管理 (MVC)」で .NET Framework 版の ASP.NET MVC5 の例を書きましたが、この記事は Core 3.1 版です。

ベースとなるのは、Visual Studio 2019 のテンプレートを使って、先の記事「ASP.NET Identity で MySQL 利用 (CORE 版)」のステップ (1)、(2) の手順で作成した ASP.NET Core 3.1 MVC アプリです。そのプロジェクトはデータベースに MySQL を使っていますが、この記事の手順は SQL Server でも同じです。

(1) Model の作成

プロジェクトのルート直下の Models フォルダに UserModel.cs というクラスファイルを追加し(名前は任意)、その中に以下のクラスを定義して使います。

using System;
using System.ComponentModel.DataAnnotations;

namespace MySQLIdentity.Models
{
    // UserController の Index, Details, Delete 用
    public class UserModel
    {
        public string Id { get; set; }

        [Display(Name = "ユーザー名")]
        public string UserName { get; set; }

        [Display(Name = "ハンドル名")]
        public string HandleName { get; set; }

        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        [Display(Name = "メール確認済")]
        public bool EmailConfirmed { get; set; }

        [Display(Name = "電話番号")]
        public string PhoneNumber { get; set; }

        [Display(Name = "電話番号確認済")]
        public bool PhoneNumberConfirmed { get; set; }

        [Display(Name = "二要素認証")]
        public bool TwoFactorEnabled { get; set; }

        [Display(Name = "ロック有効化")]
        public bool LockoutEnabled { get; set; }

        [Display(Name = "ロック終了日時")]
        public DateTimeOffset? LockoutEnd { get; set; }

        [Display(Name = "アクセス失敗数")]
        public int AccessFailedCount { get; set; }
    }

    // UserController の Create 用
    public class RegisterViewModel
    {
        [Required(ErrorMessage = "{0}は必須です。")]
        [EmailAddress]
        [Display(Name = "メールアドレス")]
        public string Email { 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; }
    }

    // UserController の Edit 用
    // 上の RegisterViewModel を使うとパスワードを入力しない場合は検証に
    // 通らない。違いは Password プロパティの [Required] を外しただけ
    public class EditViewModel
    {
        [Required(ErrorMessage = "{0}は必須です。")]
        [EmailAddress]
        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        [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; }
    }
}

UserModel クラスは Index, Details, Delete 用の「ビューモデル」です。(「ビューモデル」は Controller から View にデータを渡すために使われます。詳しくは先の記事「ASP.NET MVC の Model」を見てください)

UserModel クラスに定義されているプロパティは、IdentityUser クラスを継承した MySQLIdentityUser カスタムクラスと同じです。(上に書いたプロジェクトの作成のステップ (1)、(2) で自動生成される Areas/Identity/Data フォルダの MySQLIdentityUser.cs ファイルに定義されています)

なので、MySQLIdentityUser カスタムクラスを「ビューモデル」として使うこともできます。しかし、(a) スキャフォールディング機能を使って View を生成できない(デザイナの「Razor ビューの追加」のダイアログで[モデルクラス(M):]の一覧に MySQLIdentityUser が現れない・・・理由不明)、(b) 表示名を自由に付けられない(プロパティ名と同じになる)・・・という理由で UserModel クラスを定義して使うことにしました。

そうすると、Controller で Linq To Entities を使って取得した MySQLIdentityUser オブジェクトを UserModel クラスに詰め替えなければならないという手間が増えますがやむを得ません。

詰め替えの手間を減らすため、先の記事「EDM にデータアノテーション属性を付与」に書いたようなメタデータクラスを作って対応することもトライしましたが、無駄な努力でした。

(2) Controller の作成

.NET Framework 版の ASP.NET MVC5 との大きな違いは、Core 版には組み込みの DI 機能があることでしょうか。今回のケースではコントローラーを初期化する時 UserManager<MySQLIdentityUser> オブジェクトを注入するようにしています。

コントローラーに UserManager<MySQLIdentityUser> オブジェクトへの参照を保持するフィールドを追加し、コンストラクタに UserManager<MySQLIdentityUser> オブジェクトへの参照を受け取る引数とそれをフィールドに代入するコードを書けば、あとは Areas/Identity フォルダに生成された IdentityHostingStartup.cs ファイル内の ConfigureServices メソッドで登録されたサービスが面倒を見てくれるようです。(詳しいメカニズムは調べ切れてませんが・・・)

ASP.NET Core Identity のデータベースに対し CRUD 操作を行うためのコントローラーのコードは以下の通りです。細かい注意点はコメントとして入れましたのでそちらを見てください。

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

// 追加
using Microsoft.AspNetCore.Identity;
using MySQLIdentity.Areas.Identity.Data;
using Microsoft.EntityFrameworkCore;
using MySQLIdentity.Models;

namespace MySQLIdentity.Controllers
{
    public class UserController : Controller
    {
        // Register.cshtml.cs のコードにならって以下のフィールドと
        // コンストラクタを追加。
        // 組み込みの DI 機能を利用して、コントローラーを初期化する際
        // UserManager<MySQLIdentityUser> オブジェクトを注入するため
        private readonly UserManager<MySQLIdentityUser> _userManager;

        public UserController(UserManager<MySQLIdentityUser> userManager)
        {
            _userManager = userManager;
        }


        // GET: User/Index
        // Model は UserModel
        public async Task<IActionResult> Index()
        {
            var users = from user in _userManager.Users
                        orderby user.UserName
                        select new UserModel
                        {
                            Id = user.Id,
                            UserName = user.UserName,
                            HandleName = user.HandleName,
                            Email = user.Email,
                            EmailConfirmed = user.EmailConfirmed,
                            PhoneNumber = user.PhoneNumber,
                            PhoneNumberConfirmed = user.PhoneNumberConfirmed,
                            TwoFactorEnabled = user.TwoFactorEnabled,
                            LockoutEnabled = user.LockoutEnabled,
                            LockoutEnd = user.LockoutEnd,
                            AccessFailedCount = user.AccessFailedCount
                        };

            return View(await users.ToListAsync());
        }

        // GET: User/Details/5
        // Model は UserModel
        // Core には HttpNotFound, HttpStatusCodeResult は無いので
        // 代わりに NotFound() を使う
        // 引数の id は string 型にすること
        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var user = await _userManager.FindByIdAsync(id);

            if (user == null)
            {
                return NotFound();
            }

            var model = new UserModel
            {
                Id = user.Id,
                UserName = user.UserName,
                HandleName = user.HandleName,
                Email = user.Email,
                EmailConfirmed = user.EmailConfirmed,
                PhoneNumber = user.PhoneNumber,
                PhoneNumberConfirmed = user.PhoneNumberConfirmed,
                TwoFactorEnabled = user.TwoFactorEnabled,
                LockoutEnabled = user.LockoutEnabled,
                LockoutEnd = user.LockoutEnd,
                AccessFailedCount = user.AccessFailedCount
            };

            return View(model);
        }

        // GET: User/Create
        // Model は Model/UserModel.cs の RegisterViewModel
        public IActionResult Create()
        {
            return View();
        }

        // POST: User/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
            [Bind("Email,Password,ConfirmPassword")] RegisterViewModel model)
        {
            // PasswordValidator による判定の結果は ModelState
            // には反映されないので注意(下記 result で判定)
            if (ModelState.IsValid)
            {
                // ユーザー入力のメールアドレスを UserName, Email,
                // HandleName プロパティに設定。さらに、ここで追加
                // したアカウントは Email Confirmation 無しで使用で
                // きるよう EmailConfirmed を true にする
                var user = new MySQLIdentityUser
                {
                    UserName = model.Email,
                    Email = model.Email,
                    HandleName = model.Email,
                    EmailConfirmed = true
                };

                // 上の user とユーザーが入力したパスワードで新規
                // ユーザーを作成・登録
                var result = await _userManager.
                    CreateAsync(user, model.Password);

                // ユーザー作成・登録の成否を result.Succeeded で
                // 判定。PasswordValidator の判定結果も result に
                // 反映される
                if (result.Succeeded)
                {
                    // 登録に成功したら User/Index にリダイレクト
                    return RedirectToAction("Index", "User");
                }
                // result.Succeeded が false の場合 ModelSate にエ
                // ラー情報を追加しないとエラーメッセージが出ない。
                // Register.cshtml.cs のものをコピー
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // ユーザー登録に失敗した場合、登録画面を再描画
            return View(model);
        }

        // GET: User/Edit/5
        // ここで更新できるのは Email とパスワードのみ。
        // UseName は POST する際 Email と同じに設定する。
        // Model は Models/UserModel.cs の EditViewModel
        // 引数の id は string 型にすること
        public async Task<IActionResult> Edit(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var target = await _userManager.FindByIdAsync(id);

            if (target == null)
            {
                return NotFound();
            }

            EditViewModel model = new EditViewModel()
            {
                Email = target.Email
            };

            return View(model);
        }

        // POST: User/Edit/5
        // UserName をソルトに使っていてパスワードだけもしくは
        // UserName だけを更新するのは NG かと思っていたが問題
        // なかった。(実際どのように対処しているかは不明)
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(string id, 
            [Bind("Email,Password,ConfirmPassword")] EditViewModel model)
        {
            if (id == null)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                var target = await _userManager.FindByIdAsync(id);
                if (target == null)
                {
                    return NotFound();
                }

                target.Email = model.Email;
                target.UserName = model.Email;

                // 新パスワードを入力した場合はパスワードも更新する
                if (!string.IsNullOrEmpty(model.Password))
                {
                    // MVC5 と違って PasswordValidator プロパティはない
                    // PasswordValidators で IList<IPasswordValidator<TUser>>
                    // を取得できる。PasswordValidators[0] で検証可能
                    // (ホントにそれで良いのかどうかは分からないが)
                    // ValidateAsync メソッドの引数は MVC5 と違うので注意
                    var resultPassword = await _userManager.PasswordValidators[0].
                        ValidateAsync(_userManager, target, model.Password);

                    if (resultPassword.Succeeded)
                    {
                        // 検証 OK の場合、入力パスワードをハッシュ。
                        // HashPassword メソッドの引数は MVC5 とは異なる
                        var hashedPassword = _userManager.PasswordHasher.
                            HashPassword(target, model.Password);
                        target.PasswordHash = hashedPassword;
                    }
                    else
                    {
                        // 検証 NG の場合 ModelSate にエラー情報を
                        // 追加して編集画面を再描画
                        // Register.cshtml.cs のものをコピー
                        foreach (var error in resultPassword.Errors)
                        {
                            ModelState.AddModelError(string.Empty, error.Description);
                        }
                        return View(model);
                    }
                }

                var resultUpdate = await _userManager.UpdateAsync(target);

                if (resultUpdate.Succeeded)
                {
                    // 更新に成功したら User/Index にリダイレクト
                    return RedirectToAction("Index", "User");
                }
                // Register.cshtml.cs のものをコピー
                foreach (var error in resultUpdate.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // 更新に失敗した場合、編集画面を再描画
            return View(model);
        }

        // GET: User/Delete/5
        // Model は UserModel
        // 階層更新が行われているようでロールがアサインされている
        // ユーザーも削除可
        public async Task<IActionResult> Delete(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var u = await _userManager.FindByIdAsync(id);

            if (u == null)
            {
                return NotFound();
            }

            var model = new UserModel
            {
                Id = u.Id,
                UserName = u.UserName,
                HandleName = u.HandleName,
                Email = u.Email,
                EmailConfirmed = u.EmailConfirmed,
                PhoneNumber = u.PhoneNumber,
                PhoneNumberConfirmed = u.PhoneNumberConfirmed,
                TwoFactorEnabled = u.TwoFactorEnabled,
                LockoutEnabled = u.LockoutEnabled,
                AccessFailedCount = u.AccessFailedCount
            };

            return View(model);
        }

        // POST: User/Delete/5
        // 上の Delete(string id) と同シグネチャのメソッドは
        // 定義できないので、メソッド名を変えて、下のように
        // ActionName("Delete") を設定する
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var target = await _userManager.FindByIdAsync(id);

            if (target == null)
            {
                return NotFound();
            }

            // ロールがアサインされているユーザーも以下の一行
            // で削除可能。内部で階層更新が行われているらしい。
            var result = await _userManager.DeleteAsync(target);

            if (result.Succeeded)
            {
                // 削除に成功したら User/Index にリダイレクト
                return RedirectToAction("Index", "User");
            }

            // Register.cshtml.cs のものをコピー
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }

            return View(target);
        }
    }
}

(3) View の作成

View は Controller のアクションメソッドを右クリックして[ビューの追加(D)...]からスキャフォールディング機能を使って生成します。

Razor ビューの追加

スキャフォールディング機能で自動生成された Index.cshtml のコードだけ以下に書いておきます。(生成後、一部手を加えなければならないところもありますので注意してください。この記事の例では ActionLink ヘルパーメソッドの第 3 引数を下のように修正しました)

@model IEnumerable<MySQLIdentity.Models.UserModel>

@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.UserName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.HandleName)
            </th>
           <th>
                @Html.DisplayNameFor(model => model.PhoneNumber)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.UserName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.HandleName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.PhoneNumber)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
                @Html.ActionLink("Details", "Details", new { id=item.Id }) |
                @Html.ActionLink("Delete", "Delete", new { id=item.Id })
            </td>
        </tr>
}
    </tbody>
</table>

この記事の一番上にある画像が Index.cshtml で描画されたものです。Details, Create, Edit, Delete 用の View もスキャフォールディング機能で生成できます。

Tags: , , ,

CORE

プロファイル情報の追加 (CORE)

by WebSurfer 2020年5月23日 14:09

ASP.NET Core 3.1 MVC アプリに ASP.NET Core Identity ベースのユーザー認証を実装して、プロファイル情報(カスタムユーザーデータ)としてハンドル名を追加し、ログイン時にページのヘッダ右上にデフォルトで表示される Email に代えてハンドル名を表示する方法を書きます。

ハンドル名の表示

プロファイル情報を追加するには Microsoft のドキュメント Add, download, and delete custom user data to Identity in an ASP.NET Core project の Run the Identity scaffolder セクションに書いてあるように、IdentityUser を継承した xxxxxUser クラス(xxxxx はデフォルトではプロジェクト名)を作ってそれを使う必要があります。

そのようなプロジェクトの作成手順は、先の記事「ASP.NET Identity で MySQL 利用 (CORE 版)」のステップ「(1) プロジェクトの作成」とステップ「(2) ASP.NET Core Identity の実装」を見てください。

以下の説明は Visual Studio 2019 でプロジェクト名を MySQLIdentity として作成した ASP.NET Core 3.1 MVC アプリをベースにしています。そのプロジェクトは、記事に書いたとおりデータベースは MySQL を使うように変更していますが、以下のプロファイル情報の追加手順は SQL Server でも同じです。

(1) HandleName プロパティの追加

プロジェクトの Areas/Identity/Data/MySQLIdentityUser.cs ファイルに IdentityUser を継承した MySQLIdentityUser クラスが定義されています(注: MySQLIdentity の部分はプロジェクト名になります。すなわちプロジェクトによって変わります)

プロジェクト作成時点では MySQLIdentityUser クラスの中身は空です。それに HandleName プロパティを追加します。以下のような感じです。

using Microsoft.AspNetCore.Identity;

// 追加
using System.ComponentModel.DataAnnotations;

namespace MySQLIdentity.Areas.Identity.Data
{
    public class MySQLIdentityUser : IdentityUser
    {
        // プロファイル情報としてハンドル名を追加
        [PersonalData]
        [Display(Name = "ハンドル名")]
        [Required(ErrorMessage = "{0}は必須です。")]
        [StringLength(128, ErrorMessage = "{0}は{1}文字以内で入力してください。")]
        public string HandleName { get; set; }
    }
}

そのあと Migration 操作を行うと Entity Framework Code First の機能によりデータベースには HandleName フィールドが追加されます。HandleName フィールドはデータアノテーション属性の Required により NULL 不可に、StringLength の引数 128 により varchar(128) になります。

後で気が付いたのですが、表示名や検証のエラーメッセージは Razor ページで別に定義したモデルに付与した属性のものを使うので、ここで上のように DisplayName 属性やエラーメッセージを付与しても意味はなさそうです。しかし、害もなさそうなのでそのままにしています。

PersonalData 属性は、参考にした Microsoft のドキュメントによると "it's automatically available for download and deletion. Making the data able to be downloaded and deleted helps meet GDPR requirements." とのことです。

その "downloaded" というのは Manage ページでユーザー情報の JSON 文字列を PersonalData.json という名前のファイルにしてダウンロードする機能で、その中にハンドル名のデータが含まれるようになります。PersonalData 属性を付与しないとプロファイル情報は含まれないのかは未検証です。

"deleted" はユーザー情報がすべてデータベースから削除されるという動きになります。PersonalData 属性を付与しようがしまいが全削除には変わりはないと思われるのですが・・・

(2) Add-Migration

Visual Studio のパッケージマネージャーコンソールで Add-Migration CustomUserData コマンドを実行します。CustomUserData という名前は任意です。

成功すると Migrations フォルダに xxxxx_CustomUserData.cs という名前(xxxxx は作成日時)のクラスファイルが生成され、それに以下のようなコードが含まれているはずです。

using Microsoft.EntityFrameworkCore.Migrations;

namespace MySQLIdentity.Migrations
{
    public partial class CustomUserData : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<string>(
                name: "HandleName",
                table: "AspNetUsers",
                maxLength: 128,
                nullable: false,
                defaultValue: "");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropColumn(
                name: "HandleName",
                table: "AspNetUsers");
        }
    }
}

上のコードは、既にデータベースには ASP.NET Core Identity 用のテーブルはすべて生成済みという状態にプロファイル情報 HandleName を追加した場合です。プロジェクト作成直後のデータベースには何もない状態で Add-Migration を実行した場合に生成されるコートとは異なります。

(3) Update-Database

Visual Studio のパッケージマネージャーコンソールで Update-Database コマンドを実行します。成功すると ASP.NET Core Identity 用のデータベースの aspnetusers テーブルに HandleName フィールドが追加されます。

aspnetusers テーブル

この記事ではデータベースは SQL Server ではなく MySQL を利用しています。nvarchar でなく varchar となっているのは MySQL の事情です。

MySQL のドキュメント 10.3.7 The National Character Set によると、"MySQL uses utf8 as this predefined character set" なのでどちらでも同じなのだそうです。ちなみに create 句で nvarchar と指定しても、生成されるのは varchar になります。

(4) Register ページの修正

Areas/Identity/Pages/Account/Reguster.cshtml.cs の OnPostAsync メソッドに、以下の画像の赤枠のコードを追加します。登録時にはハンドル名は自動的に Email と同じになるようにするものです。

Register ページの修正

上のステップ (1) で HandleName プロパティには Rquired 属性を付与しましたので、aspnetusers テーブルの HandleName フィールドは NULL 不可になっています。ステップ (3) の画像を見てください。

そのため、新規ユーザーの登録を行う際 HandleName を入力しないと CreateAsync でエラーとなります。と言って、登録時の手間を増やすとユーザーに面倒がられると思われます。

なので、登録時には自動的に HandleName は Email と同じになるようにし、後で変更できるようにしました。

(5) ハンドル名の変更機能を実装

ログイン後、ページのヘッダの右上のユーザー名(デフォルトで Email)をクリックすると Manage ページに遷移しますが、そこでユーザーがハンドル名を任意に変更できるようにします。

まず Areas/Identity/Pages/Account/Manage/Index.cshtml.cs の InputModel クラスへのハンドル名用のプロパティの追加と、LoadAsync ヘルパーメソッドでのハンドル名の InputModel への設定のためのコードを追加します。入力できるハンドル名の長さはとりあえず 30 文字に制限してみました(当該 input 要素に maxlength="30" が設定されます)。

InputModel クラスの処理

同じく Index.cshtml.cs のコードの OnPostAsync メソッドにハンドル名の変更をデータベースに反映するコードを追加します。

ハンドル名の変更処理

次に Areas/Identity/Pages/Account/Manage/Index.cshtml にハンドル名入力用のテキストボックスを追加します。

ハンドル名用テキストボックス追加

(6) ハンドル名の変更操作

ユーザーがログインするとページのヘッダの右上に Hello xxxxx! と表示され、それが Manage ページへのリンクになります。それをクリックするとプロファイル情報の編集画面に遷移します。

上のステップ (5) で加えたコードの修正により、画面にはハンドル名入力用のテキストボックスが表示されています。

ハンドル名の変更

初期画面では[ハンドル名]テキストボックスには[Username]テキストボックスと同じメールアドレスが表示されます。それを任意の名前に設定し[Save]ボタンをクリックするとハンドル名は変更されます。

上の画像は初期画面のメールアドレスを WebSurfer というハンドル名に変更した後のものです。

(7) ClaimsIdentity へ追加

ページのヘッダの右上に Hello xxxxx! と表示される xxxxx にはデフォルトではメールアドレスが表示されていますが(上のステップ (6) の画像参照)、それをハンドル名に変更します。結果、この記事の一番上の画像のようになります。

プロファイル情報を Claim として ClaimsIdentity オブジェクトに追加し、拡張メソッドを使って ClaimsIdentity オブジェクトからハンドル名を取得・表示するようにしています。

ClaimsIdentity オブジェクトにハンドル名を追加すると、認証クッキーにハンドル名が含まれるようになります。認証クッキーからハンドル名を取得できれば、毎回データベースにクエリを投げて取得するより負荷は軽い(であろう)というのが ClaimsIdentity を使う理由です。

プロファイル情報を追加・取得するための詳しい手順は、先の記事「ASP.NET Core MVC の ClaimsIdentity」に書きましたので見てください。

注意すべき変更点は、(1) その記事は PhoneNumber を使っていますが、それを HandleName に変更、(2) IdentityUser を MySQLIdentity.Areas.Identity.Data 名前空間の MySQLIdentityUser に変更、(3) ClaimTypes の HomePhone を GivenName に変更(GivenName でなくても良いですが Name はすでに使われているようで NG)・・・です。

Tags: , ,

CORE

Password Recovery (CORE)

by WebSurfer 2020年5月19日 12:59

Password Recovery はユーザーがパスワードを忘れてしまっても復活できる手段を提供するものです。ASP.NET Core 3.1 の Password Recovery がデフォルトの実装ではどのような動きになるかを以下に説明します。

(先の記事「Email Confirmation の実装 (CORE 版)」の「(9) Password Recovery」の補足です)

ページのヘッダ右上に表示されている [Login] をクリックして Login ページを表示すると、その中に [Forgot your password?] というリンクがあります。それをクリックすると下の画像の ForgotPassword ページに遷移します。

ForgotPassword ページ

その Email 欄にメールアドレスを入力して [Submit] をクリックすると、ForgotPassword.cshtml.cs の OnPostAsync メソッドで (a) 送信されたメールアドレスが登録済み、(b)Email Confirmation 済みであることを確認します。

条件 (a), (b) が確認できない場合は単純に ForgotPasswordConfirmation ページにリダイレクトされるだけでそれ以外何も起こりません。

条件 (a), (b) の条件の両方が確認できるとメールが送信され、その後で ForgotPasswordConfirmation ページにリダイレクトされます。以下のような画面になります。ユーザーにメールを見るよう促しているだけです。

ForgotPasswordConfirmation ページ

送信されたメールの本文には ResetPassword ページに確認用のトークンをクエリ文字列に含めた URL が含まれます。

Reset Password メール

上の画像のメールは、ユーザーのメーラーが html の表示を許可していない可能性を考えて URL そのものを送信するようにしています。(自動生成されるコードでは、URL を a 要素の href 属性に設定したハイパーリンク "clicking here" になります)

具体的には、Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs の OnPostAsynce() メソッドの中の SendEmailAsync メソッドのコードを以下のようにしています。

await _emailSender.SendEmailAsync(
    Input.Email,
    "Reset Password",
    "Please confirm your account by: " + 
        HtmlEncoder.Default.Encode(callbackUrl));

メールの URL をクリックすると ResetPassword ページが GET 要求され、クエリ文字列から確認用のトークンが渡されます。その応答としてブラウザに以下のような画面が表示されます。

ResetPassword ページ

上のページに、最初に ForgotPassword ページで申請したのと同じメールアドレスと、新しいパスワードを入力して [Submit] をクリックすると、メールアドレスとトークンが有効であればパスワードが新しいものに変更され、その後 ResetPasswordConfirmation ページにリダイレクトされます。

ResetPasswordConfirmation ページ

そのページのリンク [click here to log in] をクリックすると Login ページに遷移するので、そこでメールアドレスと新しいパスワードを入力すればログインできます。


ユーザー登録の場合と違って、Password Recovery ではメールが受信でき、それに含まれるトークンを ResetPassword ページに送信し、そのページで新しいパスワードを登録できないと何ともならない・・・すなわちメール送信機能の実装は必須のコーディングとなっています。

メール送信機能を実装しなくても Password Recovery を可能にするには、コードを書き換えて、 ForgotPasswordConfirmation ページで上に書いた条件 (a), (b) の条件の両方が確認できたら、ResetPassword ページにリダイレクトするといった修正が必要と思われます。(未検証・未確認です)

Tags: , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar