XMLHttpRequest Level 2 + CORSで通信する時のメモ
おぼろげながら知識はあったんですが、今回ちょっと詰まってしまって、実際に自分で設定してみて上手く行ったのをメモとして残しておきます。
はじめに
XMLHtteRequest Level 2(以下、XHR)では外部ドメインへのリクエストが可能なのですが、そのリソースはCORS(Cross Origin Resource Sharing)の制限を受けてしまいます(IE8とかだとXDomainRequest使いますね)。 この時、リクエストを受けるサーバ側では、CORSのリクエストを許可するヘッダと共にレスポンスを返す必要があります。
また、GETリクエスト以外、かつMimeTypeがtext/plain以外の場合はプリフライトなリクエストが送られ、先に安全な通信が可能かどうかをブラウザがチェックするようです。 このあたりの仕組みをよくわかってなかったのでまとめます。CORSについてはMDNのサイトが一番詳しいと思います。
大体はこのサイトで分かるんですが、実際にやってみないとわかんなかったので。
リクエストのサンプル
XHRでのリクエストはだいたい以下のような感じ。
var xhr = new XMLHttpRequest();
xhr.onload = function() {
console.log('Success: ' + xhr.responseText);
};
xhr.onerror = function() {
console.log('Error: ' + xhr.responseText);
};
xhr.open('GET'undefined 'http://external.example.com/data.json'undefined true);
xhr.setRequestHeader('X-Requested-With'undefined 'XMLHttpRequest');
xhr.send(null);
リクエスト元はhttp://example.comとして、サブドメインとはいえ外部ホストのデータを参照するようなケースです。
プリフライトする条件
前述のMDNには以下のように書いてあります:
GET または POST 以外のメソッドを使用します。また application/x-www-form-urlencoded、multipart/form-data、または text/plain 以外の Content-Type とともに POST を行う場合、例えば application/xml または text/xml を用いて XML のペイロードをサーバーへ送るために POST を用いるような場合は、リクエストでプリフライトを行います。
カスタムヘッダをリクエストに設定します (例えば、X-PINGOTHER のようなヘッダを用いるリクエスト)。
Chromeで試してみましたが、プリフライトのリクエストメソッドはOPTIONSでした。
Accell-Control-Allow-XXXなヘッダ
サーバ側はCORSに関するヘッダを付与してレスポンスしますが、最低限必要なのが、
Access-Control-Allow-Origin: [アクセス元のURL、または*とか]
Access-Control-Allow-Headers: [受け入れるヘッダ]
あたりですね。他にもAccess-Control-Allow-Methodsとかもありますが、厳密な設定をする場合は適宜設定する必要があるかと。 今回は静的なリソースを外部ドメインから取得するケースで考えます。なので、基本的にGETメソッドです。
XHR側でカスタムヘッダをつけるとプリフライトする
上述の仕様の通り、X-Requested-WithみたいなカスタムヘッダをつけるとGETリクエストでもプリフライトします。X-Requested-WithはAjaxリクエストの判定に使ったりするのでできたら付与したいので、その場合は、
``` Access-Control-Allow-Origin: * Access-Control-Allow-Headers: X-Requested-With ```とヘッダをつけて許可すればOKでした。なお、AungularJSがどこかのバージョンからX-Requested-Withを付与しなくなったみたいですが、不用意なプリフライトが起こらないようにするためみたいですね。
参考:AngularJS $resource not sending X-Requested-With - Stack Overfow
BASIC認証を含む場合
BASIC認証がかかっている場合は、クレデンシャルを許可し、なおかつAuthorizationヘッダを付与してあげるとパスします。リクエスト側は、
var xhr = new XMLHttpRequest();
xhr.onload = function() {
console.log('Success: ' + xhr.responseText);
};
xhr.onerror = function() {
console.log('Error: ' + xhr.responseText);
};
xhr.open('GET'undefined 'http://external.example.com/data.json'undefined true);
xhr.withCredentials = true;
xhr.setRequestHeader('X-Requested-With'undefined 'XMLHttpRequest');
xhr.setRequestHeader('Authorization'undefined 'Basic [base64_encoded_string]');
xhr.send(null);
って感じでwithCredentials = true;とAuthorizationヘッダをつけます。サーバ側は、
# クレデンシャルを許可する場合は*が使えない
Access-Control-Allow-Origin: http://localhost
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: X-Requested-Withundefined Authorization
って感じで、クレデンシャルを許可する指定と、Authoizationヘッダも通すようにしておかないといけないっぽい。上の通り、クレデンシャルを許可するときはAccess-Control-Allow-Originに*は使えないので注意。 これでBASIC認証付きでもX-Requested-With付きでリクエストの送受信ができました。
設定例(Apache)
Apacheの場合はこんな感じで設定したら通りました。
<ifmodule headers_module>
Header append Access-Control-Allow-Origin: http://localhost
Header append Access-Control-Allow-Credentials: true
Header append Access-Control-Allow-Headers: "X-Requested-Withundefined Authorization"
</ifmodule>
http://localhostの部分は環境に合わせて。なお、Apacheの場合は、プリフライトのOPTIONSリクエストの場合、何もしなくても200 OKが返ってました。なんでかわからない
設定例(Nginx)
Nginxの場合はApacheのHeader appendに対応するのがないのかな?で、more_set_headersっていう拡張機能を使うので、yumとかで入れたnginxだとちょっと厳しかった。なので別途コンパイルして入れました。add headerだとなぜか上手くいかないので、more_set_headersの方がいいと思います。。
$ wget https://github.com/openresty/headers-more-nginx-module/archive/v0.25.tar.gz
$ tar xvfz v0.25.tar.gz
$ cd nginx-v1.7.0
$ ./configure --add-module=/path/to/headers-more-nginx-module-0.25
$ make
$ sudo make install
これでmore_set_headersが使えるようになるので、以下のように設定しました(location /のところ抜粋)。
location / {
root /var/www/nginx;
index index.html index.htm;
if ($request_method = 'OPTIONS') {
more_set_headers 'Access-Control-Allow-Origin: *';
more_set_headers 'Access-Control-Allow-Credentials: true';
more_set_headers 'Access-Control-Allow-Headers: X-Requested-Withundefined Authorization';
return 204;
}
if ($request_method = 'GET') {
more_set_headers 'Access-Control-Allow-Origin: *';
more_set_headers 'Access-Control-Allow-Credentials: true';
more_set_headers 'Access-Control-Allow-Headers: X-Requested-Withundefined Authorization';
}
}
Nginxの場合、プリフライトのOPTIONSリクエストの場合はそのままだと405を返してしまうようなので、return 204;として、明示的に204 No Contentを返却しないとブラウザ側はリクエストに失敗してしまったかのように扱ってしまうので注意。 いつも思うけどNginxのconfファイルにif文書くのすごいなーって思う…。
POSTリクエストとかは?
そもそもPOSTするということは何らかのサーバーサイドのプログラムが動いているはずなので、そのプログラムから動的にヘッダを付与すればOKですかね。試してませんが。
まとめ
ここでの設定例は、配信サーバすべてに適用するような設定なので、細かいパスやファイルごとの制御はちゃんとしないといけないかも。他のAccess-Control-Allow-XXXを併用してリクエストメソッドを限定したり。 Nginxでは204を返さないといけないくらいですかね。注意する点は。
現場からは以上です。