銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

ASP.NET Core で複数Webサーバーでセッションを共有するときは、IDistributedCacheとIDataProtectionに注意しないといけない話

ASP.NET Core Advent Calendar 13日目です。

qiita.com

初日にSignalRでエントリ書きましたが、もともと予定していたセッションについて簡単に追いかけてみましょう。ASP.NET Core でSessionを使う場合、まずはこのドキュメントを読むのがよいでしょう。

Managing Application State | Microsoft Docs

で、Sessionを使うにはまずMicrosoft.AspNetCore.Sessionパッケージを追加しろとあります。その後のNoteに書いてあることを無視して、とりあえずこのパッケージだけ追加して、Startup.csを次のように書いてみましょう。

public void ConfigureServices(IServiceCollection services)
{
    //services.AddDistributedMemoryCache();
    services.AddSession();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseSession();
}

これで実行するとこんなエラーが出ます。

Unhandled Exception: System.InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while attempting to activate 'Microsoft.AspNetCore.Session.DistributedSessionStore'.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.Service.PopulateCallSites(ServiceProvider provider, ISet`1 callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.Service.CreateCallSite(ServiceProvider provider, ISet`1 callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetResolveCallSite(IService service, ISet`1 callSiteChain)

DIでIDistributedCacheを実装したサービスが見つからないということでエラーが出ます。無視したNoteにある通り、Sessionの機能はIDistributedCacheに依存して実装しているので少なくとも1つはその実装を追加しないといけません。実際に、Microsoft.AspNetCore.SessionMicrosoft.Extensions.Caching.Abstractionsに依存していることもパッケージからわかります。

www.nuget.org

開発用途限定ですが、IDistributedCacheを実装するパッケージがMicrosoft.Extensions.Caching.Memoryです。これは特に設定の必要もなく、次のようにコード一行追加するだけで動きます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedMemoryCache();
    services.AddSession();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseSession();
}

この開発用途限定と言っているパッケージ、名前の通りインメモリーにキャッシュを保存します。注意しないといけないのは複数サーバーでスケールアウトする場合で、当然各サーバーで別々にセッションをメモリーに保存するので、別のサーバーに行くとその前にアクセスしたセッションにはアクセスできません。というわけで本番環境では、IDistributedCacheを実装しているサービスを用意する必要があります。ASP.NET Coreチームから提供されているのはSQLServerとRedisに格納するものになります。

github.com

www.nuget.org

www.nuget.org

よし、これで複数サーバーでSticky Sessionの設定をせずともセッションを共有できる... という訳には実はいきません。そのことは実はだいぶ前のエントリで書いています。

tech.tanaka733.net

この記事に書いている通り、ASP.NET Coreではセッションデータを格納する際に暗号化しています。これはIDataProtectionというインターフェースを実装するサービスがその役割をになっており、デフォルトはマシンごとに固有の鍵をローカルファイルシステムにファイルとして保存します。さらにこのインターフェースはIDistributedCacheとは独立しているので、Redis Cachingを使ったとしてもIDataProtectionはデフォルト実装のままです。デフォルト実装の際に保存されるファイルパスはこのコードにあります。

DataProtection/FileSystemXmlRepository.cs at rel/1.1.0 · aspnet/DataProtection · GitHub

実際、このようなファイルが保存されています。

.aspnet/DataProtection-Keys/key-c28289ea-0e92-4e6b-94ff-6985ba6700ac.xml
$ cat .aspnet/DataProtection-Keys/key-c28289ea-0e92-4e6b-94ff-6985ba6700ac.xml 
<?xml version="1.0" encoding="utf-8"?>
<key id="c28289ea-0e92-4e6b-94ff-6985ba6700ac" version="1">
  <creationDate>2016-11-30T12:24:55.245682Z</creationDate>
  <activationDate>2016-11-30T12:24:55.220079Z</activationDate>
  <expirationDate>2017-02-28T12:24:55.220079Z</expirationDate>
  <descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=1.1.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
    <descriptor>
      <encryption algorithm="AES_256_CBC" />
      <validation algorithm="HMACSHA256" />
      <masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
        <!-- Warning: the key below is in an unencrypted form. -->
        <value>uT3l6pCiERM0Mwd6F8T4nOnnv9DmwSU6ii8AaZj6XG6qR0MorUN1Akxa5Lqivt8hKfu2L9TBmryC16I6BuOXrQ==</value>
      </masterKey>
    </descriptor>
  </descriptor>
</key>

というわけですので、IDataProtectionも必要に応じて共有できる実装に切り替えましょう。Data Protectionについては別のドキュメントで説明されており、デフォルトでは同じ物理パスを指定してもマシン固有になることも書かれています。

Configuring Data Protection | Microsoft Docs

上に引用した記事を書いた時点では、.NET Core on Liniuxで(比較的)容易に用意できる共有先はNFSくらいだったのですが、現在はRedis実装がPreviewですが用意されています。またAzureStorage向けのものも用意されています。

www.nuget.org

www.nuget.org

といったところで長くなってきたので、実際に動くコードで検証するのは続編でかきたいと思います。