ASP.NET Core 3.1 Identity で、プロファイル情報を Claim として ClaimsIdentity オブジェクトに追加し、拡張メソッドを使って ClaimsIdentity オブジェクトからプロファイル情報を取得・表示する方法を書きます。
.NET Framework ベースの Identity の場合は先の記事「プロファイル情報を ClaimsIdentity へ追加」に書きました。以下は、それと同様な仕組みを ASP.NET Core 3.1 MVC で実装する方法です。
Core 版 Identity にも、.NET Framework 版と同様に、プロファイル情報の一つとして PhoneNumber が IdentityUser クラスに定義済みです。(なので Entity Framework Code First で自動生成される DB のテーブルには PhoneNumber フィールドが含まれます)
先の記事と同様に、ユーザーがログインした際 PhoneNumber を認証クッキーに含めてブラウザに送信し、次の要求を受けた時に認証クッキーから PhoneNumber を取得して上の画像のようにページの右上に表示するコードを実装してみました。
それに何のメリットがあるかと言うと、認証クッキーからプロファイル情報を取得する方が、いちいちデータベースにクエリを投げて取得するより負荷は軽い(であろう)ということです。詳しくは先の記事を見てください。
問題は Visual Studio のテンプレートで自動生成されるコードのどこに Claim を ClaimsIdentity に追加するためのコードを書くかということです。
.NET Framework 版 MVC5 であれば、自動生成される Models/IdentityModels.cs に定義されている ApplicationUser クラスの GenereteUserIdentityAsync メソッドの中に「// ここにカスタム ユーザー クレームを追加します」とコメントが入っていてすぐわかるのですが・・・
という訳で、ネットで asp.net core add custom claim をキーワードにググって調べて、ヒットした以下の記事を参考にさせていただきました:
以下に、定義済みのプロファイル情報 PhoneNumber を Claim として ClaimsIdentity へ追加するコード、ClaimsIdentity からプロファイル情報を取得するための拡張メソッドのコードを載せておきます。上の 2 つの記事のどちらの方法でも OK ですが、下のサンプルコードでは前者の記事の方法を取っています。
ただし、Role を使っている場合は要注意で、上に紹介した記事にある UserClaimsPrincipalFactory<TUser> クラスを使うと Role が働かなくなります(例えばアクションメソッドに [Authorize(Roles ="Administrator")] を付与とすると Administrator ロールを持っているユーザーでもアクセス拒否されます)。
Role が使われている場合は UserClaimsPrincipalFactory<TUser> クラスに代えて、下のサンプルコードのように UserClaimsPrincipalFactory<TUser,TRole> クラスを使ってください。
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Security.Principal;
namespace MvcCoreIdentity.Services
{
// Role を使う場合 UserClaimsPrincipalFactory<TUser> では
// なく UserClaimsPrincipalFactory<TUser,TRole> を継承
public class CustomClaimsPrincipalFactory :
UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
{
public CustomClaimsPrincipalFactory(
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{
}
// ログイン操作でこのメソッドが呼び出される
public async override Task<ClaimsPrincipal> CreateAsync(
IdentityUser user)
{
var principal = await base.CreateAsync(user);
// ここでは例として PhoneNumber を Claim として追加。
// 未登録(DB 上で NULL)の場合 this.PhoneNumber プロ
// パティは null を返す。null の場合は追加しても意味
// がないので追加しない
if (!string.IsNullOrEmpty(user.PhoneNumber))
{
((ClaimsIdentity)principal.Identity).AddClaims(
new[] {
// 下の「注1」を参照ください
new Claim(ClaimTypes.HomePhone, user.PhoneNumber)
});
}
return principal;
}
}
// 下の「注2」を参照ください
// ClaimsIdentity から PhoneNumber を取得する拡張メソッド
// PhoneNumber が Claims にない場合は null を返す。
public static class MyExtensions
{
public static string GetPhoneNumber(this
IIdentity identity)
{
var claimsIdentity = identity as ClaimsIdentity;
if (claimsIdentity != null)
{
var claim = claimsIdentity.Claims.
FirstOrDefault(c =>
c.Type == ClaimTypes.HomePhone);
if (claim != null)
{
return claim.Value;
}
}
return null;
}
}
}
上のコードの CustomClaimsPrincipalFactory メソッドが動くようにするには Startup.cs の ConfigureServices メソッドで以下のように設定する必要がありますので忘れないようにしてください。
public void ConfigureServices(IServiceCollection services)
{
// ・・・中略・・・
// 以下のコードを追加する
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>,
CustomClaimsPrincipalFactory>();
services.AddControllersWithViews();
services.AddRazorPages();
}
上の拡張メソッド GetPhoneNumber は名前空間をインポートすればスコープの中に取り込むことができます。例えば、上の画像のようにマスターページの右上に表示する場合は Views/Shared/_LoginPartial.cshtml に以下のように名前空間をインポートし @User.Identity.GetPhoneNumber() というコードを追加します。
@using Microsoft.AspNetCore.Identity
@using MvcCoreIdentity.Services
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a id="manage" class="nav-link text-dark"
asp-area="Identity"
asp-page="/Account/Manage/Index"
title="Manage">
Hello @UserManager.GetUserName(User) /
Phone @User.Identity.GetPhoneNumber() !
</a>
</li>
・・・以下略・・・
注1: Claim(String, String) コンストラクタの第一引数に ClaimTypes クラスのメンバーを使っていますが、Name, Email, NameIdentifier, Role フィールドは使用済みなので重複しないよう注意してください。必ずしも ClaimTypes クラスのメンバーを使う必要はなく、例えば "Phone" など任意の文字列でも OK です。
注2: 後で気が付いたのですがこの記事の例では拡張メソッドは必要なかったです��ControllerBase, RazorPageBase, PageModel クラスの User プロパティで取得できる ClaimsPrincipal クラスには Claim を取得するための FindFirst(String) メソッドがあり、上の _LoginPartial.cshtml のコードの場合ですと拡張メソッドに代えて以下のようにできます。
// 拡張メソッド利用
@User.Identity.GetPhoneNumber()
↓↓↓
// ClaimsPrincipal.FindFirst メソッド利用
@User.FindFirst(ClaimTypes.HomePhone)?.Value