WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

プロファイル情報の追加 (.NET 版)

by WebSurfer 26. May 2020 16:23

.NET Framework 版の ASP.NET MVC5 アプリに ASP.NET Identity を利用したユーザー認証を実装して、プロファイル情報としてハンドル名を追加し、ログイン時にページのヘッダ右上にハンドル名(デフォルトはメールアドレス)を表示する方法を書きます。

ハンドル名を表示

先の記事「プロファイル情報の追加 (CORE 版)」の .NET Framework 版です。CORE 版とは基本的なところでの大きな差はありませんが、細かいところでいろいろ違うので備忘録として残しておくことにしました。

(1) プロジェクトの作成

以下の説明は、Visual Studio 2019 を使って Mvc5ProfileInfo という名前で作成した ASP.NET MVC5 アプリのプロジェクトをベースしています。

プロジェクトの作成

デフォルトでは認証は「認証なし」になっていますが、それを「個別のユーザーアカウント」に変更します。それによって ASP.NET Identity を利用したユーザー認証のためのソースコードが一式自動生成されます。

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

Models/IdentityModels.cs ファイルに IdentityUser を継承した ApplicationUser クラスが定義されています。それに HandleName プロパティを追加します。以下の画像の赤枠部分がそれです。

HandleName プロパティの追加

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

(3) Enable-Migrations / Add-Migration

Visual Studio のパッケージマネージャーコンソールで Enable-Migrations コマンドを実行します。成功するとプロジェクトのルート直下に Migrations フォルダが生成され、その中に Configuration.cs という名前のクラスファイルが生成されます。

次に、Add-Migration Initial コマンドを実行します(Initial という名前は任意です)。成功すると Migrations フォルダに xxxxx_Initial.cs という名前のクラスファイルが生成されます。(xxxxx は作成日時)

Initial.cs

その中に、dbo.AspNetUsers テーブルを SQL Server に生成するコードがあります。上の画像の赤枠のコードを見てください。ステップ (2) で HandleName プロパティに付与した属性に従って HandleName というフィールドを NULL 不可、nvarchar(256) で生成するようになっています。

(注: この記事ではプロジェクト作成直後のデータベースには何もない状態から Migration 関係の操作を行っています。データーベースがすでに生成済みで、それに HandleName を追加する場合とは手順が違うので注意してください)

(4) Update-Database

Visual Studio のパッケージマネージャーコンソールで Update-Database コマンドを実行します。成功すると ASP.NET Identity 用のデータベースが Entity Framework Code First の機能によって追加されます。

ASP.NET Identity 用のデータベース

この記事では Visual Studio でプロジェクトを作成したときに自動生成された web.config をそのまま使っています。その中に "DefaultConnection" という名前で LocalDB に接続する接続文字列が定義されており、aspnet-Mvc5ProfileInfo-20200526103410.mdf という名前(数字はプロジェクトの作成日時)の .mdf ファイルを動的に LocalDB にアタッチして使うように設定されています。

Models/IdentityModels.cs ファイルには IdentityDbContext<ApplicationUser> を継承したコンテキストクラス ApplicationDbContext が定義されており、web.config の接続文字列 "DefaultConnection" を使うように設定されています。

Update-Database コマンドによって、web.config の接続文字列で LocalDB に接続し、指定された名前のデータベースを LocalDB に作成し、ステップ (3) で生成されたクラスファイル xxxxx_Initial.cs に従って ASP.NET Identity 用のテーブルを生成します。

その結果が上の画像です。aspnet-Mvc5ProfileInfo-20200526103410 という名前のデータベースが作成され、その中の dbo.AspNetUsers テーブルにステップ (2) で HandleName プロパティを追加したとおり、HandleName フィールドが NULL 不可 nvarchar(256) で生成されています。

(5) Register アクションメソッドの修正

Controllers/AccountController.cs の Register アクションメソッドに以下の画像の赤枠のコードを追加します。登録時にハンドル名を自動的に Email と同じになるようにするものです。

Register アクションメソッドの修正

ステップ (4) の画像の通り dbo.AspNetUsers テーブルの HandleName フィールドは NULL 不可になっています。そのため、新規ユーザーの登録を行う際 HandleName を入力しないと CreateAsync でエラーとなります。なので、初回登録時はとりあえずハンドル名は Email と同じになるようにしました。

登録時にユーザーにハンドル名を決めて入力してもらうようにもできますが、そういう手間を増やすと嫌がられそうですし、ユーザーによってはハンドル名は Email と同じでよいという人もいるでしょうから。

ここまで実装できれば、アプリケーションを実行してユーザー登録が可能になり、登録操作を行うと dbo.AspNetUsers テーブルの HandleName フィールドにはメールアドレスと同じ文字列が設定されているはずです。

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

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

ハンドル名の変更は、Manage コントローラーに ChangeHandleName という名前のアクションメソッドを追加しそこで行うようにします。

まず、Manage/Index ページのビュー Views/Manage/Index.cshtml に ChangeHandleName アクションメソッドへのリンクを追加します。

ChangeHandleName へのリンク追加

変更が完了した際に Manage/Index ページに表示するためのメッセージを追加します。Controllers/ManageController.cs ファイルを開いて以下の画像の赤枠のコードを追加します(2 箇所)。

メッセージを追加

メッセージを追加

Models/ManageViewModels.cs に ChangeHandleName アクションメソッドとビューの間でデータをやり取りするための ChangeHandleNameViewModel クラスを追加します。入力できるハンドル名の長さはとりあえず 30 文字に制限してみました。

// HandleName 変更用に追加
public class ChangeHandleNameViewModel
{
    [Required]
    [Display(Name = "旧ハンドル名")]
    public string OldHandleName { get; set; }

    [Required(ErrorMessage = "{0}は必須")]
    [StringLength(30, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "新ハンドル名")]
    public string NewHandleName { get; set; }
}

Views/Manage/Index.cshtml に ChangeHandleName アクションメソッドのコードを追加します。旧ハンドル名を入力できるようになっていたり、いきなり例外をスローするところがちょっと乱暴かもしれませんが、検証用ということで・・・

// GET: /Manage/ChangeHandleName
public async Task<ActionResult> ChangeHandleName()
{
    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        throw new InvalidOperationException(
            "ログイン中のユーザー情報が削除されました。");
    }

    var model = new ChangeHandleNameViewModel();
    model.OldHandleName = user.HandleName;
    return View(model);
}

// POST: /Manage/ChangeHandleName
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ChangeHandleName(ChangeHandleNameViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        throw new InvalidOperationException(
            "ログイン中のユーザー情報が削除されました。");
    }

    if (model.OldHandleName != user.HandleName)
    {
        throw new InvalidOperationException(
            "DB に既存のハンドル名と旧ハンドル名が一致しません。");
    }

    user.HandleName = model.NewHandleName;
    var result = await UserManager.UpdateAsync(user);
    if (result.Succeeded)
    {
        return RedirectToAction("Index", 
            new { Message = ManageMessageId.ChangeHandleNameSuccess });
    }
    AddErrors(result);
    return View(model);
}

アクションメソッドを追加したら、スキャフォールディングのデザイナで[テンプレート(T)]を Edit とし[モデルクラス(M)]を上に定義した ChangeHandleNameViewModel としてビューを追加します。コードは自動生成されたものがほぼそのまま使えますのでこの記事に書くのは割愛します。

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

ユーザーがログインするとページのヘッダの右上に Hello xxxxx! と表示され、それが Manage/Index ページへのリンクになります。それをクリックして Manage/Index ページを表示し、その中のリンク[ハンドル名の変更]をクリックするとすると Manage/ChangeHandleName ページに遷移します。

ChangeHandleName ページ

上の[新ハンドル名]テキストボックスに新しいハンドル名を入力し[Save]ボタンをクリックするとデータベースの HandleName フィールドが更新され、その後 Manage/Index ページにリダイレクトされます。

Manage/Index ページにリダイレクト

赤枠で示した部分にステップ (6) で追加したメッセージが表示されているところに注目してください。

(8) ClaimsIdentity へ追加

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

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

プロジェクト作成時に自動生成された Models/IdentityModels.cs ファイルの ApplicationUser クラスにそのコードを書く場所がコメントで「ここにカスタム ユーザー クレームを追加します」と示されているので、そこに追加します。以下の画像の上の赤枠部分の通りです。

カスタムユーザークレーム

ClaimsIdentity オブジェクトからハンドル名を取得する拡張メソッドもそこに書いておきます。画像の下の赤枠のコードがそれです。

画像には表示されていませんが、using 句で名前空間 System.Linq と System.Security.Principal を取り込むようにしてください。

拡張メソッドは名前空間をインポートすればスコープの中に取り込むことができます。この記事の一番上の画像のようにレイアウトページの右上に表示する場合は Views/Shared/_LoginPartial.schtml に名前空間 Mvc5ProfileInfo.Models を取り込んで User.Identity.GetHandleName() というコードで取得します。

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

Tags: , , ,

MVC

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

by WebSurfer 23. May 2020 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

BOM 無し UTF-8 と StreamReader

by WebSurfer 20. May 2020 14:36

Windows 10 のあるバージョンから、メモ帳でテキストを保存する際の文字コードのデフォルトが ANSI から UTF-8 に変わりました。下の画像を見てください。

メモ帳の文字コード

上の画像で、[ANSI] は「BOM なし Shift_JIS」、[UTF-8] は「BOM なし UTF-8」となります。それ以外はすべて BOM つきとなります。

その変更で気になっていたのは、以前のメモ帳のデフォルト [ANSI] 即ち「BOM なし Shift_JIS」を期待して StreamReader コンストラクタの Encoding を Shift_JIS に指定した以下のようなコードを使っている場合、新しいメモ帳のデフォルト「BOM なし UTF-8」で保存されたファイルを読むとどうなるかということです。

using (StreamReader reader = new StreamReader(
                             dir + "UTF8withoutBOM.txt", 
                             Encoding.GetEncoding("Shift_JIS")))
{
    Console.WriteLine(reader.ReadToEnd());
}

ダメだろうとは思いながらも、念のため試してみましたがやっぱり当たり前にダメでした。下の画像の一行目がそれで、文字化けしています。

結果

上の画像の二行目以降は、順に、メモ帳で保存するとき [UTF-8 (BOM つき)]、[ANSI]、[UTF-16 BE]、[UTF-16 LE] を選んだものです。

[UTF-8] は「BOM なし UTF-8」で、BOM が付いてないので StreamReader コンストラクタの第 2 引数に指定された Shift_JIS としてデコードされ、その結果文字化けしています。

[ANSI] は「BOM なし Shift_JIS」で、これも BOM なしですが、StreamReader コンストラクタの第 2 引数で正しく指定されている通り Shift_JIS としてデコードするので問題なしです。

[UTF-8] と [ANSI] 以外は有効な BOM が付いており、StreamReader は BOM で正しく文字コードが判定できますので、判定した通りデコードされて問題なしという結果になっています。

StreamReader コンストラクターの詳しい説明は左のリンクをクリックして Microsoft のドキュメントを見てください。

少し説明しておくと、StreamReader コンストラクタ (String, Encoding) のように Encoding を引数に指定するオーバーロードでは、ファイルの最初の 4 バイトを見てエンコーディングの検出を試み、適切な BOM で始まる場合、それによって UTF-8、リトルエンディアン Unicode、ビッグエンディアン Unicode、リトルエンディアン UTF-32、およびビッグエンディアン UTF-32 を自動的に判別し、それ以外の場合は Encoding に指定されたエンコードが使用されるそうです。

ちなみに BOM は 16 進数表現で以下の通りとなります。

  • UTF-8:    EF BB BF
  • UTF-16BE: FE FF
  • UTF-16LE: FF FE
  • UTF-32BE: 00 00 FE FF
  • UTF-32LE: FF FE 00 00

メモ帳のデフォルトが「BOM なし Shift_JIS」だった時代は、メモ帳で作るテキストファイルを読む場合、StreamReader コンストラクタの Encoding に Shift_JIS 指定を指定しておけば文字化けに悩まずに済みました。デフォルトが「BOM なし UTF-8」に変わった今はそうはいかないようです。

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  September 2020  >>
MoTuWeThFrSaSu
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar