itochin2の日記(仮)

主に備忘録。Perl、MySQL、Unity、開発管理などについて情報を残していきたい。

ajaxでクロスドメインのAPIを叩く時にやったこと

アプリのweb viiewから、ajaxでクロスドメインAPIを実行しようとして
とても大変な思いをしたので備忘録。

概要

  • APIサーバーから、アプリのWebViewで表示するHTML(文字列)を取得する
  • HTMLの中で別サーバーのJSを読み込む。
  • JSからXMLHttpRequestAPIを実行する。
  • APIはGETとPOSTの2種類。
  • APIサーバーはPerlで、WAFにAmon2を使用。

GETが失敗する件

ログを見ると、OPTIONSリクエストに403を返していた。

What is OPTIONSリクエスト?

プリフライトリクエストという。
リクエストを送信しても安全か?サーバーがリクエストに対応しているか?
ということを調べるために、ブラウザが特定の条件を満たす場合に飛ばす。

特定の条件

以下、HTTP access control (CORS) | MDNより引用。

  • GET または POST 以外のメソッドを使用します。また application/x-www-form-urlencoded、multipart/form-data、または text/plain 以外の Content-Type とともに POST を行う場合、例えば application/xml または text/xml を用いて XMLペイロードをサーバーへ送るために POST を用いるような場合は、リクエストでプリフライトを行います。
  • カスタムヘッダをリクエストに設定します (例えば、X-PINGOTHER のようなヘッダを用いるリクエスト)。

プリフライトリクエストに対応する

以下のモジュールを使った。
Plack::Middleware::CrossOrigin - search.cpan.org

app.psgi
enable 'Plack::Middleware::CrossOrigin',
    origins => '*',
    headers => '*',
    methods => ['GET', 'POST'];

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動いた!
Android 2.3.6 動かない!

Androidェ・・・
ググったらこんな以下のような記事がヒット
Android 2.3 の WebViwe で GET によるクロスドメインリクエストが最初の1回しか成功しない - latest log
webviewでGETはまともに動かないので、JSONPにしろとのこと。

JSONPに対応する

What is JSONP

  • JSでは、クロスドメインにアクセスできない制限がある(同一生成元ポリシー)
  • クロスドメインからJSをダウンロードすることは可能である。
  • よって、クロスドメインから、APIの戻り値が入ったスクリプトを取得することは可能である。

という小細工仕組み。
JSONPで公開しているリソースは、誰からでも参照できてしまうので
機密情報を入れると情報漏えいのリスクがあり、注意が必要。

以下のモジュールを使った。
Plack::Middleware::JSONP - search.cpan.org

app.psgi
enable 'Plack::Middleware::JSONP';

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 403 forbidden
Android 2.3.6 403 forbidden

Androidェ・・・
これは、Amon2がJSONPを拒否していた。
JSON hijacking対策として以下の条件を満たす場合は403。
・GETリクエスト
・X-Requested-Withヘッダが付いていない
Cookie送っている
UserAgentにAndroidがある ←!!

JSONPスクリプトタグからのリクエストなので、ヘッダは付けられない。

他の対策案を考える

JSONPを拒否するのをやめる

せっかくフレームワークレベルで対策しているのに、いけてない感。

POSTでGET

語感だけは楽しそうだけど、いけてない感。

GETするのをやめる

HTMLを返すときに、APIの結果を埋め込めばいいんだ!
そして、埋め込まれた結果を使って、JSでDOMを弄くろう!
(いま考えると、なぜテンプレートを使おうという発想にならなかったのか)

APIの結果をHTMLに埋め込んだ。

こんな感じに。

{"result":{"id":1, "message":"hogehoge"}}

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動かない!
Android 2.3.6 動かない!

htmlに書いたJSに「//」が含まれていた。
そしたら、以降がコメントとみなされて動かなくなってた。

加工済みのテンプレートを返す対応

結局こうなった。

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動いた!
Android 2.3.6 動いた!

紆余曲折あり、なんとかGETできるようになった。
うそ。GETじゃなかった。条件分岐したテンプレを返しただけ。

POSTが失敗する件

Androidだけまた動かない。
OPTIONSで200 OKを返しているが、ブラウザでabortしている。

ここは理由が分からなかったので課題。
OPTIONSを飛ばさないでPOSTする分には動作していたので、
カスタムヘッダを削る&Content-Typeに「application/x-www-form-urlencoded」を指定で対応。

結果

PC 動いた!
iPhone 動いた!
Android 4.1.2 動いた!
Android 2.3.6 動いた!

こいつ、動くぞ!
要件は満たされた。

Access-Controll-Allow-Origin:"*"ってどうなのか

全てのホストからのリクエストを許可する設定はよくないので、Originを指定したいが
Androidのweb viewはリクエストのOriginがnullになっているというやる気のなさ。

しょうがないので、特定のリクエストの時のみ、CORSヘッダを付与するようにした。

まとめ

動いたことに満足しがちだけど、クロスドメインに紐づくセキュリティリスクも
きちんと知ることが重要。
あと、Androidのweb viewでクロスドメインは罠が非常に多かったので
jsをAPIと同じホストに置くのが一番良いのではないかと思う。

参考

http://www.w3.org/TR/cors/
https://developer.mozilla.org/ja/docs/HTTP_access_control
ここを熟読して、CORSとはなんなのかをよく学ぶべき。

http://www.atmarkit.co.jp/ait/articles/0908/10/news087.html
JSONPを使用するまえに熟読して、リスクを知るべき。