Spring Boot 3.0 (Spring Framework6.0) へのアップデートで対応したこと

## 概要

自分自身の学習用に作成した Spring Boot アプリケーションを3.0に上げました。その際に対応したことをまとめます。
複雑なアプリでは無いので修正内容も限定的ですが誰かの参考になればと思います。
尚、修正箇所は Spring Security関連が多めです。また 言語はKotlinですがJavaの場合でも同じ対応でOKだと思います。

## TL;DR

修正内容はPRを作成しているのでよければそちらをご覧ください。
いくつかの修正はSpring Bootのアップデートと関係ないのでここでは説明しません。

https://github.com/kazokmr/book-manager/pull/76


## 修正点

主に自分がつまずいた点を挙げていきます。詳細を確認したい場合は冒頭のPRをご覧ください。

### 依存関係のアップデート

`spring-boot-starter-xxx` のバージョンを全て 2.7.5 から 3.0.0 上げました。

**つまずいた点**

`spring-security-test` を 5.7.3 から 6.0.0 に上げるのを忘れてしまった。
このためTestクラスに後述のパッケージ変更が適用されず、テストを実行したときにエラーとなってしまいました。
このアプリでは、Spring Framework の依存関係を使っているのはこれだけでした。

### パッケージ名の変更

`javax.*` を `jakarta.*` に変更しました。
ここは IntelliJがコンパイルエラーを出し、かつ修正案をサジェストしてくれるのでコツコツと修正していくだけでした。

### APIの変更やDeprecatedへの対応

ここも先ほどと同様に IntelliJが出力するエラーと修正候補に更新した。

**つまずいた点**

つまずいたというほどではないが、SecurityConfigクラスで、`mvcMatchers` や `ignoringAntMatchers` が使えなくなったので、変更後のAPIを調査し、それぞれ `requestMatchers` と `ignoringRequestMatchers` に変更した。

### SecurityConfig クラスのアノテーションを見直した

SecurityConfigクラスに `@Configuration` を付けた。
Spring Securityの5.7までは `@EnableWebSecurity` に内包されていたが5.8でひっそりと外されていた。また公式ドキュメントのサンプルコードも5.8からは `@Configuration` を付けていた。

**つまずいた点**

SecurityConfigTestクラスに `@ContextConfiguration(classes = [SecurityConfig::class])` を付けていたので、単体テストは問題なくPassするのに TestRestTemplateを利用した IntegrationTestではテストがことごとく失敗するので悩んだ。

最終的にアプリケーションを実際に立ち上げてリクエストを送信して確認していたら、SecurityConfigの設定が効いていないことに気がつき、EnableWebSecurityクラスを確認してアノテーションが削除されていることに気づいた。

今後のバージョンアップで同様の問題が発生することを避ける意味で、Testクラスに `@ExtendWith(SpringExtension::class)` を明示的に付けるようにした。これは今は `@WebMvcTest`で宣言しているので問題ないのだが、SpringFWの公式ドキュメントのサンプルでは明示的に付けられていたので合わせるようにした。

### CSRF対策を見直した

これまで CookieベースでCSRF対策トークンを利用していたがアップデートした所、これまでGETリクエストや CSRF対策の対象外としていたPOSTリクエストなどのレスポンスでは、CookieにCSRF対策トークンがセットされなくなってしまった。変更の背景や原因は今だに不明だがこれではアプリの初回アクセスでCSRF対策トークンが取得できなかったので、以下の方針でコードを対応するようにした。

* CSRFトークンの管理を Cookieベースから HTTP Sessionベースに戻した
* ログイン認証を行う `POST /login` もCSRF対策の対象とした
* 新たに CSRF対策トークンを取得するための `GET /csrf_token` を追加した

このアプリはRESTアプリなのでCSRF対策トークンをレスポンスヘッダに埋め込む必要があったので、以下のような対応も行った

* `GET /csrf_token` のコントローラで、レスポンスに生成された CSRF対策トークンをセットするようにした
* `POST /login` によって認証するとセッションが更新されるので、CSRF対策トークンも更新される。これに対応するため 認証成功時に呼び出される AuthenticationSuccessHandlerでも 同様にレスポンスにCSRF対策トークンをセットするようにした
* IntegrationTestを実行時に呼び出される ClientHttpRequestInterceptorクラスでは、これまでレスポンスでCookieにセットされたCSRF対策トークンを次回以降のリクエストのヘッダーにセットしていた処理を レスポンスヘッダから取得しリクエストヘッダにセットするように変えた

**つまずいた点**

今回の更新で一番悩んだ問題だった。これも単体テストでは検出できず、IntegrationTestで検出したのと実際にアプリケーションを起動してPostmanでリクエストとレスポンスを見ながら調査、確認を行いながら対応していった。

ただ、Spring Securityの[ドキュメント](https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf-considerations) にも記載があるように「Login処理にもCSRF対策を行った方が良い」ことや「CookieベースのCSRF対策トークンに対する既知の問題」もあったので、今回の対策に踏み切れたのは良かった。

:information_source: 補足ですが その後のPRでCSRF対策トークンのハンドリングを少し変えています。

https://github.com/kazokmr/book-manager/pull/77

#### 補足

Spring Security 6 からは 必要なリクエストが来るまで CSRFトークンのロード（CSRFトークンを生成して セッションやCookieへのセットする処理）を遅延するのがデフォルトとなり、CSRFトークンによる検証が不要なリクエストだとトークンが取得できないことがわかった。ソースコードを確認すると CSRFトークンの取得に Supplierを使っており get()メソッドが呼ばれるまでロードしないようになっている。このためCSRFトークンを取得するためには 意図的に get()メソッドを呼ぶようにする必要がある。

例:
* HttpSecurity の Csrf設定で CorsRequestHandlerに `setCsrfRequestAttributeName(null)` をセットする
  * こうすると内部で強制的に get()が呼ばれる 
* カスタムFilterやCSRFを取得するControllerなどを用意し、`request.getAttribute("_csrf")` で取得できる CsrTokenオブジェクトで getToken()を実行してトークンを取得する

https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#deferred-csrf-token

### Redisのセッション管理設定を見直した

Updateの影響かは未確認ですが、以下の3点についてIntelliJ で警告が出ていたので修正しました

* application.yaml で設定していた `spring.redis.host` と `spring.redis.port` をそれぞれ `spring.data.redis.host`, `spring.data.redis.port`に変更しました
* spring.session.store-typeプロパティが IntelliJから認識されなかっため、 HttpSessionConfig に`@EnableRedisHttpSession` を付ける対応に変更しました
* Jackson2JsonRedisSerialize の `setObjectMapper()` が deprecatedになるためコンストラクタにobjectMapperを渡すように変更した。

**つまずいた点**

１つ目の `spring.data.redis.host`, `spring.data.redis.port` は、Test実行時に起動しているTestContainerの設定も変えなければいけなかったが失念していた。

というのもこれまでのCSRF対策の動作確認などの際に、TestContainerとは別にRedisをDockerコンテナで起動していたのでTest実行時もTestContainerを見ずにそっちのDockerコンテナにアクセスしていたことに気づかなかった。
結局、冒頭のPRを作成したときに実行したGithub Actionsで設定漏れが判明した。

## 感想

この規模のアプリケーションでも修正は大変だった。主に調査に時間を掛けたわけですが。
その反面、特に「IntegrationTestの実行」と「依存関係の定期的な更新」の２つを行ってきたことが、問題の早期発見と対策に非常に役立った。

Integration Testを用意していたことで、UnitTest では Pass する問題にも気づくことができた。また CSRF対策についてはIntercepterの処理も行っていたので、CookieベースからHttpSessionベースに切り替えでもようにテストコードも対応することができて良かった。

また、依存関係はこまめにアップデートしてテストと修正を行うようにしていたのも良かった。 Spring Securityは少し前に5.7にUpdateされた際にも比較的大きい修正が入っており、これについて以前対応済みだったので今回は Spring Security 6.0 (Spring Boot3.0)の対応に注目して修正することができた。

https://qiita.com/kazokmr/items/1b87db4787c7cdb6c76c

というわけで、これからも新しいことにも挑戦しつつ こんな感じで既存の依存関係アップデートも試して早めに対応方法を把握できるようにしていく。