WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core MVC の ClaimsIdentity

by WebSurfer 4. February 2020 13:56

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[] { 
            new Claim(ClaimTypes.HomePhone, user.PhoneNumber) 
          });
      }

      return principal;
    }
  }

  // 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>
・・・以下略・・・

Tags: , , ,

CORE

プロファイル情報を ClaimsIdentity へ追加

by WebSurfer 16. April 2017 14:13

ASP.NET Identity 2.0 でプロファイル情報を Claim として ClaimsIdentity オブジェクトに追加し、拡張メソッドを使って ClaimsIdentity オブジェクトからプロファイル情報を取得・表示する方法を書きます。

プロファイル情報の表示

プロファイル情報とは、ユーザーのメールアドレス、電話番号などのユーザー固有の情報です。ASP.NET プロファイル機能は、フォーム認証のためのクレデンシャル情報(ユーザー名とパスワード)と共に、プロファイル情報を個々のユーザーに関連付けてデータベースに格納します。

ASP.NET Identity 2.0 では、プロファイル情報として Email, PhoneNumber が IdentityUser クラスに定義済みです。それらに加えて、CodeZine の記事 ASP.NET Identityのプロファイル情報のカスタマイズにあるように、姓、名、誕生日などの任意の情報を追加することができます。

プロファイル情報の標準的な設定方法や取得方法は CodeZine の記事を読んでいただければわかるのでここでは書きません。(手抜きでスミマセン。上にリンクを張った CodeZine の記事は ASP.NET Identity 1.0 のもので、2.0 のものとは AspNetUsers テーブルの内容などが異なりますが、基本は同じです)

ここでは、CodeZine の記事のようにその都度データベースから情報を取得するのではなく、User.Identity プロパティから取得できる ClaimsIdentity オブジェクトにプロファイル情報を含めておき、それから取得する方法を書きます。

そのような方法を取る理由は、例えば上の画像のようにマスターページの右上に常にユーザー情報を表示するような場合、ページを描画するたびにデータベースにクエリを発行してプロファイル情報を取得するのは負荷が重そうに感じたからです。

ClaimsIdentity オブジェクトにプロファイル情報を含めれば、ユーザー認証後は認証クッキーに含まれたプロファイル情報がクライアントから送信されてきて、それをベースに ClaimsIdentity オブジェクトを再生成するのだと思います。(それが書いてある Microsoft の公式文書が見つからないので想像の域を出ませんが、実際にいろいろ試した結果からその想像は合っていると思います)

であれば、再生成された ClaimsIdentity オブジェクトからプロファイル情報を取得する方が、データベースから取得するより、負荷は軽そうです。(実は気にするほどの差はないのかもしれませんが)

以下に、例として、PhoneNumber という定義済みのプロファイル情報を Claim として ClaimsIdentity へ追加するコード、ClaimsIdentity からプロファイル情報を取得するための拡張メソッドのコードを載せておきます。

ベースは ASP.NET Web Forms の Web アプリケーションプロジェクトを Visual Studoi 2015 Community のテンプレートを使って自動生成した IdentityModel.cs です。それに Claim を追加するコードと拡張メソッドを追加しています。(Web サイトプロジェクトでは、自動生成される IdentityModel.cs がかなり異なり、同じようにできるかどうかは未確認です。)

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using WebFormsApp.Models;

// 拡張メソッドで FirstOrDefault を使うため追加
using System.Linq;

namespace WebFormsApp.Models
{
  public class ApplicationUser : IdentityUser
  {
    public ClaimsIdentity GenerateUserIdentity(
            ApplicationUserManager manager)
    {
      var userIdentity = manager.CreateIdentity(
        this, DefaultAuthenticationTypes.ApplicationCookie);

      // ここでは例として PhoneNumber を Claim として追加。
      // 未登録(DB 上で NULL)の場合 this.PhoneNumber プロ
      // パティは null を返す。null の場合は追加しても意味
      // がないので追加しない。 (null のまま追加しようとす
      // ると Claim コンストラクタで例外がスローされる)
      if (!string.IsNullOrEmpty(this.PhoneNumber))
      {
        // ClaimTypes クラスは System.Security.Claims 名前
        // 空間に定義済みなのでそれを利用。PhoneNumber プロ
        // パティは IdentityUser クラスに定義済み。
        userIdentity.AddClaim(
          new Claim(ClaimTypes.HomePhone, this.PhoneNumber));
      }

      return userIdentity;
    }
    // ・・・中略・・・
  }

  // ClaimsIdentity から PhoneNumber を取得する拡張メソッド
  // PhoneNumber が Claims にない場合は null を返す。
  public static class MyExtensions
  {
    public static string GetPhoneNumber(
        this System.Security.Principal.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;
    }
  }
  // ・・・中略・・・
}

MVC5 アプリでは、テンプレートで自動生成される IdentityModel.cs のコードが上の Web Forms アプリのものとは少々異なりますが、自力で書いて追加する部分のコードは上記と全く同じになります。

上記の拡張メソッドは名前空間をインポートすればスコープの中に取り込むことができます。例えば、上の画像のようにマスターページの右上に表示する場合は以下のようにします。

<%@ Import Namespace="WebFormsApp.Models" %>

<a runat="server" href="~/Account/Manage" 
  title="Manage your account">
  Hello, <%: Context.User.Identity.GetUserName()  %> !
  Phone: <%: Context.User.Identity.GetPhoneNumber() %>
</a>

Tags: ,

ASP.NET

Web アプリケーションプロジェクトと Profile

by WebSurfer 4. December 2011 16:34

Web アプリケーションプロジェクトで新規ユーザー登録をする際に、ユーザーの漢字の氏名、住所などのユーザープロファイル情報を取得し、ASP.NET プロファイルを使用して保存する方法の紹介です。

プロファイル情報の取得

先の記事 ユーザー登録時のプロファイル情報取得 では、Web サイトプロジェクトを利用しました。その場合は、どのページでも Profile プロパティでプロファイル・データにアクセスできるようになります。

例えば、以下のように、String 型の KanjiName を profile の properties の add 要素 に定義したとします。

<profile>
  <properties>
    <add name="KanjiName" />
  </properties>
</profile>

すると、ProfileBase クラス を継承する ProfileCommon クラスが自動的に作成され、profile の properties の add 要素 に定義されているそれぞれのプロパティの get/set アクセサが ProfileCommon クラスに追加されます。以下のような感じになると思います。(実際に自動生成されたコードを見たわけではないので想像ですが)

public class ProfileCommon : ProfileBase
{
  public virtual string KanjiName 
  {
    get
    {
      return (String) HttpContext.Current.Profile.
                        GetPropertyValue("KanjiName");
    }

    set
    {
      HttpContext.Current.Profile.
        SetPropertyValue("KanjiName", value);
    }
  }
}

さらに、各ページに、ProfileCommon クラスのインスタンスを取得するため、以下のような Profile プロパティが自動的に追加されます。

protected ProfileCommon Profile
{
  get
  {
    return (ProfileCommon)this.Context.Profile;
  }
}

それゆえ、Web サイトプロジェクトでは、コードは一行も書かなくても、どのページでも Profile.KanjiName プロパティを使ってプロファイル・データにアクセスできるようになります。

しかしながら、Web アプリケーションプロジェクトではそうはいきません。(Web アプリケーションプロジェクトと Web サイトプロジェクトの違いについては MSDN ライブラリ を参照してください)

何故なら ProfileCommon クラスや Profile プロパティは自動的に定義されないからです。

そのあたりのことは、MSDN ライブラリの チュートリアル : Visual Studio の Web サイト プロジェクトから Web アプリケーション プロジェクトへの変換 の「プロファイル オブジェクト コードの変換」のセクションに説明があります。

KanjiName の場合を例に取ると、以下のように手動で ProfileCommon を定義する必要があります。この場合、ProfileCommon は ProfileBase を継承する必要はありません。

public class ProfileCommon
{
  public virtual string KanjiName 
  {
    get
    {
      return (String) HttpContext.Current.Profile.
                        GetPropertyValue("KanjiName");
    }

    set
    {
      HttpContext.Current.Profile.
        SetPropertyValue("KanjiName", value);
    }
  }
}

次に、ProfileCommon クラスのインスタンスの 1 つである Profile を、次のコード例のように、プロファイル システムを使用する必要があるページに追加します。

public partial class _Default : System.Web.UI.Page
{
  ProfileCommon Profile = new ProfileCommon();

  protected void Page_Load(object sender, EventArgs e)
  {
    string kanjiName = Profile.KanjiName;
    if (String.IsNullOrEmpty(kanjiName))
    {
      Label1.Text = "漢字名は設定されていません。";
    }
    else
    {
      Label1.Text = Server.HtmlEncode(kanjiName);
    }            
  }

  protected void Button1_Click(object sender, EventArgs e)
  {
    if (User.Identity.IsAuthenticated)
    {
      Profile.KanjiName = TextBox1.Text;
      Label1.Text = Server.HtmlEncode(Profile.KanjiName);
    }
    else
    {
      Label2.Text = "ログインしていないと設定できません。";
    }
  }
}

なお、web.config に、データベースとのプロファイル・データのやり取りのためプロバイダの設定は必要です。以下の providers の設定は、Visual Studio 2010 が自動的に生成する profile 要素の例です。profile の properties の add 要素に KanjiName を設定する必要はありませんの設定も必要です(2011/12/6 誤記訂正。下の追記参照)。anonymousIdentification の設定は不要です。

<profile>
  <providers>
    <clear/>
    <add name="AspNetSqlProfileProvider" 
      type="System.Web.Profile.SqlProfileProvider" 
      connectionStringName="ApplicationServices" 
      applicationName="/"/>
  </providers>
  <!-- やはり KnajiName の設定は必要 -->
  <properties>
    <add name="KanjiName" />
  </properties>
</profile>

以上の手順で、認証済みユーザーのプロファイルの取得、設定はできるようになります。

ただし、新規ユーザー登録をする際に、ユーザーの漢字の氏名などの情報を取得し、プロファイルを使用して保存する場合は Profile.KanjiName = TextBox1.Text; のようにしてもうまくいきません。

何故なら、その時点ではまだユーザーが認証されていないので、SetPropertyValue メソッドで例外がスローされてしまうからです。

以下のように、ProfileBase.Create メソッド を使って指定したユーザー名のプロファイルのインスタンスを作成し、それを操作するとうまくいきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Profile;

namespace WebApplication1.Account
{
  public partial class CreateUser : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      CreateUserWizard1.ContinueDestinationPageUrl = 
        Request.QueryString["ReturnUrl"];
    }

    protected void CreateUserWizard1_CreatedUser(
      object sender, EventArgs e)
    {
      ProfileBase profile =
        ProfileBase.Create(CreateUserWizard1.UserName, true);
      profile.SetPropertyValue(
        "KanjiName", lastName.Text + " " + firstName.Text);            
      profile.Save();

      FormsAuthentication.SetAuthCookie(
        CreateUserWizard1.UserName, false);

      string continueUrl = 
        CreateUserWizard1.ContinueDestinationPageUrl;
      if (String.IsNullOrEmpty(continueUrl))
      {
        continueUrl = "~/";
      }
      Response.Redirect(continueUrl);
    }
  }
}

---------- 2011/12/6 追記 ----------

先に作ったプロジェクトでは、何故か KanjiName の設定がなくても問題ありませんでした。

しかし、新たに別プロジェクトを作って、同様なコードを試してみたところ、KanjiName の設定がないと Profile.KanjiName プロパティで以下の例外がスローされます。

"System.Configuration.SettingsPropertyNotFoundException: 設定プロパティ 'KanjiName' が見つかりませんでした。"

理由は不明です(調査中)。違いは、先に作ったプロジェクトは .ASPXANONYMOUS クッキーが発行されること(anonymousIdentification は設定してないのに)と、開発マシンの Vista の IIS7 上で動かしていること(新たに作った方は開発サーバー)の 2 点ぐらいです。後者は関係なさそうですが、前者が怪しい感じです。

Tags:

ASP.NET

About this blog

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

Calendar

<<  February 2020  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
2425262728291
2345678

View posts in large calendar