WebSurfer's Home

Filter by APML

Blazor Web App の WebAssembly モード

by WebSurfer 17. November 2024 18:15

Visual Studio 2022 を使って下の画像のように Blazor Web App をテンプレートに選んで作成した Blazor アプリは、予想に反して WebAssembly モードでも Server-Side Rendering (SSR) になることがありました。以下にそのことを書きます。

Blazor Web App を選択

作成時に [Interactive render mode] を [WebAssembly] に設定しても、[Interactive location] に [Per page/component] を選んだ場合(多くの人はこちらを選ぶのではなかろうかと思います)、

[WebAssembly], [Per page/component] に設定

アプリを起動して [Home], [Counter], [Weather] ボタンをクリックして画面を切り替えると、

アプリを起動

下の Fiddler によるキャプチャ画像の通り、

Fiddler によるキャプチャ画像

毎回サーバーに要求を出し、完全な html ソースをサーバーから応答として受け取り、それをブラウザに表示するという SSR になります。WebAssembly だからサーバーとのやり取りはしない、即ち CSR になることを期待していましたがそうはなりません。

Microsoft のドキュメント ASP.NET Core Blazor render modes(日本語版もありますが翻訳がアレなので英語版がお勧め)の Render modes のセクションに説明があります。

そこにはコンポーネントで @rendermode を InteractiveWebAssembly に設定すると Client-side rendering (CSR) using Blazor WebAssembly となって Render location が Client になると書いてあります。

なので、試しに Home, Counter, Weather 全てのコンポーネントで @rendermode InteractiveWebAssembly を設定してみました。しかし、相変わらずサーバとのやり取りを行う SSR になります。

プロジェクトを作成する際に [Interactive location] を [Global] に設定すればサーバーとのやり取りはなくなり CSR になります。コンポーネントに @rendermode InteractiveWebAssembly を設定する必要はないようで、自動生成される各コンポーネントには @rendermode の設定は含まれていません。

知ってました? 自分は知らなかったです。 [Interactive render mode] を [WebAssembly] に設定するだけで無条件で CSR になると思ってました。WebAssembly のメリットは CSR のはずなので、勝手に知らないうちに SSR になっては困ると思うのですが・・・

ちなみに、テンプレートに Blazor WebAssembly を使って作成した Blazor アプリの場合は [Home], [Counter], [Weather] ボタンをクリックして画面を切り替えてもサーバーとの通信は発生せず CSR になります。


そのあたりのことを書いた Microsoft のドキュメントは見つからなかったので Copilot に聞いてみました。回答を載せておきます。(ホントかどうかの裏は取ってませんのでご参考まで)

(1) Blazor Web App と Blazor WebAssembly の違いに関して

The difference you're seeing between the two cases is indeed a bit subtle but has to do with how the Blazor Web App and Blazor WebAssembly templates are configured and what each is optimized for.

Blazor Web App

Interactive Render Mode: When you set the interactive render mode to WebAssembly but also have it set to Per page/component, Blazor is likely configured to request the server to render the pages/components interactively. This is why you're seeing GET requests being made to the server.

Behavior: Even though WebAssembly is being used, each time you navigate, a new GET request is made to the server. This means the server is still involved in delivering the HTML content, and the WebAssembly part is handling the interactive functionalities of the individual components on the page. This can be useful if you want the server to control the navigation and then WebAssembly to handle the client-side interactivity.

Blazor WebAssembly

Pure Client-Side Rendering: This template is optimized for a purely client-side experience. Once the initial application is loaded, all subsequent navigation and component rendering are handled entirely by the client without additional requests to the server.

Behavior: This makes it behave like a traditional Single Page Application (SPA) where the entire application, including navigation between pages, is handled within the client browser using WebAssembly. This is why you're not seeing additional requests to the server when navigating between Home, Counter, and Weather.

To summarize, the main difference is in how much the server is involved in rendering pages. In the Blazor Web App with interactive mode set to WebAssembly and per page/component, the server still plays a role in serving up pages, whereas, in Blazor WebAssembly, once the application is loaded, the server is not involved in further page rendering. This setup can affect performance and user experience depending on your application's needs.


(2) [Interactive location] を [Global] に設定すると何が変わるか

You're right, changing the Interactive location to Global in a Blazor Web App with the WebAssembly interactive render mode would make it behave similarly to the pure Blazor WebAssembly app, where no additional server requests are made during navigation. This setup alters the application's structure in a way that makes the WebAssembly component responsible for rendering more globally, not just per page/component.

Configuration Differences

Project Structure:

Global Interactive Location: This mode places components like Home.razor and Weather.razor under the .Client project, indicating that the client-side WebAssembly is handling the rendering globally for all these components.

Per Page/Component Interactive Location: In this mode, each page or component might still be interacting with the server for rendering purposes, leading to the behavior you observed with additional server requests.

Component Loading:

Global Interactive Location: The client-side WebAssembly loads the components globally, so once the application is loaded, navigation between different pages like Home, Counter, and Weather is handled entirely on the client side.

Per Page/Component Interactive Location: Each navigation may involve a server request because the rendering is done on a per-component basis, making the server more involved in the process.

Potential Reasons for Similar Program.cs

Shared Configuration: The Program.cs file might not differ significantly because the core configuration and service registrations for Blazor WebAssembly applications are quite similar. The main difference lies in how the application components are managed and rendered within the project structure and specific component files.

Conclusion

By setting the interactive location to Global, you've essentially optimized the application to function more like a typical SPA where the client-side WebAssembly handles most of the rendering and navigation logic, reducing the need for server interaction during page changes. This change primarily affects how components are structured and rendered rather than altering the fundamental configuration in Program.cs.

Tags: , , , ,

CORE

App.Debug.config と App.Release.config を使った構成ファイル書き換え

by WebSurfer 10. November 2024 18:31

.NET Framework 版の ASP.NET Web アプリは構成ファイルに Web.config を使っており、Visual Studio で開発を行う場合は運用環境にデプロイする際の書き換えを定義した Web.Release.config というファイルを作っておき、Visual Studio の発行ツールを使ってのデプロイ時に自動的に書き換えることが可能になっています。

詳しくはMicrosoft のドキュメント���Visual Studio を使用した ASP.NET Web 配置: Web.config ファイル変換」を見てください。

Windows Forms アプリや WPF アプリにはそのような機能は用意されていません。しかし、ネットの記事などを読むと、ASP.NET Web アプリと同様に、書き換え定義ファイルを作って構成ファイルを書き換えることができるそうです。

という訳で、今さらながらですが、Visual Studio 2022 Community のテンプレートを使って作成したターゲットフレームワーク .NET Framework 4.8 の Windows Forms アプリおよび WPF アプリで試してみました。

(注: .NET をターゲットフレームワークにしたアプリでは App.config ではなくて appsettings.json を使うことが推奨されています。その場合の書き換え方法については先の記事「WinForms アプリで構成情報の上書き (CORE)」を見てください)

Windows Forms アプリ、WPF アプリは App.config を構成ファイルのベースに使います。App.config 自体はアプリが使う構成ファイルではないことに注意してください。書き換えを行わない場合は、App.config の内容がそのままコピーされた <プロジェクト名>.exe.config という名前のファイルが作成され、それが構成ファイルになります。

App.Debug.config と App.Release.config は、App.config との差分を指定してデバッグ用とリリース用の構成ファイルを作成するための変換ファイルになります。環境(Debug または Release)ごとに App.config の内容を指定された差分に従って書き換えて <プロジェクト名>.exe.config という名前の構成ファイルを作成し、bin\Debug または bin\Release フォルダに配置するようにします。

以下に手順を書きます。Windows Forms アプリと WPF アプリで同じ手順になります。

(1) App.Debug.config と App.Release.config の追加

App.config は Visual Stidio のテンプレートを使って作成した Windows Forms アプリ、WPF アプリのプロジェクトには最初から含まれています。

それに App.Debug.config と App.Release.config という名前の 2 つのファイルをプロジェクトに追加します。ソリューションエクスプローラーのプロジェクトノードを右クリックし、表示されたコンテキストメニューから[追加(D)]⇒[新しい項目(W)]⇒[アプリケーション構成ファイル]と進んで、App.Debug.config と App.Release.config を追加してください。

追加したらプロジェクトファイルを開いて、

<None Include="App.config" />
<None Include="App.Debug.config" />
<None Include="App.Release.config" />

<None Include="App.config" />
<None Include="App.Debug.config">
  <DependentUpon>App.config</DependentUpon>
</None>
<None Include="App.Release.config">
  <DependentUpon>App.config</DependentUpon>
</None>

に書き換えます。

これによりソリューションエクスプローラーで構成ファイルを見た時、下の画像のように階層表示されるようになります。

App.Debug.config と App.Release.config

なお、これをやらなくても、自分が試した限りですが、書き換えには影響ありませんでした。Visual Studio のソリューションエクスプローラーでの見た目の問題だけなのかもしれません。

(2) App.config の例

今回の例では App.config には以下のように接続文字列と基本情報を配置してみました。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
  </startup>
  
  <connectionStrings>
    <add name="Database"
      connectionString="Data Source=(localdb)\mssqllocaldb;Initial Catalog=Database;Integrated Security=True"
      providerName="System.Data.SqlClient" />
    <add name="MyDB"
      connectionString="Data Source=(localdb)\mssqllocaldb;Initial Catalog=Northwind;Integrated Security=True"
      providerName="System.Data.SqlClient" />
  </connectionStrings>
  <appSettings>
    <add key="FilePath" value="C:\Users\surfe\Documents"/>
    <add key="FileName" value="original.txt"/>
  </appSettings>
</configuration>

(3) App.Debug.config の例

App.Debug.config には App.config の FileName を original.txt から development.txt に変更するための変換定義を書きます。

上にも書きましたが、App.Debug.config と App.Release.config は書き換え方法を指定する XML ファイルです。 変換操作は xdt プレフィックスにマップされる XML-Document-Transform 名前空間で定義されている XML 属性を使用して指定します。

詳しい方法は Microsoft のドキュメント「Web アプリケーション プロジェクト配置の Web.config 変換構文」が参考になると思います。(日本語版は翻訳がアレなので英語版を読むことをお勧めします)

configuration 要素に xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform" という属性を追加し、書き換え方法を指定するコードを追加します。

以下の定義で FileName を original.txt から development.txt に変更します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <appSettings>
    <add key="FileName" value="development.txt"
         xdt:Transform="Replace"
         xdt:Locator="Match(key)"/>
  </appSettings>
</configuration>

(4) App.Release.config の例

App.Release.config には、App.config の接続文字列を本番用に変更し、FileName を release.txt に変更し、さらにリリース版でのみ必要な情報 AdditionalInfo を追加するための変換定義を書きます。

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add name="Database"
      connectionString="Data Source=Release;Initial Catalog=DB1;Integrated Security=True"
      xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
    <add name="MyDB"
      connectionString="Data Source=Release;Initial Catalog=DB2;Integrated Security=True"
      xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
  </connectionStrings>

  <appSettings>
    <add key="FileName" value="release.txt"
      xdt:Transform="SetAttributes" xdt:Locator="Match(key)"/>
    <add key="AdditionalInfo" value="release version specific info"
      xdt:Transform="Insert"/>
  </appSettings>
</configuration>

(5) プロジェクトファイルに書き換え指示を追加

プロジェクトファイルの下の方にある、

<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

の下に以下のコードを追加します。追加する場所に制約があるようで、場所を間違えると変換されないので注意してください。

<UsingTask TaskName="TransformXml"
  AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="exists('app.$(Configuration).config')">
  <TransformXml Source="app.config"
    Destination="$(IntermediateOutputPath)$(TargetFileName).config"
    Transform="app.$(Configuration).config" />
  <ItemGroup>
    <AppConfigWithTargetPath Remove="app.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
</Target>

上のコードの意味については、この記事の下の方にオマケとして書きましたので興味があれば見てください。

(6) 書き換え結果

以上で、Visual Studio でプロジェクトをビルドすると、構成マネージャーの「構成」(Debug または Release)の設定に応じて、

構成マネージャ

App.config の内容が、変換ファイル App.Debug.config または App.Release.config に従って書き換えられて <プロジェクト名>.exe.config という名前の構成ファイルが作成され、bin\Debug または bin\Release フォルダに 配置されます。下の画像はプロジェクト名が WindowsFormsApp3 という名前の Windows Forms アプリのものです。

bin フォルダの構成ファイル

この記事の例で、bin\Relese フォルダに生成される WindowsFormsApp3.exe.config は以下のようになります。期待通り App.config の内容が App.Release.config に従って書き換えられています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
  </startup>
  
  <connectionStrings>
    <add name="Database" 
      connectionString="Data Source=Release;Initial Catalog=DB1;Integrated Security=True" 
      providerName="System.Data.SqlClient"/>
    <add name="MyDB" 
      connectionString="Data Source=Release;Initial Catalog=DB2;Integrated Security=True" 
      providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <appSettings>
    <add key="FilePath" value="C:\Users\surfe\Documents"/>
    <add key="FileName" value="release.txt"/>
    <add key="AdditionalInfo" value="release version specific info"/>
  </appSettings>
</configuration>

(7) アプリの実行例

もちろんアプリのコードで構成ファイルから取得する情報には上の書き換え結果が反映されます。例えば、Windows Forms アプリの場合、以下のコードを、

using System.Configuration;
using System.Windows.Forms;

namespace WindowsFormsApp3
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            this.label1.Text = ConfigurationManager
                               .ConnectionStrings["Database"]
                               .ConnectionString;

            this.label2.Text = ConfigurationManager
                               .ConnectionStrings["MyDB"]
                               .ConnectionString;

            this.label3.Text = ConfigurationManager
                               .AppSettings["FilePath"];

            this.label4.Text = ConfigurationManager
                               .AppSettings["FileName"];
            
            this.label5.Text = ConfigurationManager
                               .AppSettings["AdditionalInfo"];
        }
    }
}

Visual Studio の構成マネージャーの「構成」を Release にして実行すると以下の結果になります。

アプリを実行して構成情報を取得


以下はオマケの情報です。上の「(5) プロジェクトファイルに書き換え指示を追加」に書いたコードについて調べていろいろ分かったことがあったので、備忘録として残しておくことにしました。100% 間違いないところまで深く調べたわけではなく、想像と Copilot に聞いた話が混じっていますが。

以下にコードを再掲して説明を書きます。

<UsingTask TaskName="TransformXml"
  AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="exists('app.$(Configuration).config')">
  <TransformXml Source="app.config"
    Destination="$(IntermediateOutputPath)$(TargetFileName).config"
    Transform="app.$(Configuration).config" />
  <ItemGroup>
    <AppConfigWithTargetPath Remove="app.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
</Target>

UsingTask の AssemblyFile 属性には書き換えに使うアセンブリ Microsoft.Web.Publishing.Tasks.dll へのパスが指定されており、それを用いて書き換えを行っています。Web という名前のフォルダにあるということは、もともとは ASP.NET Web アプリの web.config を書き換えるのに用いているもののようです。

Target は、変換ファイル app.$(Configuration).config が存在する場合はコンパイル後に書き換えを実行するという設定をしています。$(Configuration) は構成オプションによって Debug または Release になります。

TransformXml は、プロジェクトの App.config ファイルを変換ファイル app.$(Configuration).config をベースに書き換えて、中間出力ディレクトリ(通常 obj\Debug または obj\Release)に $(TargetFileName).config という名前で保存するという設定になっています。

$(Configuration) は構成オプションによって Debug または Release になります。$(TargetFileName) はビルドで生成される exe ファイルの名前で、<プロジェクト名>.exe となります。

ItemGroup の中の最初の AppConfigWithTargetPath Remove="app.config" は最終出力フォルダ(bin\Debug または bin\Release)にオリジナルの App.config が書き込まれないことを確実にするためだそうです。

copilot に聞いた話ですが、AppConfigWithTargetPath は bin\Debug または bin\Release フォルダを意味するわけではないそうです。copilot によると "AppConfigWithTargetPath doesn't directly refer to the bin/Debug or bin/Release folders themselves, but it is related to how the app.config file is processed and used within those output directories." とのことでした。

(自分が AppConfigWithTargetPath Remove="app.config" を削除してビルドして試した限りでは、App.config が bin\Debug または bin\Release に書き込まれることはなかったのですが・・・)

ItemGroup の中の 2 つ目の AppConfigWithTargetPath Include=... は、中間出力ディレクトリに置かれた $(TargetFileName).config ファイルを、最終出力フォルダ(bin\Debug または bin\Release)に <プロジェクト名>.exe.config という名前で配置するという設定になっています。


もう一つオマケを書きます。

x64 Native Tools Command Prompt を使ってプロジェクトファイルの $(MSBuildExtensionsPath) などの値を調べる方法です。

x64 Native Tools Command Prompt は Visual Studio Installer で「C++ によるデスクトップ開発」のワークロードを追加し、インストールの詳細で「最新の v143 ビルドツール用 C++ ATL」と「最新の v143 ビルドツール用 C++ MFC」にチェックを入れてインストールすれば、Windows の「スタート」メニューに現れるようになります。

x64 Native Tool

そのツールを使ってプロジェクトファイルの $(MSBuildExtensionsPath) などの値を調べるには、プロジェクトファイルに以下の様なコードを追加します。

<Target Name="PrintProperties">
  <Message Text="MSBuildExtensionsPath="$(MSBuildExtensionsPath)""/>
  <Message Text="VisualStudioVersion="$(VisualStudioVersion)""/>
  <Message Text="Configuration="$(Configuration)""/>
  <Message Text="IntermediateOutputPath="$(IntermediateOutputPath)""/>
  <Message Text="TargetFileName="$(TargetFileName)""/>
  <Message Text="AppConfigWithTargetPath="$(AppConfigWithTargetPath)""/>
</Target>

そして x64 Native Tools Command Prompt を起動し、プロジェクトファイルのあるディレクトリに移動してから、msbuild <プロジェクトファイル名> /t:PrintProperties とタイプして実行すれば、以下のように結果が表示されます。

Command Prompt

Tags: , , , ,

.NET Framework

ASP.NET Core Identity の SecurityStamp

by WebSurfer 8. November 2024 18:06

ASP.NET Core Identity の SecurityStamp についていろいろ調べましたので、調べたことを備忘録として書いておきます。

どういう機能かを簡単に書くと、複数のユーザーが同じアカウントで同時にログインしている場合に、あるユーザーがパスワードの変更など重要なセキュリティに関わる変更を行った場合、他のログインユーザーの次回のアクセス時にサインアウトさせるというもので、デフォルトで有効に設定されています。

ASP.NET Core Identity が使うデータベースの AspNetUsers テーブルの SecuriryStamp フィールドと、認証クッキーに含まれる SecuriryStamp の値を使って上記の機能を実現しています。

下の画像はデータベースの AspNetUsers テーブルの SecuriryStamp 列の各ユーザーの値を SQL Server Management Studio で見たものです。

AspNetUsers テーブルの SecurityStamp 列

ユーザーがログインすると認証クッキーが発行されますが、その際データベースの SecurityStamp の値を Claim として認証クッキーに含めます。下の画像の赤枠部分を見てください。上の画像のデータベースの SecurityStamp の値と同じになっています。

認証クッキーに含まれる SecurityStamp

一旦ユーザーがログインに成功すると、それ以降はそのユーザーがサイトにアクセスしてくるたび認証クッキーが送信され、送られてきた認証クッキーをチェックして有効であればログインが継続されるという仕組みになっています。

ログインしたユーザーは自分のアカウントの設定を変更する管理画面にアクセスできます。下の画面は自分のパスワードを変更するページです。

パスワード変更画面

上の画面でパスワードの変更に成功すると、同時にデータベースの SecuriryStamp の値が変更されます。

パスワードを変更した際、ユーザーには認証クッキーが再発行されます。それにより、変更後のデータベースの SecuriryStamp の値と再発行された認証クッキーに含まれる SecuriryStamp の値は同じになります。

一方、同じアカウントでログインしている他のユーザーには認証クッキーは再発行されませんので、認証クッキーに含まれる SecuriryStamp の値は古いまま、すなわちデータベースと認証クッキーの SecurityStamp とは異なった値になります。

なので、あるユーザーによるパスワード変更後、他のユーザーがアクセスして来た時にデータベースと認証クッキーの SecuriryStamp の値を比較すれば異なっているので、そのユーザーをサインアウトさせることが可能です。

ただし、比較するにはデータベースにクエリを投げてデータベースの SecuriryStamp の値を取得してくる必要があります。それをユーザーがアクセスしてくるたびに行うのはサーバーの負担が大きいという問題があります。それゆえインターバル(デフォルトで 30 分)を設けています。

例えば 10,000 人のユーザーが同時にログインしていて、一人当たり 1 分に 5 回アクセスしてくるとします。インターバルを設けないでアクセスのたび毎回 SecurityStamp の比較を行うとすると、1 分間に 50,000 回データベースにクエリを投げることになります。

インターバルを 1 分にすると、ユーザー当たり 1 分に 5 回のリクエストの内 4 回は検証しないので 1/5 に、すなわち全体では 10,000 回/分に減ります。

さらに、インターバルを 30 分に増やすと 1/150 に、すなわち全体では 333 回/分に減ります。その程度であれば問題ないであろうということでデフォルトが 30 分になっているようです。

ただし、インターバルが短いほどセキュリティは高くなるはずなので、ユーザーが少なくアクセスの少ないサイトであれば短くした方が良さそうです。

Program.cs に以下のコードを追加することによりインターバルを変更できます。

// インターバルは FromMinutes(m) の m で設定。下のコードは 30 分
builder.Services.Configure<SecurityStampValidatorOptions>(options =>
        options.ValidationInterval = TimeSpan.FromMinutes(30));

詳しくは Microsoft のドキュメント「ISecurityStampValidator とすべてからのサインアウト」に書いてありますので見てください。


以上で分かった気になっていたのですが、よく考えてみると「チェックを 30 分のインターバルで行うとして、そのタイミングと、ユーザーがアクセスするタイミングは当然異なるはずだが、そこはどうしているのか?」が疑問です。そのあたりをさらに調べてみました。

上に紹介したMicrosoft のドキュメントには "Identity の既定の実装では、SecurityStampValidator がメインのアプリケーション cookie と 2 要素 cookie に登録されます。検証コントロールは、各 cookie の OnValidatePrincipal イベントにフックして Identity を呼び出し、ユーザーのセキュリティ スタンプ クレームが cookie に格納されているものから変更されていないことを確認します" と書いてあります。

具体的には、「メインのアプリケーション cookie」の方でいうと、そのために以下のオプション設定がデフォルトでされているということのようです。

builder.Services.ConfigureApplicationCookie(options =>
{
    // cookie 設定

    options.Events = new CookieAuthenticationEvents
    {
        OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
    };
});

上のコードで行っているのは以下の通りです。

CookieAuthenticationOptionsEvents プロパティCookieAuthenticationEvents (Allows subscribing to events raised during cookie authentication) を設定。

CookieAuthenticationEvents の OnValidatePrincipal プロパティ (Invoked to validate the principal) に SecurityStampValidator クラスValidatePrincipalAsync 静的メソッド (Validates a principal against a user's stored security stamp) を設定

ということで、

  1. ユーザーが認証クッキーを持ってアクセスしてくる
  2. cookie authentication プロセスが行われる
  3. CookieAuthenticationEvents が種々イベントをサブスクライブしている
  4. ValidatePrincipal イベントが発生すると ValidatePrincipalAsync が invoke される
  5. cookie と DB の SecurityStamp が異なっているとユーザーをサインアウトさせる

・・・というプロセスになるようです。

ValidatePrincipal イベントが発生するたび ValidatePrincipalAsync が無条件に呼ばれるようですが、その際上のステップ 5 を行うか否かにインターバルが関係しています。

そのあたりの詳細はググって調べてヒットするドキュメントでは分からなかったので Copilot に聞いてみたところ以下の回答でした。要するにミドルウェアが良しなにやっているとのことです。

  1. Initial Login: When the user logs in, an authentication ticket with the security stamp is issued and stored in the authentication cookie.
  2. Request Handling: When a request comes in, the authentication middleware reads the cookie and the authentication ticket.
  3. Interval Check: The middleware checks if the time since the last validation exceeds the ValidationInterval. This check is done based on the current time and the internally tracked last validation timestamp.
  4. Update: If the validation occurs, the middleware updates its internal record of the last validation time.

AI の回答は間違っていることもあるので、裏を取るため SecurityStampValidator.cs の ValidatePrincipalAsync メソッドが呼び出す ValidateAsync メソッドのコードを調べてました。Copilot の言っていることに間違いはなさそうです。

参考に SecurityStampValidator.cs の ValidateAsync メソッドのコードをコピーして以下に載せておきます

public virtual async Task ValidateAsync(CookieValidatePrincipalContext context)
{
    var currentUtc = TimeProvider.GetUtcNow();
    var issuedUtc = context.Properties.IssuedUtc;
 
    // Only validate if enough time has elapsed
    var validate = (issuedUtc == null);
    if (issuedUtc != null)
    {
        var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
        validate = timeElapsed > Options.ValidationInterval;
    }
    if (validate)
    {
        var user = await VerifySecurityStamp(context.Principal);
        if (user != null)
        {
            await SecurityStampVerified(user, context);
        }
        else
        {
            Logger.LogDebug(EventIds.SecurityStampValidationFailed, 
                  "Security stamp validation failed, rejecting cookie.");
            context.RejectPrincipal();
            await SignInManager.SignOutAsync();
            await SignInManager.Context.SignOutAsync(
                          IdentityConstants.TwoFactorRememberMeScheme);
        }
    }
}

その他気が付いたことも書いておきます。

上に紹介した Microsoft のドキュメントに書いてありますが、データベースの SecurityStamp を変更するには userManager.UpdateSecurityStampAsync(user) を呼び出します。

上に書いた管理画面でのパスワード変更にはそのメソッドが実装されているようで、パスワード変更操作でデータベースの SecurityStamp が変更されます。

ユーザーにアサインされるロールが変更された場合もデータベースの SecurityStamp を変更した方が良さそうですが、先の記事「ASP.NET Identity のロール管理 (CORE)」に書いた自作のメソッド EditRoleAssignment の AddToRoleAsync, RemoveFromRoleAsync ではデータベースの SecurityStamp は変わりません。SecurityStamp を変更するには userManager.UpdateSecurityStampAsync(user) の呼び出しを追加で実装する必要があります。

また、Microsoft のドキュメントに書いてあった sign out everywhere については、Logout.cshtml.cs に以下のように追加すれば実現できます。

public async Task<IActionResult> OnPost(string returnUrl = null)
{
    // ログアウトで SecurityStamp を再生成するため追加
    var user = await _userManager.GetUserAsync(User);
    await _userManager.UpdateSecurityStampAsync(user);

    await _signInManager.SignOutAsync();
    _logger.LogInformation("User logged out.");
    if (returnUrl != null)
    {
        return LocalRedirect(returnUrl);
    }
    else
    {
        // This needs to be a redirect so that the browser performs a new
        // request and the identity for the user gets updated.
        return RedirectToPage();
    }
}

もう一つ気になっていることがあります。それはデフォルトで有効になっている SlidingExpiration の影響です。これが働くと有効期間が延長された認証クッキーが再発行されますが、それによる SecurityStamp への影響が不明です。調べる気力がなくなったのでまだ未確認ですが、分かったら追記します。

Tags: , ,

Authentication

About this blog

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

Calendar

<<  January 2025  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar