Rails における XML でのリクエスト
Rails に、Content-Type: application/xml で、ボディが XML のリクエストを送ると、コントローラ中では params に展開される。
# リクエスト# ヘッダはいろいろ省略してます
POST /entries HTTP/1.1
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<entry>
<body>New blog entry</body>
</entry>
# EntriesController
def create
p params # => {"entry" => {"body" => "New blog entry"}, ...
...
これは主に ActiveResource のための仕様だろうが、XML によるレコードのエクスポートとインポートを行う目的で、同じインターフェイスを HTML から使いたいと考えた。
JavaScript によるリクエスト
HTML の form で Content-Type を application/xml にすることはできない (よね?) ので、jQuery を使って JavaScript で送る。
// url に URL 文字列、
// xml に XML 文字列が格納されているものとする。
$.ajax({
type: "POST",
url: url,
dataType: "xml", // Accept ヘッダをセットする。レスポンスを XML で受けとるなら必要。なくてもよい。
contentType: "application/xml", // Content-Type を XML にする
data: xml,
success: function(res, type) { alert(res); },
error: function(req, message, error){ alert(error); }
});
しかし、HTTP メソッドが GET 以外の場合、リクエストに含まれる authenticity_token をチェックし、無いか session[:authenticity_token] と一致しないと、session が空になってしまう (※)。
これは具合が悪いので authenticity_token を送信する方法を考える。
※ ApplicationController などで protect_from_forgery メソッドが呼ばれている場合。普通呼ばれてます。
CSRF プロテクショントークンを送る
form_for で作ったフォームには authenticity_token が hidden フィールドで格納されているので、そのまま submit すればよい。また、Ajax 通信でも、JSON で送るなら、送信するオブジェクトに authenticity_token プロパティを付与すればよい。この場合 authenticity_token は meta タグから取得する。
<!-- 最近の Rails (3 以降?) なら application.html.erb の head 要素にこんな記述があるはず -->
<%= csrf_meta_tag %>
<!-- これにより下記のような meta 要素が生成される -->
<meta content="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" name="csrf-token" />
// jQuery でこれを取得する
var csrf_token = $("meta[name=csrf-token]").attr("content");
しかし、XML はルート要素が 1 つと決められているし、ルート要素は params の 1 要素に納められてしまうため、リクエストボディに authenticity_token を含めることはできない。
そこで、authenticity_token をチェックしているコードをみてみる。
# actionpack/lib/action_controller/metal/request_forgery_protection.rb
def verified_request?
!protect_against_forgery? || request.get? ||
form_authenticity_token == params[request_forgery_protection_token] ||
form_authenticity_token == request.headers['X-CSRF-Token']
end
どうやら X-CSRF-Token ヘッダにトークンを書いてもいいらしい。
jQuery.ajax は beforeSend オプションで、XMLHttpRequest オブジェクトを変更できるので、XMLHttpRequest.setRequestHeader() を使って X-CSRF-Token ヘッダを追加する。
var csrf_token = $("meta[name=csrf-token]").attr("content");
$.ajax({
type: "POST",
url: url,
dataType: "xml", // Accept
contentType: "application/xml",
beforeSend: function(xhr) {
// X-CSRF-Token ヘッダのセット
xhr.setRequestHeader("X-CSRF-Token", csrf_token)
},
data: xml,
success: function(res, type) { alert(res); },
error: function(req, message, error){ alert(error); }
});
これで OK。