問題
バックエンド単体でのテストでは問題なく動作していたにもかかわらず、フロントエンドと接続してブラウザ経由でリクエストを送ると、常に403 Forbiddenが返るようになりました。
原因の調査
ブラウザの開発者ツールで確認したところ、CSRFトークンはCookieおよびリクエストヘッダーの両方に含まれていました。
しかし、それにも関わらず403エラーが発生していたため、「サーバー側でトークンが正しく認識されていないのではないか」と考えました。
CSRFの設定を見直したところ、.csrfTokenRequestHandler(...) の設定が不足しており、リクエストヘッダーのトークンが正しく検証に使用されていなかったことが原因でした。
解決法
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) を追加し、リクエストヘッダーのCSRFトークンが正しく検証に利用されるようにしました。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) // ←追加
)
なぜこの設定がないと403になるのか?
CSRF対策では「トークンを保存すること」と「リクエストから取り出して検証すること」の両方が必要です。
.csrfTokenRepository(...) はサーバーがトークンの保存先を定義する設定ですが、これはあくまで「トークンを発行してクライアントに渡す」までの役割しか持ちません。
一方で、実際のリクエスト時には「どこからトークンを取得するか」が別途必要になります。
.csrfTokenRequestHandler(...) は、サーバーが送られてきたトークンをどこからどのように取得するかを定義する役割を持っています。
この設定がない場合、トークン自体は存在していてもサーバーがそれを検出できず、不正なリクエストと判断されて403が返されます。
まとめ
今回の原因は、CSRFトークンが送られていないのではなく、サーバー側で正しく取得できていなかったことでした。
Cookieやヘッダーにトークンが存在していても、.csrfTokenRequestHandler(...) の設定がないと検証に使用されず、結果として403エラーになります。
CSRF対策は「トークンを発行すること」だけでなく、「リクエストからどう取得するか」まで含めて成立するものだと理解できました。