複数のASP.NET Core Webアプリーションでセッション情報を共有する方法

# 目的
複数のASP.NET Core Webアプリーションでセッション情報を共有する必要があり、実現方法を調査しました。
本稿ではRedisを用いてセッション情報を共有するまでの手順について記載します。

**対象アーキテクチャ**
- .NET 6
- ASP.NET Core Webアプリ（Razor Pagesアプリーション）

# 1．セッションの使用
「ASP.NET Core Webアプリ」のテンプレートで作成したプロジェクトはそのままではセッションが使えません。
`Program.cs`に下記コードを追加することでセッションが有効になります。

```cs
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromSeconds(180);
});
```

```cs
app.UseSession();
```

:::note info
セッションの詳細な設定については、Microsoftの技術情報の[ASP.NET Core でのセッションと状態の管理](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/app-state?view=aspnetcore-6.0)を参考にして下さい。
:::

# 2．分散キャッシュを用いたセッション管理
先ほどの設定のみでは、セッションデータが各メモリ内に格納され、異なるアプリケーション同士で共有はされません。
これを解消するには分散キャッシュを用います。セッションに分散キャッシュを用いることで、スケールアウトで複数サーバーに配置されたアプリケーション間でもセッション情報を共有できるようになります。
Microsoftの技術情報の[ASP.NET Core の分散キャッシュ](https://docs.microsoft.com/ja-jp/aspnet/core/performance/caching/distributed?view=aspnetcore-6.0)では`IDistributedCache`の実装となる下記分散キャッシュについて提示されています。

|バッキング ストア|分散キャッシュサービスを追加する実装|
|--|--|
|SQL Server|[AddDistributedSqlServerCache](https://docs.microsoft.com/ja-JP/dotnet/api/microsoft.extensions.dependencyinjection.sqlservercachingservicesextensions.adddistributedsqlservercache)|
|[Redis](https://redis.io/)|[AddStackExchangeRedisCache](https://docs.microsoft.com/ja-JP/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangerediscacheservicecollectionextensions.addstackexchangerediscache)|
|[NCache](https://github.com/Alachisoft/NCache)|[AddNCacheDistributedCache](https://www.alachisoft.com/resources/docs/ncache/prog-guide/aspnetcore-sessions-ncache-idistributedcache-provider.html)|

# 3．Redisサーバーの準備
今回は分散キャッシュにRedisを選択しました。開発用に使えれば良いのでDockerで立ち上げました。

`docker-compose.yaml`
```yaml
version: '3'
  services:
    redis:
      image: "redis:latest"
      ports:
       - "6379:6379"
```

# 4．セッションに用いる分散キャッシュを設定
Nugetで`Microsoft.Extensions.Caching.StackExchangRedis`パッケージをインストールします。
これでサービスに分散Redisキャッシュを追加できるようになります。

```cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "127.0.0.1:6379";
    options.InstanceName = "sample";
});
```

# 5．データ保護対策
さて、これでセッションが共有できるようになるかと期待したのですが、`WebAppA`で設定したセッションを別の`WebAppB`で読み込もうとすると以下のような例外が発生するようになりました。
```
Microsoft.AspNetCore.Session.SessionMiddleware: Warning: Error unprotecting the session cookie.

System.Security.Cryptography.CryptographicException: The payload was invalid. For more information go to http://aka.ms/dataprotectionwarning
   at Microsoft.AspNetCore.DataProtection.Cng.CbcAuthenticatedEncryptor.DecryptImpl(Byte* pbCiphertext, UInt32 cbCiphertext, Byte* pbAdditionalAuthenticatedData, UInt32 cbAdditionalAuthenticatedData)
   at Microsoft.AspNetCore.DataProtection.Cng.Internal.CngAuthenticatedEncryptorBase.Decrypt(ArraySegment`1 ciphertext, ArraySegment`1 additionalAuthenticatedData)
   at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.UnprotectCore(Byte[] protectedData, Boolean allowOperationsOnRevokedKeys, UnprotectStatus& status)
   at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Unprotect(Byte[] protectedData)
   at Microsoft.AspNetCore.Session.CookieProtection.Unprotect(IDataProtector protector, String protectedText, ILogger logger)
```

ASP.NET Coreではセッションデータを格納する際に暗号化されています。
Microsoftの技術情報の[データ保護の構成](https://docs.microsoft.com/ja-jp/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-6.0)のセクションを読むと下記記載が見つかりました。

> **SetApplicationName**
> 既定では、データ保護システムによって各アプリが、それらのコンテンツルートパスに基づいて互いに分離されます。同じ物理キーリポジトリを共有している場合でも同様です。この分離により、各アプリで互いの保護されたペイロードを把握できなくなります。
> 保護されたペイロードをアプリ間で共有するには:
> - 各アプリで同じ値を使って SetApplicationName を構成します。
> - アプリ全体で同じバージョンのデータ保護 API スタックを使います。 各アプリのプロジェクト ファイルで、次のいずれかを実行します。
>   - Microsoft.AspNetCore.App メタパッケージを使って、同じ共有フレームワーク バージョンを参照します。
>   - 同じデータ保護パッケージ バージョンを参照します。

今回は同一サーバー上で動作するため、物理キーリポジトリは共有できていると思われますが、コンテンツルートパスが異なります。
下記設定を追加することで、2つのWebアプリでセッション情報が共有されることが確認できました。

```cs
builder.Services.AddDataProtection().SetApplicationName("shared");
```

:::note warn
今回は同一サーバー上の複数アプリケーションでしたので、この設定だけで動作しましたが、複数サーバーで共有する場合は鍵の共有についても検討が必要だと思います。
:::

# 6．まとめ
- セッションを共有するには分散キャッシュを用います。
- 複数のWebアプリケーションで互いのペイロードを共有するには`SetApplicationName`を用います。



なお、今回の修正を当てた`Program.cs`全文は下記となります。

```cs
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

builder.Services.AddDataProtection().SetApplicationName("shared");

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "127.0.0.1:6379";
    options.InstanceName = "sample";
});

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromSeconds(180);
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();

app.Run();
```
:::note warn
このコードでは接続先やタイムアウトの設定などを、何が設定しているのが分かりやすいよう直接指定しています。
実際に使用する場合は`appsettings.json`に設定することを検討してください。
:::