読者です 読者をやめる 読者になる 読者になる

Spring Sessionのバージョンアップとデシリアライズエラー

Spring Security + Spring Sessionを使っている場合、通常、セッションの中身はシリアライズされてセッションストアに保存されます(実際に確認したのはRedisのみ)。

シリアライズされる org.springframework.security.core.context.SecurityContextImpl には serialVersionUID が定義されているのですが、その値は SpringSecurityCoreVersion の中にあります。
https://github.com/spring-projects/spring-security/blob/4.1.x/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java#L35-L41

コードの通り、Spring Securityのバージョンごとに決まった値になります。

したがって、Spring Securityをバージョンアップした場合、それまで使っていたセッションがリクエストされると、デシリアライズに失敗します。
issueがGitHubやstarckoverflowに挙がってます。

シリアライズに失敗した場合、サーバ側エラーとなって500エラーを返してしまうので、本来はセッションの有効期限切れと見なして403エラー辺りを返したいところです。

対象方法についてのメモです。

対処1: いったんセッションの中身をクリアする

乱暴ではありますが、ソースコメントにも「Classes are not intended to be serializable between different versions.」とある通り、シンプルに、バージョンアップの際にセッションストア側をクリアしてしまうのが確実だと思います。

Redisの場合は、キーが spring:session:* にマッチするものを削除すればOKです。

対処2: 独自の SessionRepository を作ってデフォルトの挙動をカスタマイズする

以下に紹介されていました。

sdqali.in

シリアライズエラーをcatchして、該当のセッションをRedisから削除しています。
また、getSessionnull を返すことで、有効なセッションが見つからなかった時と同じ振る舞いにさせているっぽいです。

方法1と比較すると、追加のコードが必要ではあるものの、バージョンアップの度にRedisの中身をメンテする必要はなくなるので、保守性では優れている気がします。

対処3: セッションの中身をマイグレートする

方法1と方法2も、バージョンアップ前のセッションを破棄するアプローチでした。
Spring Sessionのバージョンアップの頻度はそう高くはないとは思いますが、依存ライブラリのバージョンアップというサーバ側の都合で、セッションが破棄されてしまうのが許容できないケースもあるかもしれません。

そういうったケースの対応方法を考えてみました。一つのプログラム内で新旧の SecurityContextImpl が共存できないので、2つのステップ(プログラム)に分けています。

  1. 既存のセッションの中身をエクスポートする。エクスポートする際は旧バージョンのSpring Securityを利用してデシリアライズし、 Javaシリアライズに依存しない形 (例えばJSONなど)で出力する。
  2. エクスポートした内容を読み取り、新バージョンのSpring Securityを利用してセッションストアに戻す。

うーん、無理やりな感じが否めないので、オススメはできそうにないですね。