クロスドメイン通信を実現するには Access-Control-Allow-Origin だけでは不十分

2014年9月24日 00:57

まず最初に CORS について補足。これは Cross-Origin Resource Sharing の略称で、直訳するとオリジンをまたいだリソースの共有……って、余計に分かりにくいですね。この場合のオリジンというのはブラウザが HTML を取得したサーバのことで、ブラウザが JavaScript などを通じて、オリジン以外のサーバからデータを取得する際の仕組みを意味する。

世間一般(?)的にはクロスドメイン通信などと呼ばれているが、ドメインが違うサーバと Ajax などで通信することに対して、ブラウザは制限をかけている。これは、クロスサイトスクリプティングと呼ばれる攻撃から身を守るためである。よって、jQuery の $.ajax などを使って他のサーバからデータを取ってこようとすると、そのリクエストが投げられた時点でエラーとなる。

そこで前述した CORS である。CORS は W3C がワーキングドラフトとして進めている世界標準のルールであり、今やほとんどのブラウザがこの CORS に(対応方法に多少の違いはあれど)対応している。そして CORS には、クロスドメイン通信を許可する(正確には、ブラウザに許可させる)方法も用意されている。

このへん、ネットで検索すると関連情報がずらずら出てくると思うが、よく知られているのが Access-Control-Allow-Origin というヘッダ。データ送出元のサーバがこのヘッダを「*」で出力すればクロスドメインができる、というものだ。しかしながら、正確にはこの Access-Control-Allow-Origin だけではダメっぽいというか、おそらく Google App Engine に限った話ではないと思うので、そのへんをフォローしておきたい。

Access-Control-Allow-Origin はその名のとおり「許可するオリジン」。なので、通常は許可したいサーバのアドレスを指定する。本来は http://www.mnemonic.co.jp というようにスキーム+ドメインとなるが、ワイルドカードが使えるのでサーバ側のポリシーが許す限りは「*」で問題ないだろう。

許可を求めるべきはサーバアドレスだけではない。リクエストメソッドとリクエストヘッダも含める必要がある。Python でいえば以下の要領でレスポンスヘッダを出力することになる。

self.response.headers['Access-Control-Allow-Origin'] = '*'
self.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
self.response.headers['Access-Control-Allow-Headers'] = '*'

Access-Control-Allow-Headers も本来は 'Authorization' などと具体的に記すべきだろうが、こちらもワイルドカードで問題ないだろう。あと、Access-Control-Allow-Methods に OPTIONS という見慣れないリクエストメソッドがあるのがお分かりだろうか? これは、リソースがサポートしているメソッドを取得するために用いられるメソッドなのだが、どうやら Firefox の場合は POST の際に事前にこの OPTIONS を使ってリクエストを投げるらしいのだ。メソッドによって関数が分けられている場合は OPTIONS のぶんも用意して同様のレスポンスヘッダを出力しないと、Firefox + POST の組み合わせで不都合が生じる(といいつつ、手元では試せていませんが……)。

というわけで、POST で JSON を得るための API、サーバ側のコードはこんな感じ(GAE/Python)。Access-Control-Allow-Methods は個々のぶんだけで構わないようにも思えるが、念のため(GET も含めて)すべて列記しておいた。

import webapp2
import json

class MainHandler(webapp2.RequestHandler):
    def post(self):
        data = {
            # 出力したいデータ
        }
        self.response.headers['Access-Control-Allow-Origin'] = '*'
        self.response.headers['Access-Control-Allow-Headers'] = '*'
        self.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
        self.response.headers['Content-Type'] = 'application/json;charset=utf-8'
        self.response.out.write(json.dumps(data).decode('utf_8'))

    def options(self):
        self.response.headers['Access-Control-Allow-Origin'] = '*'
        self.response.headers['Access-Control-Allow-Headers'] = '*'
        self.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'

app = webapp2.WSGIApplication([
    ('/', MainHandler)
], debug=True)