はじめに
Laravel+Vue.jsでSPAを作るときのCSRFトークンの扱いについてつまずいたので設定方法や原因を私なりにまとめました。
もっと良い方法がある、この方法だとこれがだめ、といったことがありましたら指摘いただければ幸いです。
環境&前提
- Laravel 5.8
- 認証機能はLaravel標準を使用
- Vue.js 2.6.10
- axiosでAPIを叩いてPOST
設定方法
結論から言うと、今回の私の環境や前提では特に何もしなくてよいでした。
前提通りなら特に設定は不要で、axiosがCookieからCSRFトークン(を暗号化したもののようです)を取得して送信してくれます。
日本語のドキュメントにも書いてありますね…
https://readouble.com/laravel/5.8/ja/csrf.html#csrf-x-xsrf-token
いくつかのJavaScriptフレームワークや、AngularとAxiosのようなライブラリーでは、自動的に値をX-XSRF-TOKENヘッダに設定するため、利便性を主な目的としてこのクッキーを送ります。
私はこれを知らず、余計なことをしたためにハマりました。。。
私がハマったこと
状況
ログイン直後にフォームからPOSTすると、CSRFトークン不一致のエラーになる。
画面をリロードすると正常にPOSTできる。
やったこと(誤っていた設定)
https://readouble.com/laravel/5.8/ja/csrf.html#csrf-x-csrf-token
上記を参照して以下2点を行いました。
-
headタグ内にCSRFトークンを埋め込む
<meta name="csrf-token" content="{{ csrf_token() }}">
-
resources/js/bootstrap.js
を読み込む
このファイルに以下のような記載があって、これでmetaタグのトークンをaxiosのヘッダに埋め込んでいるようです。
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
トークン不一致の原因
フロント側のCSRFトークンが更新されず、サーバ側が持っている値と一致していないためでした。
それぞれ、以下のような状態です。
- フロント側:ログインしてもHTMLが再読み込みされるわけではないので、headタグ内のCSRFトークンの値は更新されない
- サーバ側:ログインによってトークンが更新される
※Laravel標準の認証機能だと、サーバ側のトークンがログインのタイミングで更新されるようになっていました
何もしないでもトークンを送ってくれるんじゃないの?
となるんですが、実際axiosはCookieから取得したトークンを送ってくれていました。
ただ、Laravel側で参照するトークンの優先順位があり、headタグ内に埋め込んだほうの更新されてないトークンが参照されていたためにエラーとなっていたようです。
LaravelのCSRFトークンの参照の優先順位
優先順位はこうなっています。
高:HTMLのheadタグに埋め込まれたX-CSRF-TOKEN
の値
低:Cookieから取得したX-XSRF-TOKEN
の値(axiosが自動で送信してくれる値)
なので、どちらも送られている場合はX-CSRF-TOKEN
の値が参照され、不一致エラーとなっていました。
こちらのブログの図が非常にわかりやすかったです。ありがとうございます。
http://shirangana.omaww.net/laravel52/laravel%205.2%20csrf%20token%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF
なお、ドキュメントでこの優先順位について記載がなかったので、VerifyCsrfToken
クラスを確認したところそれらしいメソッドがありました。
protected function getTokenFromRequest($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
$token = $this->encrypter->decrypt($header, static::serialized());
}
return $token;
}
そもそも環境によって使い分けるべきなので優先順位とかは本来あまり関係ないのだろうなと思いました。
(更に言うと初めからソースを参照していればそんなに悩まずに済んだかも…!)