Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

JavaScript Ajax : XmlHttpRequestのLevel2でSameOriginPolicyを回避する

クロスドメイン制限の回避について

  • 今まではXHR(XmlHttpRequest)の仕様によりJSを読み込んでいるHTMLファイルがあるサーバから異なるドメインサーバへのAjaxリクエストが制限されていました。Same Origin Policyと呼ばれているものです。Same Origin Policyの役割としては悪意のあるscriptが個人情報等を他のサイトに転送する事を防ぐためです。このセキリティ制限を回避するために多くの人が代表的なJSONP(JSON with Padding)を利用してサーバサイドでクライアントのコールバック関数をechoしてクライアント側で実行されることにより、クロスドメイン間のAjax通信をそれっぽく動くように対応していたと思います。
  • JSONPについては以前記事を書いたので宜しければ参照してください。20秒で理解するJSONP - Yuta.Kikuchiの日記 はてなブックマーク - 20秒で理解するJSONP - Yuta.Kikuchiの日記
  • JSONPを利用するのは少し面倒でコールバック関数をリクエストされたサーバサイドから返却しないといけなく、またセキュリティ面でも不安で十分に気をつける必要があります。JSONP以外にもリバースProxyを用意する、Flashを経由するとクロスドメインリクエスト可能となりますがこの面倒な事に頭を悩ませるJavascripterも多かったと思うのですが、色々と調べてみるとXHRのLevel2の仕様によりJSONPを使わずとも異なるドメイン間でのAjaxリクエストが可能となりそうです。

制限を回避するために

Access-Control-Allow-Origin
  • HTTP access control - MDN はてなブックマーク - HTTP access control - MDNにあるようにAccess-Control-Allow-OriginというHttpResponseHeaderを仕込むとクロスドメインを回避する事ができるようです。このHttpResponseHeaderをどこに仕込むのか?ということが疑問になりそうですが、Ajaxリクエスト先のAPIサーバに設置します。(リクエスト元に設定する方式だとXSRFなどが好き勝手出来てしまうのでそれは無いですね)
  • Access-Control-Allow-Originにリクエストを受け付けるURLを指定、もしくは*(ワイルドカード)を指定すると全てのURLを受け付ける設定になります。
関連Header一覧
  • 色々なブラウザ仕様があるのでAccess-Control-Allow-Originだけでなく、念のため以下のResponseHeaderも設定しておくと動作が確認できるようです。
    • Access-Control-Allow-Origin : 上で説明した通りでアクセス元のURLを指定します。*(ワイルドカード)指定可能です。
    • Access-Control-Allow-Methods : GET,POST,PUT,DELETE,OPTIONSなどの受け付けるRequestMethodを指定します。
    • Access-Control-Allow-Headers : RequestHeaderに仕込んである値を見て許可する内容を指定します。*(ワイルドカード)指定可能です。
    • Access-Control-Max-Age : 各種OptionHeaderの有効時間を設定します。

Demo

今回のAPI/ClientともにサンプルコードをGitHubに挙げました。https://github.com/yutakikuchi/JS/tree/master/crossdomain

API Server
  • アクセスするAPIサーバをGoogle App Engineに置きます。上で説明したHeaderを仕込み、とりえあずは簡単な文字列(CrossDoaminRequest)だけをprintします。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.ext.webapp import template, util

class CrossDomain(webapp.RequestHandler):
  def get(self):
    self.response.headers[ 'Content-Type' ] = 'text/plain'
    self.response.headers[ 'Access-Control-Allow-Origin' ] = '*' 
    self.response.headers[ 'Access-Control-Allow-Methods' ] = 'GET'
    self.response.headers[ 'Access-Control-Allow-Headers' ] = '*' 
    self.response.headers[ 'Access-Control-Allow-Age' ] = '86400'
    self.response.out.write( 'CrossDoaminRequest' )

def main():
  application = webapp.WSGIApplication([('/CrossDomain', CrossDomain)])
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()
API出力Header
http://mobiles-proxy.appspot.com/CrossDomain

GET /CrossDomain HTTP/1.1
Host: mobiles-proxy.appspot.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:8.0.1) Gecko/20100101 Firefox/8.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Connection: keep-alive

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: text/plain
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET 
Access-Control-Allow-Headers: *
Access-Control-Allow-Age: 86400
Expires: Fri, 01 Jan 1990 00:00:00 GMT 
Content-Encoding: gzip
Vary: Accept-Encoding
Date: Sat, 25 Feb 2012 08:29:03 GMT 
Server: Google Frontend
Content-Length: 38
Request Client
  • IEでも動くようにXDomainRequestとXMLHttpRequestをそれぞれ使い分けるようなコードを書きます。色々と試してみたところクライアント側でAPI ServerのResponseを取得した時にAccess-Control-Allow-Originの値にて許可されたサイトであることを判定しているようです。
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
<title>CrossDomainAjax</title>
<script>
var Request = function() {
    var url = 'http://mobiles-proxy.appspot.com/CrossDomain';
    var XHR;
    // IE対策
    if( window.XDomainRequest ) {
        XHR = new XDomainRequest();
        XHR.onload = function(){ 
           echo( XHR.responseText )
        };
        XHR.open( 'GET', url );
        XHR.send();
    // IE以外
    } else {
        XHR = new XMLHttpRequest();
        XHR.onreadystatechange = function(){
            if( XHR.readyState === 4 && XHR.status == 200 ) 
                echo( XHR.responseText );
            };
        XHR.open( 'GET', url, true );  
        XHR.send();
    }
}
var echo = function( text ) {
    alert( text );
}
</script>
</head>
<body>
<input type='button' name='ajax' value='CrossDomainRequest' onClick='javascript:Request();' />
</body>
</html>
実行検証
  • いくつかのBrowserで動作確認をします。環境がMacなのでIEは試していません。
  • 結果SafariAccess-Control-Allow-Originが無くても実行が出来てしまいました。またOperaは動作しませんでした。
Browser Access-Control-Allow-Origin Access-Control-Allow-Methods Access-Control-Allow-Headers Access-Control-Allow-Age
Safari ver 5.1.2 不要 不要 不要 不要
Chrome ver 17.0.963.56 不要 不要 不要
FireFox ver 10.0.2 不要 不要 不要
Opera ver 11.61 - - - -

まとめ

  • JSONPを用いなくてもXHR Level2を利用するとCrossDomainRequestが可能です。
  • XHR Level2を利用するにはAccess-Control-Allow-OriginというResponseHeaderをアクセス先のサーバが返す必要があります。
  • IEで動作させるにはXDomainRequestオブジェクトを生成します。
  • 各種ブラウザによって挙動が異なり、SafariAccess-Control-Allow-Originさえ無くても動き、Operaは未対応です。
  • 今回の検証サンプルコードを以下に設置しました。https://github.com/yutakikuchi/JS/tree/master/crossdomain