Blazor

Windows Formsの癖でstaticを使ったらユーザー情報が混ざった話|Web開発 #06

こんにちは、社内SEのなんでも屋です。

前回の記事(#05)では、Webシステムの環境構築として Blazor Server + IIS の構成を選定したお話をしました。

今回は、システム開発の初期段階で私が見事に踏み抜いたデカすぎる地雷(ハマりポイント)についての解説です。

WinFormsやC#のデスクトップアプリ開発に慣れている方がBlazor Serverへ移行する際、99%の確率で事故りそうになる「static(静的メンバー)の使い方」について、実体験を交えて注意喚起と解決策をお伝えします!

発生したトラブル:「別の人の画面に私の名前が…?」

開発中、手元で動作確認をしていたときのことです。 テストユーザーAでログインし、別ブラウザ(シークレットウィンドウ)でテストユーザーBとしてログインした瞬間、恐ろしい現象が起きました。

  • トラブル1:ユーザー情報の混ざり(なりすまし状態) ユーザーBの画面に、なぜか「ようこそ、ユーザーAさん」と表示される。
  • トラブル2:DB接続先のごっちゃ混ぜ 開発環境とテスト環境の切り替えフラグを判定する処理で、タイミングによってテストDBと本番DBの接続情報が入れ替わり、テストデータが本番側に飛びそうになる。

「えっ……ブラウザ分わせてるのになんでデータが共有されてるの!?」と冷や汗が吹き出しました。
(web開発者にとっては当たり前だとは思うのですが、私の工場システムはほぼデスクトップアプリケーションでstatic多様が伝統であったこともあり・・・)

原因:WinFormsの癖による「static」の誤用

原因は至ってシンプル。私がデスクトップアプリ(Windows Forms)を開発していた頃の癖で、共通で使うデータや設定保持クラスに static(静的変数・静的クラス)を使っていたことです。

// 【NG例】WinFormsの感覚で書いてしまった共通クラス
public static class UserContext
{
    // staticにすると、サーバー上で全ユーザー共有になってしまう!
    public static string CurrentUserId { get; set; }
    public static string DbConnectionString { get; set; }
}

なぜBlazor Serverでstaticを使ってはいけないのか?

  • Windows Formsの場合: アプリ(.exe)はユーザーのPC上で個別に起動します。メモリ空間もユーザーごとに独立しているため、static変数を使っても他のユーザーに影響を与えることはありません。
  • Blazor Serverの場合: サーバー(IISなど)上の「単一のプロセス(メモリ空間)」で、接続してくる全ユーザーのSignalR通信を同時に処理しています。

つまり、Blazor Serverで static 変数を書き換えるということは、「サーバーにアクセスしている全ユーザーの画面の変数を裏で一斉に書き換える」ことと同義なのです。

解決策:DI(依存性注入)の「Scoped」を使おう!

Blazor Server(ASP.NET Core)で「ユーザーごとのセッション内で共通データを保持したい」場合は、static ではなく DI(Dependency Injection)の Scoped サービス を使用するのが鉄則です。

1. 状態保持クラスを「普通のクラス(non-static)」で作る

まずは static を外し、普通のインスタンス化できるクラスとして作成します。

// 【OK例】通常のクラスとして定義
public class UserState
{
    public string CurrentUserId { get; set; }
    public string DbConnectionString { get; set; }
}

2. Program.cs で「Scoped」として登録する

Program.cs にて、サービス登録を行う際に AddScoped を使用します。

// Program.cs
builder.Services.AddScoped<UserState>();

ASP.NET Coreにおけるサービスの生存期間(ライフタイム)には以下の3つがあります。

  • Transient(一時的): 要求されるたびに新しいインスタンスを生成。
  • Scoped(スコープ): 1つのWeb接続(Blazor Serverでは1つのユーザーセッション/Circuit)ごとに1つのインスタンスを保持。【★これを使う!】
  • Singleton(単一): アプリ起動中、全ユーザーで1つのインスタンスを共有(※staticに近い動きになるので注意!)。

3. Razorコンポーネントで @inject して使う

画面(.razor)側では、DIを使って注入して使用します。

Razor CSHTML

@inject UserState UserState

<h3>ようこそ、@UserState.CurrentUserId さん</h3>

@code {
    protected override void OnInitialized()
    {
        // このユーザーのセッション内でのみ保持されるので混ざらない!
        UserState.CurrentUserId = "User_A"; 
    }
}

まとめ:Blazor Server開発者が心に刻むべき教訓

  1. Blazor Serverでの static は「全ユーザー共有のグローバル変数」になる!
  2. ユーザー情報や接続情報など、「人によって変わる値」には絶対に static を使わない。
  3. セッションごとに共有したいデータは、AddScoped でDIコンテナに登録して使う。

「デスクトップアプリの感覚のままWeb(特にBlazor Server)に参入すると、思わぬセキュリティ事故やデータ汚染に繋がる」という、身をもって知った非常に良い勉強(恐怖体験)になりました……。

これからBlazor Serverで開発を始める方は、ぜひこの static の罠に気を付けて実装してみてください!

-Blazor