Rails に XML をボディとするリクエストを JavaScript で送る

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。