Play Framework(2.8)を使って、Ajaxでデータを送信する時の、CSRFトークンのエラーの解決方法を紹介します。
特定のルーティングの時だけCSRFトークンをOFFにするという方法ではなく、ちゃんとCSRFトークンを送信・受信・マッチする方法です。
0. 最初に
通常のTwirlを使ったフォームの作成・送信では、CSRF.formField
を使います。
しかし、Ajaxの場合、このメソッドは使いません。
Ajaxの場合、<input type="hidden" name="csrfToken" ...>
は含めずにフォームを送信します。
1. application.confの設定
まず、htmlのフォームにCSRFトークンを使わないので、cookieにCSRFトークンを登録させます。
application.conf
に、play.filters.csrf.cookie.name
の設定で、CSRFトークンのcookieの名前を設定します。
play.filters.csrf.cookie.name = "csrf_token"
名前は、ここでは例としてcsrf_token
としましたが、他のcookieとダブらなければ何でもいいです。
こうすることで、Playが自動的に、cookieにCSRFトークンを登録してくれるようになります。
2. Ajaxのヘッダ設定
JavaScriptでCookieから、csrf_token
の値を取得して、AjaxのヘッダにCsrf-Token
というフィールド名で埋め込みます、
axios
以下、axiosの場合の例です
window.axios = require('axios');
// cookieから、application.confで指定したcookieのキーから値を取得
let csrfToken = document.cookie.match(new RegExp('(^|)' + 'csrf_token' + '=([^;]+)'))[2]
window.axios.defaults.headers.common = {
"X-Requested-With": "XMLHttpRequest",
"Csrf-Token": csrfToken
};
"X-Requested-With": "XMLHttpRequest"
の部分も必要です。
CSRFトークンの値を入れるヘッダーのフィールドはCsrf-Token
にする必要があります。
あとは、普通にaxiosのpostをするだけです。
axios.post(url, {"foo": "bar"}) // .then ... catch ....
これで、AjaxにCSRFトークンをつけて、送信でき、Play側もそのCSRFトークンを認識してPOSTが受信されるようになります。
jQuery
jQueryの場合は以下のような感じです。
axiosの例は動くことを確認していますが、このjQueryのコードは試してないので、動くかどうかは不明です。ただ、イメージとしてこんな感じですというものです。
window.$ = require('jquery');
// cookieから、application.confで指定したcookieのキーから値を取得
let csrfToken = document.cookie.match(new RegExp('(^|)' + 'csrf_token' + '=([^;]+)'))[2]
$.ajaxSetup({
// ...
// ...
headers: {
"X-Requested-With": "XMLHttpRequest",
"Csrf-Token": csrfToken
},
// ...
// ...
})
Cookieの扱い(CookieをJavaScriptで普段使わない方のために)
JavaScriptの場合、cookieの取得が少し面倒なので、プロトタイプ/クラスか、何かオブジェクトにまとめてしまうと楽です。
以下、クッキー操作のクラスの例です。
こちらのコードは https://stackoverflow.com/questions/10730362/get-cookie-by-name を参考にしています。
export default class Cookie {
static get(key) {
const match = document.cookie.match(new RegExp('(^|)' + key + '=([^;]+)'));
if (match) {
return match[2];
}
return null;
}
static set (key, value) {
document.cookie = key + '=' + value
}
}
// 使用例
// cookie設定
Cookie.set("csrf_token", "1234567890abcdef......")
// cookie取得
let cookieValue = Cookie.get("csrf_token");
cookieは、文字列がただ;
区切りでつながっているだけなので、
-
split
で文字列を分割して配列にして、 -
for
で一致するものを探す
という方法が、読む・書くという点では楽です。get
メソッドに関しては、上記の例はループではなく、正規表現で一気に取得する例です。
set
の方はkey
とvalue
のバリデーションが入っていないので、チームで使う場合はもう少し慎重なコードを書いたほうがいいかもしれません。
ここではシンプルな例として書いてあるだけです。
終わりに
AjaxでのCSRFトークンの送信方法について、知っている人からしたら、どのライブラリやフレームワークを使っても、自然に思いつく方法なのかもしれません。
ただ、私はLaravelを主に使っていて、フレームワークがAjaxのCSRFトークンを自動的に処理していたので、あまり意識することがありませんでした。
Play Frameworkの場合、AjaxのCSRFトークンは自動的ではなく(そもそもJavaScript部分がフレームワークに含まれてない)、個別の設定が必要なため少しつまづきました。
さらに、Play(2.8)のドキュメントには、Twirlでの使用例しかなく、Ajaxの場合の例が書いてありません。
内容的には、「X-Requested-With
とCsrf-Token
というヘッダをAjaxでは使いましょう」ということが書いてあるくらいです。
ネットで検索しても、「Ajaxの場合はCSRFをOFFにさせる」方法の例が多く、根本的な解決方法の具体例も全然見つかりませんでした。
結局、Playのドキュメントを何度も読み、たまに「cookie
からcsrfトークンを取得すればいいんだよ。Play + Angularの場合はそうやってるよ」というような感じの記事を見つけたので、そこからヒントを得て、PlayでAjax送信をする方法がわかりました(何度探しても、それらの記事を再度見つけることができなかったので、参照としてのリンクも貼れませんでした・・・)。