HTTP プロトコルはその特性上ステートを持たないため、常に Web アプリケーションはユーザー ステート管理の負担を担ってきました。すばらしいことに、ASP.NET にはユーザー ステートを保持する手段としていくつもの方法が用意されています。中でも特にすぐれているのが、セッション ステートの保持です。ASP.NET のセッション ステート機能は、任意のアプリケーション ステートを特定ユーザー セッションに関連付けするための便利なプログラミング インターフェイスを提供し、アプリケーションに必要なバックエンド ステート ストレージやクライアント セッション管理といった処理を引き受けます。この記事では、ハイパフォーマンスでスケーラブルかつ安全なセッション ソリューションの設計および開発について深く掘り下げ、既存の ASP.NET セッション ステート機能だけでなく、ASP.NET 機能チームから届いたばかりの新機能に基づくベスト プラクティスを提案します。
セッション ステートの働き
ASP.NET のセッション ステートを使用することで、ステート データを含んだサーバーサイド文字列やオブジェクト ディレクトリを特定の HTTP クライアント セッションに関連付けすることが可能になります。セッションとは、ある一定時間の間に同一のクライアントから発行された一連のリクエストのことであり、その管理は各クライアントごとにセッション ID を関連付けすることにより行われます。セッション ID は、リクエストごとにクライアントにより発行され、クッキーまたはリクエスト URL の一部分として保存されます。セッション データは、サポートされたセッション ステート ストア、たとえば、インプロセス メモリ、SQL Server データベース、ASP.NET State Server サービスなどに格納されます。SQL Server データベースと ASP.NET State Server サービスを使用した場合は、同一 Web ファーム上にある複数の Web サーバーでセッション ステートを共有することが可能になり、サーバー アフィニティの必要もありません (つまり、特定の Web サーバーを使用するようセッションを設定する必要がないということです)。
セッション ステートの実行時オペレーションは、SessionStateModule クラスにより実装され、アプリケーションでは IHttpModule としてリクエストのパイプライン処理に組み込まれます。SessionStateModule は、AcquireRequestState パイプライン ステージのハンドラ実行の前に一回、ReleaseRequestState パイプライン ステージのハンドラ実行の後に一回、実行されます (図 1 を参照)。AcquireRequestState ステージの SessionStateModule は、リクエストからセッション ID を抽出し、そのセッション ID に対応するセッション データをセッション ステート ストア プロバイダから取得しようと試みます。このとき、セッション ID が存在していて、ステートの取得が成功すれば、モジュールがセッション ステート ディレクトリを構築します。このセッション ステート ディレクトリは、ハンドラがセッション ステートを調査、変更するのに使用するものです。

図 1 セッション ステートの保持
ASP.NET 2.0 の場合、セッションの取り出しはセッション ID の管理コンポーネントにカプセル化されています。このコンポーネントは、カスタム インプリメンテーションで置き換えることができ、セッション ID のカスタム管理スキーマ (たとえば、セッション ID をクエリ文字列やフォームのフィールドに保存するなど)をサポートすることが可能です。デフォルトのセッション ID 管理では、クッキーベースと URL ベース、両方のセッション ID をサポートしています。また、ASP.NET 2.0 のデフォルト セッション ID 管理では、デバイス プロファイルまたはクライアントとのランタイム ネゴシエーションから、使用すべきセッション ID モードを自動的に検出する機能もサポートしています。
リクエストにセッション ID が存在していなく、ハンドラが何らかの変更をセッションに対して行うと、ReleaseRequestState ステージで新しいセッション ID が発行されます。この新しいセッション ID はセッション ID 管理の実装により、クライアントに送信されます。この場合と既存のセッションに対して変更が行われていた場合は、SessionStateModule が、ステート ストア プロバイダを使用してセッションに対して行われた変更を保持します。
また、ASP.NET 2.0 では、セッション ステートの保存機能もセッション ストア プロバイダ コンポーネントにカプセル化されています。このコンポーネントとしては、あらかじめ用意されている InProc、SQLServer、StateServer プロバイダ、あるいはセッション データ、セッションの作成、既存セッションへの変更の保存の取り出しを実装したカスタム プロバイダのいずれかを使用することができます。
パフォーマンスの改善
ASP.NET アプリケーションでセッション ステートを使用すると、アプリケーション パフォーマンスに著しいオーバーヘッドがかかることがあります。このオーバーヘッドは、保存されたステートを取得するためのネットワーク リクエスト等、各リクエストの処理中に実行されるアプリケーションのコード量が多いほど顕著です。しかしながら、要求されたステート管理機能を維持しながら、セッション ステートによるパフォーマンスへの影響を抑えるための、アプリケーションでの利用が可能なテクニックがいくつか存在します。
SessionStateModule モジュールには、リクエスト単位で発生するセッション ステートの一括処理機能が備わっています。このモジュールは、リクエスト情報により識別されたセッション ステートを ASP.NET リクエスト ハンドラが取り出す前に一回と、同じく ASP.NET リクエスト ハンドラが、新規セッションを作成した後またはハンドラの実行中に既存セッションへの変更を保存した後に一回、実行されます。
InProc セッション ステート モードは、あらかじめ用意されているストレージ モードの中で最も高速なモードです。InProc セッション ステート モードを使用した場合のオーバーヘッドの発生は、リクエストからのセッション ID の抽出、アプリケーションのメモリ空間に保存されたステート ディクショナリのキャッシュ ルックアップの実行、セッションの消失を回避するためにアクセス中であることをマークする場合に限られます。セッション内のデータへの更新はメモリ中のオブジェクトに対して直接行われるため、次のリクエスト用にセッション ステートを保持するための作業は一切必要ありません。ただし、InProc モードはアプリケーション リスタート等の耐障害性に乏しく、また Web ファーム シナリオでは動作しないという理由から、ほとんどの場合、アプリケーションには他のアウトオブプロセス モードのいずれかを使用する以外の選択肢はありません。
デフォルトでアウトオブプロセス モードの SQLServer と StateServer の場合、セッション ステートの取り出しは外部のストアから行い、AcquireRequestState ステージにおいて、バイナリ ブロブ (BLOB) リプレゼンテーションからインメモリ ステート ディクショナリへのデシリアライズを行わなければいけません。ReleaseRequestState ステージでは、インメモリ ステート ディクショナリを再度シリアライズして外部ストレージに転送する必要があります。また、データの消失を回避するため、ストア内のセッション エントリを更新して、最終アクセス日時を示しておく必要もあります。SQLServer、StateServer モードの場合、ステート データのシリアライゼーションおよびデシリアライゼーション、アウトオブプロセスのデータ転送処理は、リクエスト パスにおけるセッション ステート関連のオペレーションの中でも最もコストの高いオペレーションです。
SessionStateModule では、オーバーヘッドを可能な限り回避するために、デフォルトで多くの最適化を実行します。こうした最適化は、次の 4 つに分類されます。
1 つ目は、セッション ステートが必要なしとマークされているハンドラまたはページの場合です。この場合、セッション ステート処理は一切行われません。ただし、セッションがストア内でアクセス中とマークされている場合を除きます。読み取り専用セッション ステート アクセスが必要であるとマークされているページについては、最終アクセス日時のマークと初期データの取り出しのみが行われます。
2 つ目は、セッション ID をまた指定していないリクエストの場合で、この場合、データが実際にセッション ディレクトリに保存されるまで、セッションを開始しません。
3 つ目は、セッション ステートへのアクセスが一度も行われていないリクエストまたは不変なプリミティブ型を持つセッション変数に対し、読み取りアクセスしか行われていないリクエストの場合で、この場合、リクエストの最後の時点でステートの変更は保持されず、プロバイダへの書き込みは行われません。可変のセッション データにアクセスがあったリクエストまたはそのセッション データに修正が行われているリクエストについては、アクセスが行われた変数に対してのみシリアライズが行われ、残りの部分はバイナリ ブロブ (BLOB) リプレゼンテーションからコピーしてきます。
4 つ目は、プリミティブ型は直接シリアライズが行われますが、オブジェクト型の場合は、比較的速度の遅い、BinaryFormatter シリアライズ メソッドを使用してシリアライズが行われます。
セッション ステート管理によるパフォーマンスへの影響は、こうした最適化とこの記事で紹介するベスト プラクティスを上手に活用することで軽減することができます。 今回検討するプランでは、最も効果が期待できる次の 3 つのアプローチにフォーカスし、セッション使用時のアプリケーション パフォーマンスの改善、セッション ステート パフォーマンスの最適化の有効活用に取り組みます。
- オーバーヘッドの完全回避を目的として、セッション ステートを可能な限り無効にする。
- ステート データのシリアライゼーションおよびデシリアライゼーションによるオーバーヘッドを削減する。
- セッション ステートをステート ストアとの間でやりとりするアウトオブプロセスなデータ転送によるオーバーヘッドを削減する。
セッション ステートを無効にする
アプリケーションでは、必ずしもすべてのページがセッション ステートにアクセスする必要があるわけではありません。アクセスの必要がないページの場合、セッション ステートが不要であるということをマークすることで、ページにリクエストがあった場合にステート ストアからのセッション データの取り出しを抑制することができます。
セッション ステートを更新しないページの場合は、読み取り専用アクセスが要求されていることを示すことができます。この場合、ステート データの取り出しは抑制されません。しかし、データベース上で読み取りロックがかかるため、複数の読み取り専用リクエストが一度にセッションにアクセスすることが可能になるだけでなく、同一セッション ID で複数のリクエストがサーバーに対して行われたときのロック コンテンションも回避できます。何より、この方法だと、セッション ステート モジュールによる、リクエストの最後に行われるステートの更新を常に回避することができます (この手段としては、他にも方法があります。それについては、後述します)。
以下は、セッション ステートを無効にするコードです。。
<%@ Page EnableSessionState="False" %>同様に、以下は読み取り専用セッション ステートを示すコードです。
<%@ Page EnableSessionState="ReadOnly" %>実際、ショッピング カートのような標準的なページでは、ほとんどの場合、セッション ステートを更新するのは、ユーザーが商品をカートに追加または削除するなどのポストバック アクションを実行した場合のみで、ショッピング カートを参照しただけでは更新はしません。ショッピング カートの場合、カートの参照部分のみを、更新を実行しない読み取り専用セッション ステートを持つ独自のページに独立させることで、アプリケーション パフォーマンスを調整することができます。ショッピング カートの更新部分は、アクションのリクエスト時に参照部分を記述したページがポストを実行する別ページに独立させることが可能です (ASP.NET 2.0 ではクロス ページ ポスティングをサポートしています)。
また、<pages> というコンフィグレーション要素を Web.config ファイル内で使用することにより、アプリケーションのデフォルト動作を読み取り専用としたり、セッション ステートをデフォルトでオフにしたりすることができます。さらに、EnableSessionState というページ属性を下のように適宜設定することで、セッション ステートを明示的に有効にしたり、セッション ステートを必要とするページでのみ書き込みアクセスを有効にしたりすることができます。
<!--デフォルト状態ではセッション ステートなし-->
<configuration>
<system.web>
<pages enableSessionState="false" />
</system.web>
</configuration>
<!--デフォルト状態では読み取り専用セッション ステート-->
<configuration>
<system.web>
<pages enableSessionState="ReadOnly" />
</system.web>
</configuration>
また、アプリケーションでのリクエスト処理用にカスタム ハンドラを作成している場合は、ハンドラ クラスで System.Web.State.IRequiresSessionState マーカー インターフェイスを実装しなくても、セッション ステートをデフォルト状態で無効にすることができます。読み取り専用モードを有効にするときは、System.Web.State.IRequiresSessionState の代わりに System.Web.State.IRequiresReadOnlySessionState マーカー インターフェイスを実装します。
ただし、アプリケーションでセッション ステートをオフにしていると、ページまたはハンドラがセッション ステートにアクセスしようとした場合に、HttpContext.Session プロパティから例外が投げられますので注意が必要です。また、ページを読み取り専用とマークしていると、アウトオブプロセス モードで行われたセッションへの更新は、リクエストを超えて保存はされません。ただし、InProc モードの場合、更新は複数リクエストを通じてメモリに常駐するライブ オブジェクトに対して行われるため、次のリクエストでデータが失われることはありません。
また、特定のページまたはハンドラに対してセッション ステートをオフにしても、SessionStateModule は、ストア内のセッションをアクセス済みとマークします。そのため、残念なことに、ストアに対して確立される接続は、InProc モードを使用していなくてもアウトオブプロセスとなってしまいます。これは、リクエスト先のリソースがセッション ステートを必要とするか否かに関係なく、ユーザーがアプリケーションに対してアクティブにリクエストを行っている限りはセッションの終了を回避するための期待された正しい動作です。しかしながら、パフォーマンス上の理由から、一部のリクエストでこの動作を無効にしたい場合があるでしょう。たとえば、ASP.NET を使って、すでにセッションがアクティブとマークされている親 ASP.NET ページ内で使用される画像やページ リソースを提供している場合などです。
この動作の最適化を図るには、ASP.NET 2.0 のカスタム セッション ID 生成機能を利用し、リクエストのセッション ID を隠蔽します。こうすることで、そのリクエストに対する一切のセッション ステート関連処理を無効にできます。具体的な実装方法としては、System.Web.SessionState.SessionIDManager から派生したカスタム型を実装し、セッション ステートを必要としないリクエストについては NULL セッション ID を返す GetSessionID を実装します (図 2 を参照)。それ以外のリクエストすべてについては、ASP.NET でデフォルトのクッキーおよびクッキーレス セッション ID サポートを提供する、SessionIDManager クラスのデフォルト GetSessionID インプリメンテーションに処理を委譲します。
セッション ステートのトラックを無効にする ShouldSkipSessionState の実装方法には、面白い方法があります。たとえば、リクエスト先がカスタム コンフィグレーション セクションに指定した拡張の 1 つであると、セッション ステートを省略してもかまいません。また、現在のリクエスト ハンドラのインスタンス (HttpContext.Current.CurrentHandler として利用が可) が、独自の ISkipSessionState インターフェイスを実装している場合も、セッション ステートを無視できます。このインターフェイスは、独自のページ基本クラスを指定することで、ページに実装させることができます。
あるいは、現在のリクエスト ハンドラのインスタンスが IRequiresSessionState を実装していない場合で、アクセス済みとマークされたセッションのセッション ID で、最近リクエストが行われたばかりの場合です。(このことは、独自のカスタム クッキーの作成、または ASP.NET キャッシュにセッション ID とタイムスタンプのペアを保存することで、記録することができます。) たとえば、セッションをアクティブとマークしたリクエストが最後に行われたのが今から 30 秒以内の場合は、アクティブとしてはいけません。これにより目的が達成される一方で、セッションのアクティビティ ウインドウが減ることはありません。
カスタム セッション ID の管理コードは、App_Code アプリケーション ディレクトリに配置してもいいですし、アセンブリにコンパイル後に \Bin アプリケーション ディレクトリに配置することもできます。また、\Bin アプリケーション ディレクトリに配置する代わりに GAC にインストールすることもできます。そして、次のようなセッション ステート コンフィグレーションで登録を行うことで、コードは有効になります。
<configuration>
<system.web>
<sessionState
sessionIDManagerType="IndustryStrengthSessionState.
SessionDisablementIDManager"/>
</system.web>
</configuration>
クッキーレス セッション ID の使用時にこのテクニックを適用すると、リンクの生成や、クッキーレス セッションを保持するリダイレクト処理を実行する ASP.NET 機能の中には、NULL ID を返すリクエストで機能しないものがあります。
ネットワーク/ストレージのオーバーヘッドを軽減する
ネットワーク ラウンドトリップのオーバーヘッドを軽減し、さらにシリアライゼーションとデシリアライゼーションの影響を小さくするには、セッションに容量の大きいデータを保存しないようすることです。アプリケーションを設計する際に、セッションには操作を必要としないサイズの小さい情報セットのみを格納するようにし、残りのセッション オブジェクト モジュールについては、この情報セットをはさむようにリクエスト単位でビルドするようにしてください。この情報の再構築にそれほどコストがかからないのであれば、シリアライズ、デシリアライズ、データ転送の高速な、小さく、シンプルなセッションを実現できます。
同一セッションに対する複数のリクエストがサーバーに対して行われた場合に発生するロック コンテンションの発生を減らすには、サイトでのフレーム使用を抑制したり、セッションを多用するハンドラに提供される、画像やスタイルシートといったダウンロード可能なリソースを ASP.NET アプリケーションで使わないようにします。いずれの場合も、ブラウザは、同一セッション ID を使用して複数の同時リクエストをサーバーに対して発行します。その結果、セッションは複数回アクセスされることになり、他のリクエストによりセッション ロックが開放されるのを待機しなければならないため、時間が浪費されることになります。ロック コンテンションを回避するには、前述の読み取り専用セッション ステート テクニックを使用して、セッション ステート モジュールにリーダー ロックを使わせるようにします。これにより、同時複数読み取りが可能になります。
シリアライゼーションの最適化
セッション ステートでは、カスタム シリアライゼーション メカニズムを使用して、セッション ディクショナリとその内容をバイナリ ブロブに変換し、その上でアウトオブプロセス ストアにデータを保存します。このシリアライゼーション メカニズムは、.NET Framework のプリミティブ型 (String、Boolean、DateTime、TimeSpan、Int16、Int32、Int64、Byte、Char、Single、Double、Decimal、SByte、UInt16、UInt32、UInt64、Guid、IntPtr 等) を直接サポートしています。これらの型は、直接ブロブに書き込まれます。一方、オブジェクト型の場合は、BinaryFormatter でシリアライズが行われ、こちらの場合速度は遅くなります。デシリアライゼーションの場合も同様です。セッション コンテンツを最適化することで、ステート データのシリアライゼーションおよびデシリアライゼーションによるオーバーヘッドを著しく軽減することができます。
セッション オブジェクト モデルを設計するときは、オブジェクト型をセッションに保存しないようにします。代わりに、セッション ディクショナリにプリミティブ型のみを保存し、セッション データを使用するビジネス レイヤー セッション オブジェクトについてはリクエスト単位で再構築するようにしてください。これにより、BinaryFormatter 使用によるオーバーヘッドが回避されます。
セッションに様々な種類のデータ アイテムを保存している場合は、それらを単一のクラスやバッファにまとめて 1 つのアイテムとするのではなく、独立したたくさんのセッション エントリにフラット化した方が効率的です。これにより、必要なアイテムにだけアクセスするといったより緻密なアクセスが可能になります。また、アプリケーションでは、セッション アクセスの際、必要データのオンデマンド デシリアライゼーションを利用することができます。またこの方法では、リクエストの実行中にデータの一部が変更された場合に、もう一度シリアライズする必要のあるデータ アイテム数を軽減することができます。
プリミティブ型の代わりにクラスを使用している場合は、System.Runtime.Serialization.ISerializable インターフェイスを実装することで、BinaryFormatter に実行されるシリアライゼーション処理を制御することができます。この実装により、必要なデータのみをオブジェクトのシリアライズ時に書き出して、オブジェクトのデシリアライズ時に残りのデータを再構築することで、シリアライゼーション プロセスを最適化することができます。あるいは、クラスに SerializableAttribute でマークを施している場合は、NonSerializedAttribute を使用することで、デフォルトのシリアライゼーション メカニズムにシリアライズさせたくないオブジェクト データ メンバをマークすることができます。(.NET Framework のバイナリ シリアライゼーションの詳細については、バイナリ シリアル化 を参照してください。
コードでは、必要なセッション アイテムにのみアクセスする、ASP.NET 2.0 の部分デシリアライゼーション メカニズムを利用してください。セッション内の残りのアイテムについては、デシリアライゼーションによるオーバーヘッドの影響を受けることなく、ステート ストアの更新中 (再度シリアライズをせず)、アウトゴーイング セッション ブロブへのコピーのみが行われます。書き込み可能なセッション ステート ページ上のすべての読み取りアクセスが不変なプリミティブ型に対して行われていることを徹底することで、さらに効率性を高めることができます。これが、セッション ステート コレクションの書き込みアクセスの欠如と相まって、セッション データを更新するためのサーバーへのラウンドトリップを完全に回避することができます。
それでは、こうしたテクニックは実際どのように使われているかを見てみることにしましょう。図 3 は、あるセッションベースのショッピング カートの、ビジネス ロジックのレイヤー インプリメンテーションの最初の部分を示したものです (紙面の都合上、簡単なメソッドとプロパティ アセッサを省略しています)。
この実装は、製品アイテム、製品アイテムと個数を含む注文、アイテムのコレクションを含むショッピング カートを含む、ショッピング カート オブジェクト モデルを定義しています。クラスはシリアライゼーション可能とマークされているため、カートの参照を保存し、セッション ステートに実行時にオブジェクト グラフをシリアライズ/デシリアライズさせるだけで、カートをセッションに保存することができます。
図 4 は、今まで説明してきたテクニック - オブジェクト モデルをプリミティブ型に変換する、リクエスト単位でセッションから取得可能なデータを保存しない、プリミティブ型を使用してコレクションをフラット化する、アイテムの読み取り/書き込みアクセスを最適化してシリアライゼーションの再実行や更新をできる限り回避する - を適用した、最適化された実装です (サンプル内の変更されていないメソッドは省略してあります)。
この実装では、セッションへの保存が必要となる ID と個数に関する情報から、実行時に生成可能なアイテム情報を分離しています。また、注文のコレクションをフラット化してセッションに取り込むメカニズムも提供しています。つまり、ショッピング カートの注文の読み取り、書き込みが、互いに独立した状態で可能になり、部分デシリアライゼーション/シリアライゼーション メカニズムを利用できるようになります。最後に、この実装では、セット呼び出しの回避と読み取りアクセスの際に不変なプリミティブ型のみを使用することで、カートに変更が行われなかった場合の更新ラウンドトリップを、慎重に回避しています。
この実装では、オブジェクト モデル自体への変更は行っていませんが、SaveShoppingCartToSession および GetShoppingCartFromSession メソッドの使用による、セッション内にカートを保存、取得する方法のみを変更しています。これにより、ほとんどのアプリケーションから、必要な変更が切り離されます。便宜上、今回のサンプル コードでは、コレクションの削除または取得機能を実装していませんが、いずれの機能も、最適化の効果を損なうことなく実装することが可能です。
インプロセスの最適化
InProc モードは、アウトオブプロセス モードに比べるとはるかに高速ですが、インプロセス パフォーマンスを最適な状態で維持するために、いくつか覚えておかなければならないガイドラインが存在します。
1 つは、セッションにはシングルスレッド アパートメント (STA) COM オブジェクトを保存しないようにしてください。このガイドラインは、ASP 時代から有効です。STA COM オブジェクトは同じスレッドでしかアクセスすることができないため、ASP.NET は、セッションに対するすべてのリクエストが同じスレッドに処理されていることを確認した上で、COM オブジェクトへのアクセスをシリアライズします。ただし、これは、AspCompat ページ属性に True が設定されている場合に限ります。2 つ目は、セッションの全体サイズをできるだけ小さくして、メモリの使用量の増加を回避するようにしてください。これを怠ると、キャッシュ チャーンが発生したり、アプリケーション全体のパフォーマンスやキャパシティにマイナス影響があります。
スケーラビリティの向上
ASP.NET のセッション ステートでは、アウトオブプロセス ステート ストレージをサポートすることにより、ASP.NET アプリケーションでのホリゾンタル スケーリングが有効になります。これにより、複数の Web ファーム マシンで、セッション データを消失することなく、同一セッションに対するリクエストを処理することができます。しかしながら、Web ファームのキャパシティは、Web サーバー ノードを追加していくことでほぼ直線的に増加することができますが、セッション キャパシティが増加していくに伴い、1 つだけのセッション ステート ストアはボトルネックとなります。InProc モードの場合 (Web ファームで動作するアプリケーションの外部や適切なアフィニティを設定した Web ファーム内での使用が可能)、Web サーバー メモリ内でのセッションのメモリの使用が禁止になることもあります。
ASP.NET 2.0 のセッション ステート パーティショニング
ASP.NET 2.0 のステート パーティショニング機能により、セッション ステート ストアのホリゾンタル スケールアウトを有効にすることで、スケールアップが可能です。ASP.NET 2.0 には、その際に生じる問題の解決策が用意されています。ステート パーティショニングを使用すると、セッション データおよび対応する処理負荷を複数のアウトオブプロセス ステート ストアに分割することができるため、Web ファームの拡大や同時実行セッション数の増加に伴うセッション ステートの負担を減らすことが可能になります。具体的には、SessionStateModule にカスタム パーティショニング アルゴリズムを提供することで実現します。SessionStateModule はこのアルゴリズムを使って、ステート ストア接続文字列を現在のリクエストで使用すべきかどうかをセッション ID を元に判断します。SQLServer、StateServer いずれのプロバイダも、適切な接続文字列を使って、セッションの取り出し、保存を行います。
パーティショニング スキーマは、System.Web.IPartitionResolver インターフェイスからクラスを派生し、セッション ID - 接続文字列のマッピングを ResolvePartition メソッド内に構築することで実装できます。図 5 に示す基本実装では、Initialize メソッドにおいて、利用可能なステート ストア パーティショニングに対応したハードコード状態の接続文字列の配列を作成します。ResolvePartition メソッドでは、リゾルバがセッション ID 文字列に対してハッシュ処理を行い、ロードされている接続文字列の 1 つに対応したバケットを決定し、結果の接続文字列を選択しています。
理想としては、Initialize メソッドでロードする利用可能なパーティションを指定するためのコンフィグレーション コレクションを実装するか、Web ファームのネットワーク上にある集中ロケーションからコレクションを取得したいと思うでしょう。シンプルかつ一環したハッシュ処理を実装したとしても、セッションのストアへの配布でさえ相対的に見ると時間がかかります。これは、セッション ID の生成がランダムに行われるためです。一方で、現在のパーティションの負荷を基準にセッションを配置するパーティションを動的に決定するようなロードバランシング スキーマを実装してもよいでしょう。それには、パーティション ID をセッション ID にエンコードする必要があります。処理としては、カスタム SessionIDManager 派生と PartitionResolver を併用して新規セッションのパーティションを決定し、パーティション ID をエンコードした状態のセッション ID を生成します。将来のリクエストでは、パーティション リゾルバ内のセッション ID から抽出することで、パーティション ID を決定します。
パーティション リゾルバの実装は、App_Code アプリケーション ディレクトリに配置してもいいですし、アセンブリにコンパイル後に \Bin アプリケーション ディレクトリに配置することもできます。また、\Bin アプリケーション ディレクトリに配置する代わりに GAC にインストールすることもできます。最後に、完全修飾名を partitionResolverType 属性に指定して、リゾルバ型をセッション ステート コンフィグレーションに追加しなければいけません。
<configuration>
<system.web>
<sessionState
mode="StateServer"
partitionResolverType=
"IndustryStrengthSessionState.PartitionResolver" />
</system.web>
</configuration>
パーティション リゾルバは、セッション ステートが SQLServer または StateServer モードを使用している場合にしか使うことができません。また、sqlConnectionString や stateConnectionString 属性を使っても、接続文字列を指定することはできないので注意が必要です。
セッション ステートは、Web ファームのセッション ステートを管理する手段として、別の選択肢も用意しています。Web ファーム上でセッション ID にも対応したアフィニティ スキーマが使えることが前提になりますが、この方法を使用すると、アプリケーションは分散した InProc のステート ストレージの速度を抑制することができます。アフィニティ スキーマは、同じセッション ID を持つリクエストはすべて同一の Web サーバーに渡されることを保証する必要があります。その場合、各 Web サーバーは、他の Web サーバーとは共有しない独自のセッション ステート ストアを保持することができます。
アフィニティ スキーマは、同じセッション ID を持つすべてのリクエストが同一の Web サーバーに導かれることが保証する、リクエストのセッション ID 等、特徴のある要素がベースとなっている必要があります。こうしたスキーマは、クライアント IP (クライアントは、動的に割り当てられた Web プロキシを経由している可能性を念頭に入れておいてください) のネットワーク範囲、またはユーザー エイジェント ヘッダーをベースにすることもできます。こうしたアフィニティ スキーマの実装上の問題としては、HTTP レベルでのリクエストのルーティングが必要になるため (一般的な IP または TCP レベルでの接続ルーティングとは対照的ですが)、ハードウェア ロードバランシング システム上ではすぐには使用できないという点が上げられます。また、ステート ストアへのセッション ID のマッピングでステートを保持しなければならないという点から、ルーティングは確定的である必要があるために、このようなスキーマを実装すると、実際のロード バランシング処理ができなくなります。
セッション ステートの保護
ASP.NET のセッション ステートは、実際のステート情報はサーバー側に保存され、クライアントや HTTP リクエスト パスのネットワーク エンティティ上には現れないという点で、クライアントのステート管理テクニックに対してセキュリティ上の利点があります。しかしながら、セッション ステート オペレーションには、アプリケーションのセキュリティを維持する上で検討しなければならない重要な側面がいくつかあります。セキュリティに関するベスト プラクティスは、大きくわけると次の 3 つに分類されます: セッション ID のスプーフィング (なりすまし) やインジェクションを回避する、バックエンドのステート ストレージの保護、専用または共有環境におけるセッション ステート デプロイメントのセキュリティ確保。
セッション ステートはクライアント チケットを使用してサーバー側のセッションを識別しているため、セッション ID のなりすましやインジェクション攻撃の危険にさらされています。セッション ID のなりすまし攻撃とは、悪意のある第三者がリクエストにおいて他人のユーザーのセッション ID を使用し、アプリケーションを欺き、なりすましたユーザーのセッションをロードすることです。一方、インジェクション攻撃は、なりすまし攻撃とはほとんど反対の意味を持ちます。インジェクション攻撃の場合、悪意のある第三者が攻撃者のセッション ID を使って、正規のユーザーにサーバー対して強制的にリクエストを実行させます。セッション ID は、BASE64 エンコードの 62 文字からなるランダムな文字列であるため、ブルートフォースによるセッション ID 検索は、実行不可能であり、セキュリティの脅威とは認識されていません。
ASP.NET 2.0 のセッション ステートは、セッション ID のなりすましやインジェクション攻撃を回避できるよう強化が行われており、具体的には HTTPOnly というクッキー機能をうまく利用しています (現時点では、Microsoft Internet Explorer 6.0 SP1 または Windows XP SP2 をインストールした Internet Explorer 6.0 でサポートされています)。この機能により、クライアント側のスクリプトでセッション ID クッキーが使用できなくなります。その結果、セッション ID クッキーを盗むよう設計されたクロスサイト スクリプティング エクスプロイトについても同様の効果があります。この機能は、対応ブラウザでは自動的に有効になります。
また、ASP.NET 2.0 には、セッション ID の再生機能が用意されています。この機能は、サーバー上に見つからない、期限切れのセッション ID を新しく生成したセッション ID で強制的に置き換える機能です。これにより、クッキーレス セッションのリンク (検索エンジン クローラにより索引付けされるリンク等) で起こりやすいセッション ID の予想外の再利用を回避できるのはもちろんですが、さらにクッキーレス セッション リンクのポスティングによる悪意のあるセッション ID インジェクション攻撃も回避することができます。この機能は、デフォルトでオンになっていて、クッキーレス セッション ID でのみのサポートとなっています。アプリケーションでクッキーレス セッション ID を使用している場合は、この機能を常に有効にしておくようにしてください。ただし、オリジナル セッションがアクティブ状態のときに、攻撃者にセッション ID が分かってしまったような場合は、この機能ではセッションのなりすましを回避することはできませんので注意が必要です。
以前のバージョンの ASP.NET セッション ステートでは、アプリケーションの設定を、クッキーベースのセッション ID とクッキーレス リンクベースのセッション ID のどちらか一方を使用するようにしなければいけませんでした。クッキーが使用できないモバイル クライアントやブラウザをサポートする必要のあるアプリケーションでは、アプリケーション全体でクッキーレス モードを使用する以外、選択肢はありませんでした。しかし、クッキーレス ID は人の目に触れやすいため、なりすましも容易です。さらに、リンク ポスティングによるインジェクションやフィッシング攻撃にもさらされやすいため、セキュリティ上の観点から、クッキーレス モードの使用はお奨めできません。それでも、クッキー非対応ブラウザのサポートは必須です。
ASP.NET 2.0 では、2 つの新しいセッション ID モード、UseDeviceProfile と AutoDetect を使用できます。それぞれ、リクエストごとにどのタイプのセッション ID を使ったらよいかを、ブラウザ デバイス プロファイルを使用するか、クッキー機能の自動判別処理を実行することで判断します。ノンクッキー クライアントをサポートする必要がある場合は、クッキーレス ID を必要とするクライアントでのみクッキーレス ID を使用することでクッキーレスのリスクを軽減する、UseDeviceProfile モードを使用します。
なりすまし攻撃のリスクをさらに軽減する高度なテクニックとしては、ランダムなセッション ID とクライアントのユーザー エージェントのハッシュ、それからクライアントの IP アドレスのネットワーク部分を組み合わせ、ID の検証の際、情報の各部分が一致しているかどうかを確認する方法があります。残念ながら、これは完璧なプラクティスとはいえません。なぜなら、クライアントの多くが単一のプロキシ サーバーを経由している可能性があるため、同じユーザー エージェント文字列を共有している可能性もあるためです。それでも、なりすまし攻撃に対してはワンランク上の防御を提供します。また、前述のように、ASP.NET 2.0 のカスタム SessionIDManager 拡張メカニズムを使用することでも、この機能を実装することができます。ASP.NET 1.x での実装例ですが、MSDN マガジンの 2004 年 8 月号の Jeff Prosise's column (英語) に類似機能の実装例が紹介されています。
セッションの有効期限のタイムアウトをできるだけ小さい値に設定するのも、セキュリティ上すぐれたプラクティスであり、セッション ID エクスプロイトのほとんどをうまく回避できます。さらにハイ レベルな防御を実現したいのであれば、サイトを去る前のログアウトやセッションを中止する方法を簡単にした上で、JavaScript を使ってブラウザ ウインドウのクローズを検出し、サーバー上で強制的にセッションを破棄するようにします。
最後に、ネットワークレベルでのセッション ID、認証チケット、アプリケーション クッキー等のリクエスト/レスポンス情報のスニッフィング、を回避するため、SSL (Secure Sockets Layer) を使用するようにしてください。SSL の使用時は、クッキーで指定した URL スコープ内で HTTP 以外の URL を使用する場合、URL へのリクエスト中にブラウザにより送信されたクッキーはスニッフ (盗み見) される可能性があるということを認識しておいてください。このため、SSL を使用するときは、URL のサブセットのみを保護するのではなく、ドメイン全体を保護することを推奨します。
セキュリティの設定
SQLServer ステート プロバイダを使用する場合、通常、各バージョンに対応した Framework ディレクトリにある aspnet_regsql.exe ユーティリティを使用して、セッション ステート スキーマを SQL Server データベースに配置します。このユーティリティは必要なセッション ステート テーブルを作成してくれるのですが、データベース オブジェクトへのユーザー アクセスは付与してくれません。そのため、サーバー接続に使用する認証の種類 (統合 Windows 認証か SQL Server 認証か) に応じて、個々の Windows または SQL Server アカウントにデータベースと必須オブジェクトへのアクセスを明示的に付与する必要があります。
方法としては、SQL Server データベース ロール ("SessionStateFullAccess" と呼ぶことにしましょう) を作成し、テーブル自体には一切のアクセス権を付与せずに、セッション ステート ストアド プロシージャを実行するだけのロール権限を付与するやり方を推奨します。このロールは、Web サーバーのセッション ステート コンフィグレーションで使用するログイン アカウントに付与してください。これは最小特権という、あらゆる ASP.NET データ主導型の機能にとって汎用的なベスト プラクティスであり、その多くにはすでにロールの設定として取り込まれています。
SQL Server 接続文字列で統合 Windows 認証を使用している場合は、これまでのバージョンのようにリクエスト ID を基準とするのではなく、ホスティング ID を基準としてサーバーに接続する、ASP.NET 2.0 の設定オプションを利用してください。このオプションは、デフォルトで有効になっており、<sessionState> 構成要素で useHostingIdentity 属性を使用することで設定を行います。このオプションを設定すると、ドメイン全体や特定のユーザー セットに対してではなく、ASP.NET ワーカー プロセスやアプリケーション ID に対してデータベースのアクセス権が付与されるために、イントラネット上における認証管理が簡便化されます。
SQL Server 接続文字列で SQL Server 認証を使用している場合は、接続文字列を <connectionStrings> 構成セクションに設定して、接続文字列名を <sessionState> 構成に設定するだけで、サーバーへの接続を確立できます。さらに、ASP.NET 2.0 の構成の暗号化機能を用いて、<connectionStrings> セクションを暗号化することで、ログイン クレデンシャルを保護できます。
それから、SQL Server インスタンスが Web サーバー以外のマシンによるリモート アクセスから必ず保護されるよう注意してください。これは、Windows ファイアーウォールまたは IPSec ポリシーにより実現できます。
State Server サービスには、認証機能が用意されていません。そのため、State Server サービスにネットワーク アクセスできる人であれば、誰でもセッション データを変更することができてしまい、予期せぬ動作を引き起こすことがあります。State Server インスタンスの安全性を確保するには、Windows ファイアーウォールまたは IPSec ポリシーを使用して、Web サーバー以外のマシンからの不正なアクセスから保護されるよう注意しなければいけません。また、サーバーが動作するネットワーク ポートも変更した方がよいでしょう。ポートは、次のレジストリ キーに新しいポートを設定することで変更できます。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state \Parameters\Port=PORT
.NET Framework 1.1 の時代から (.NET Framework 1.0 SP3 と .NET Framework 2.0 も含む)、ステート サーバーは、デフォルト状態ではローカル ループバック インターフェイス上の接続しか待機しません。ステート サーバーは、ステート サーバー インスタンスが動作するマシン上の次のレジストリ キーを設定することで、Web サーバーからのリモート接続を待機するよう設定することが可能です。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state \Parameters\AllowRemoteConnection=1
まとめ
ASP.NET セッション ステートでは、単一のアプリケーションや Web ファーム環境で、ステート管理を行う強力なメカニズムを提供することができます。ASP.NET 2.0 には、たくさんの新しいセッション ステート機能が用意されています。このセッション ステートのパワーをアプリケーションでうまく活用することで、従来はクライアント アプリケーションでしか実現できなかったリッチでステートフル (処理状態を把握できる) な操作性の構築が可能になります。本資料で説明してきたデザインおよびデプロイメントに関するベスト プラクティスを適用することにより、パフォーマンス、スケーラビリティ、安全性にすぐれたセッション ステートの保持を Web アプリケーションで実現することができます。