CORS(Cross-Origin Resource Sharing)によるクロスドメイン通信の傾向と対策
CORS(Cross-Origin Resource Sharing)って何?
CORS(Cross-Origin Resource Sharing)は、その名の通り、ブラウザがオリジン(HTMLを読み込んだサーバのこと)以外のサーバからデータを取得する仕組みです。各社のブラウザには、クロスドメイン通信を拒否する仕組みが実装されています。これは、クロスサイトスクリプティングを防止するためです。Aというサイトに訪問したのに、Bというサイトに向けて個人情報を送っていたというのは困りますよね。例えば、オリジンから読み込んだHTML内のJavaScriptでJSONデータを読み込むとしましょう。JSONデータが同じサーバにあれば普通に読み込めますが、別のサーバにある場合は読み込めません。まぁ実際のところはJSONPという仕組みを使ってできちゃったりしますが、抜け道的なやり方で使われていました。CORSは、W3Cがワーキングドラフトとして進めている世界標準のルールです。
各ブラウザでCORSがどの程度実装されているか確認してみたいと思います。最近のブラウザであればどれも対応していますね。注意する点は、IE6,7は非対応で、IE8,9は特殊な対応となっている点です。これについては、後で解決策を示します。
S3でCORSの設定ができるようになった
S3でCORSの設定ができるようになって嬉しいことは何でしょうか。例えば、オリジンWebサイトがEC2で動いていたとします。読み込んだHTML内のJavaScriptでは、動的にパラメータを読み込んでいます。CORS設定が無い場合には、EC2を経由してブラウザに返していたりJSONPを使っていましたが、CORS設定があれば、JavaScript内のAjax通信でクロスドメインのS3から情報をダイレクトに取得できます。
逆に、S3をオリジンWebサイトとして運用している場合は、EC2側でCORS設定を行う事で、S3とEC2のドメインが異なっていたとしてもAjax内で呼び出しができるようになります。今まで、S3を起点としたWebページの場合、EC2の動的プログラムを呼び出すにはJSONPを使った方法しか無かったのですが、普通のJSON呼び出しをはじめ、JSP/PHP/Ruby/Python/ASP等を呼び出せるようになりました。さらに、S3はキャッシュサーバとしてCloudFrontのオリジンとしても動きますので、トップページをCloudFrontにして静的コンテンツは全てここから配信し、動的コンテンツをCORSを使って呼び出す事が出来て、各サーバの役割分担がより明確になります。
Amazon S3はストレージサービス
ここで軽くおさらいです。向かうところ敵無しのAmazon S3ですが、ストレージサービスという特性上、動的なプログラムを実行することはできません。主にファイルを保存するために使われています。この保存するという点において、99.999999999%の耐障害性を持っていることと、非常に安価でデータを保存できることから、とりあえずファイルはS3に置いておけば安心だよねとなっています。そんな中、最近では静的なWebサイトをS3で運用できるようになっていて、ストレージサービスの枠を超えた使い方ができるようになって来ています。そして、今回CORSの発表によって、クライアント側でリッチに動くWebアプリからクロスドメインでS3の情報を取得できるようになったわけです。
S3でCORSの設定をしてみる
S3のバケットにCORS設定をしてみたいと思います。まずはバケットの作成から。
CORSの設定はバケットに対して行いますので、バケットのプロパティ画面を開きます。「Add CORS Configuration」と表示されているはずです。
中身を見てみましょう。
以下テキストです。
<CORSConfiguration> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <MaxAgeSeconds>3000</MaxAgeSeconds> <AllowedHeader>Authorization</AllowedHeader> </CORSRule> </CORSConfiguration>
AllowedOriginは、どこからクロスドメインによるアクセスを可能にするか指定します。上記の例ではワイルドカード指定していますので、どこからでもアクセスできる設定になっています。AllowedMethodは、どのようなHTTPメソッドを許可するか指定します。上記の例ではGETメソッドを許可しています。AllowedHeaderは、許可するHTTPヘッダを指定します。他にも細かい設定をすることができますが、基本的にはこれだけでOKです。これらのルールは、CORSRuleという単位で複数記述することができます。例えば、全てのドメインからGETを許可し、特定のドメインからPOSTを許可するといった使い方です。
WebサイトからS3にあるJSONデータをGETする
S3上にWebアプリがホストされていて、読み込んだHTML内のJavaScriptからAjaxでS3のJSONデータを取得する例です。まずはオリジンとなるHTMLとJavaScriptです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>CORS</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script> <script> window.onload=function(){ $.ajax( { type: "get", dataType: "json", url : "http://b-site.s3-website-ap-northeast-1.amazonaws.com/", success : function( json ){ $("#msg").text("success : "+json.result); }, error : function (xhr,text,thrown){ $("#msg").text("error : "+text); } }); } </script> </head> <body> <b id="msg">...</b> </body> </html>
以下はクロスドメインで返されるJSON文字列です。このバケットでは、上記のオリジンからのアクセスをCORS設定で許可しているためクロスドメインで以下のJSON情報を取得できます。Content-Typeをapplication/jsonにしました。
{"result":"CORS supported !!"}
このときのS3のCORS設定は以下です。
<CORSConfiguration> <CORSRule> <AllowedOrigin>http://a-site.s3-website-ap-northeast-1.amazonaws.com</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <AllowedHeader>*</AllowedHeader> </CORSRule> </CORSConfiguration>
WebサイトからEC2へJSONデータをPOSTする
次に、S3にホストされているWebサイトからEC2へJSONオブジェクトをPOSTしてみましょう。以下は、オリジンとなるS3のソースです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>CORS</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script> <script> function doCors(){ $.ajax({ type: "post", dataType: "json", data: {"name":"hogehoge"}, url : "http://ec2-54-248-XXX-XXX.ap-northeast-1.compute.amazonaws.com/", success : function( json ){ $("#msg").append("success : "+json.name+" : "+json.price+" : "+json.timestamp+"<br>"); }, error : function (xhr,text,thrown){ $("#msg").text("error : "+text); } }); } </script> </head> <body> <input type="button" value="CORS Test" onclick="doCors()"><br> <p id="msg"></p> </body> </html>
以下はEC2側のnode.jsのソースです。HTTPレスポンスヘッダにCORS設定を書いています。そして、レスポンスはJSONオブジェクトです。
var http = require('http'), sys = require('sys'), fs = require('fs'); http.createServer(function (req, res) { console.log('accessed'); res.writeHead(200, { 'Content-Type':'application/json; charset=utf-8', 'Access-Control-Allow-Origin':'http://a-site.s3-website-ap-northeast-1.amazonaws.com', 'Access-Control-Allow-Methods':'POST, GET, OPTIONS', 'Access-Control-Allow-Headers':'*' }); res.end('{"name":"bag","price":"1000","timestamp":"'+new Date().toString()+'"}'); }).listen(80); process.on('uncaughtException', function (err) { console.log('uncaughtException => ' + err); });
実行結果は以下になります。
iOS6 Mobile Safariで、Ajax POSTをキャッシュしてしまう問題の対応
前回対策について記事にしましたが、CORSでも問題になりますので対応します。1つ目の地雷ですねw。毎回タイムスタンプを入れるように$.ajaxPrefilterブロックを追加しました。以下、HTMLのソースです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>CORS</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script> <script> function doCors(){ $.ajaxPrefilter(function (options, originalOptions, jqXHR) { if(originalOptions.type.toLowerCase() == 'post'){ options.data = jQuery.param($.extend(originalOptions.data||{}, { timeStamp: new Date().getTime() })); } }); $.ajax({ type: "post", dataType: "json", data: {"name":"hogehoge"}, url : "http://ec2-54-248-XXX-XXX.ap-northeast-1.compute.amazonaws.com/", success : function( json ){ $("#msg").append("success : "+json.name+" : "+json.price+" : "+json.timestamp+"<br>"); }, error : function (xhr,text,thrown){ $("#msg").text("error : "+text); } }); } </script> </head> <body> <input type="button" value="CORS Test" onclick="doCors()"><br> <p id="msg"></p> </body> </html>
これでiOS6 Mobile Safariに対応できました!!
IE 8,9でjQuery CORSするとエラーになる対策
えー、本日2つ目の地雷ですw。IE 8,9では、AjaxでCORSをするとエラーになります。なんじゃそりゃっっ!。実は、正確にはIE 8,9はCORSに対応していません。IE10からの正式対応です。モダンなブラウザでは、AjaxでCORSする際にXMLHttpRequestオブジェクトを使っているのですが、IE 8,9ではXDomainRequestオブジェクトを使います。jQueryでは、XDomainRequestを使っておらず、AjaxでCORSをしようとするとエラーとなります。これを回避するためには大きなIF文を書く必要があってjQueryのスマートさが消えてテンション下がります。そんなあなたにテンションが戻る解決方法をご紹介します。jQueryプラグインであるxdr.jsの登場です。xdr.jsは、jQueryのAjax通信時に内部でIEかどうかの判定処理を行ってXDomainRequestを使ってくれます。
以下は、xdr.jsのソースです。
(function( jQuery ) { if ( window.XDomainRequest ) { jQuery.ajaxTransport(function( s, userOptions, jqXHR ) { if ( s.crossDomain && s.async ) { if ( s.timeout ) { s.xdrTimeout = s.timeout; delete s.timeout; } var xdr; var progressEvent = {lengthComputable: false, loaded: 0, total: 0}; return { send: function( _, complete ) { function callback( status, statusText, responses, responseHeaders ) { xdr.onload = xdr.onerror = xdr.ontimeout = xdr.onprogress = jQuery.noop; xdr = undefined; complete( status, statusText, responses, responseHeaders ); } xdr = new XDomainRequest(); xdr.open( s.type, s.url ); xdr.onload = function() { callback( 200, "OK", { text: xdr.responseText }, "Content-Type: " + xdr.contentType ); }; if ( userOptions.progress ) { xdr.onprogress = function() { progressEvent.loaded = xdr.responseText.length; return userOptions.progress(jqXHR, progressEvent); }; } else { xdr.onprogress = function() {}; } xdr.onerror = function() { callback( 404, "Not Found" ); }; if ( s.xdrTimeout ) { xdr.ontimeout = function() { callback( 0, "timeout" ); }; xdr.timeout = s.xdrTimeout; } xdr.send( ( s.hasContent && s.data ) || null ); }, abort: function() { if ( xdr ) { xdr.onerror = jQuery.noop(); xdr.abort(); } } }; } }); } })( jQuery );
そして、xdr.jsに対応したHTMLが以下です。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>CORS</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script> <script src="xdr.js" type="text/javascript"></script> <script> function doCors(){ $.ajaxPrefilter(function (options, originalOptions, jqXHR) { if(originalOptions.type.toLowerCase() == 'post'){ options.data = jQuery.param($.extend(originalOptions.data||{}, { timeStamp: new Date().getTime() })); } }); $.ajax({ type: "post", dataType: "json", data: {"name":"hogehoge"}, url : "http://ec2-54-248-XXX-XXX.ap-northeast-1.compute.amazonaws.com/", success : function( json ){ $("#msg").append("success : "+json.name+" : "+json.price+" : "+json.timestamp+"<br>"); }, error : function (xhr,text,thrown){ $("#msg").text("error : "+text); } }); } </script> </head> <body> <input type="button" value="CORS Test" onclick="doCors()"><br> <p id="msg"></p> </body> </html>
これなら許容範囲ですね!!
まとめ
今回は、Amazon S3がCORS対応したことをキッカケに、CORSの動作を確認しました。iOS6 MobileSafariがPOSTをキャッシュする問題を回避しました。また、IE 8,9でCORSの動きが異なることから対策としてjQueryを拡張しました。CORSによってWebアプリケーションの世界観が大きく変わります。コンテンツ配信に載せるHTMLとAPIを提供するクラウドサーバ群という構図は、マルチプラットフォーム、マルチデバイス、マルチスクリーン環境のイマドキIT事情にフィットしたソリューションになりそうです。気がかりなのは、ブラウザの実装によって挙動が変わることですが、ポイントを事前に抑えておけば概ね大丈夫かなぁ。今日からあなたもクロスドメイン職人!
参考資料
Cross-Origin Resource Sharing - Working Draft
Amazon Simple Storage Service Developer Guide Enabling Cross-Origin Resource Sharing
Cross-Origin Resource Sharing W3C Working Draft 3 April 2012