今回は久しぶりにWebネタです。
HTTPのページからHTTPSを使ってJavaScriptでサーバと通信するのは特に問題無さそうにも思えますが、普通にAjaxを使おうとしても拒否されてしまいます。
<a href="#" id="test">test</a>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
$("#test").click(function() {
$.ajax({
type: "POST",
url: "https://〜/hello.php",
data: "name=taro",
success: function(msg) {
alert(msg["msg"]);
},
error: function(req, sts, err) {
alert("Error! sts=" + sts);
}
});
});
});
</script>
<?php
header('Content-type: application/json; charset=UTF-8;');
echo '{"msg":"Hello, ' . urlencode($_POST['name']) . '"}';
?>
上のスクリプトをchromeで実行すると、デバッグコンソールに「XMLHttpRequest cannot load https://〜/hello.php. Origin http://〜 is not allowed by Access-Control-Allow-Origin. 」といったメッセージが表示されてエラーになります。同じドメインからの通信であってもスキームが違う(httpとhttps)だけでクロスドメインと認識されてしまうようです。
でもこれは最近のブラウザではCORS(Cross-Origin Resource Sharing)を使うことで回避できます。
CORSに対応するには、以下のような通信を許可するHTTPヘッダを返すようにします。
Access-control-allow-origin: http://〜
http://〜の部分は通信を許可するドメインを指定します。制限しない場合は、以下のように*(アスタリスク)を指定します。
Access-control-allow-origin: *
CORSに対応するためサーバ側のPHPスクリプトを以下のように修正すれば通信が成功します。
<?php
header('Access-control-allow-origin: http://〜'); // 通信を許可する接続元ドメインか*(アスタリスク)を指定
header('Content-type: application/json; charset=UTF-8;');
echo '{"msg":"Hello, ' . urlencode($_POST['name']) . '"}';
?>
ただ、このやり方でもIE8やIE9ではうまくいきません。(IE10以降はOK)
IE8とIE9自体はCORSに対応している(IE7以下は未対応)のですが、XMLHttpRequestオブジェクトではなく、IE独自のXDomainRequestオブジェクトを使用しないといけません。 ┐(´ー`)┌
そのために、JQueryで使用できるxdr.jsというプラグインがあるのですが、これを使っても、異なるドメイン間の通信は許可できますが、スキームが違うとうまくいきません。
この解決方法が、http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspxにありました。
具体的にはiframeを使ってhttpsのページを読み込みそこから通信を行います。フレーム間はpostMessageを使ってやり取りします。
実際にやってみます。
<a href="#" id="test">test</a>
<iframe style="display:none;" id="oProxy" src="https://〜/proxy.html"></iframe>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
$(window).on("message", function(e) {
var obj = $.parseJSON(e.originalEvent.data);
alert(obj.msg);
});
$("#test").click(function() {
var data = '{"url":"https://〜/hello.php", "params":{"name":"taro"}}';
try {
oProxy.postMessage(data, 'https://〜'); // 第2引数は'*'でも可
} catch(e) {
alert("Error");
}
});
});
</script>
フレームはスタイルを設定して非表示にしています。フレームにpostMessageでメッセージを送ります。フレームに渡すデータは自由に決められますがここでは、送信先のURLと送信内容を渡しています。わざわざJSON形式にしないで直接オブジェクトを渡してもいいのですが、IE8の場合に、オブジェクトをpostMessageで渡してもうまくデータがとれなかったのでここではJSON形式で渡すようにしました。 ┐(´ー`)┌
また、フレームからのレスポンスを待つため、messageのイベントリスナーを登録しています。
フレームのソースは次のような内容になります。
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
var outerPagePtr;
var outerPageOrigin;
function doXDR(url, data) {
var xdr = new XDomainRequest();
xdr.onload = function()
{
outerPagePtr.postMessage(xdr.responseText, outerPageOrigin);
}
xdr.open("POST", url);
dxr.send(data);
}
$(window).on("message", function(e) {
outerPagePtr = e.originalEvent.source;
outerPageOrigin = e.originalEvent.origin;
data = $.parseJSON(e.originalEvent.data);
doXDR(data.url, data.params);
});
});
</script>
親ウィンドウからのメッセージを受け取り、XDomainRequestオブジェクトを生成してサーバへ送信しています。onloadのコールバックで結果が受け取れるので、結果を親フレームへpostMessageで返しています。
XDomainRequestは、IEにしか存在しないオブジェクトなので、実際使用する時にはブラウザのバージョンやXDomainRequestオブジェクトの有無を確認して処理を分岐することになると思います。
サーバ側も少し変更が必要です。
<?php
if (isset($HTTP_RAW_POST_DATA)) {
parse_str($HTTP_RAW_POST_DATA, $_POST);
}
header('Access-Control-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']);
header('Content-type: application/json; charset=UTF-8;');
echo '{"msg":"Hello, ' . urlencode($_POST['name']) . '"}';
?>
XDomainRequestからPOSTメソッドで送信されたデータは、phpの$_POSTに設定されません。そのため$HTTP_RAW_POST_DATAの内容を$_POSTに設定するようにしています。
また、許可するドメインも、http、httpsどちらのページからアクセスされても応答できるようにするには、http://〜とhttps://〜の両方をAccess-Control-Allow-Originに設定する必要がありました。ここでは便宜的に、$_SERVER['HTTP_ORIGIN']を設定しています。(この辺の理由はちょっとよく分りませんでした。同じスキーム同士なら設定無しで許可してくれてもよさそうなものですが...)
かなり、面倒臭いですね...IE9以下の古いブラウザが早く根絶されることを願うばかりです。IE8の方はWindowsXPのサポート終了に伴い大分進むとは思うのですが。
もう1つこのような場合に利用できる方法があります。JSONPを使う方法です。
JSONPは、外部のJavaScriptの読み込みに関してはクロスドメインの制限がかからないことを利用して、JavaScriptで<SCRIPT>タグを生成しサーバとデータをやり取りします。
scriptタグのsrc属性に、URLと渡したいパラメータを付加するわけですが、この時に結果を処理する関数名も渡して、サーバ側でその関数がクライアントで実行されるように記述します。
jQueryを使わない場合は、document.createElementで、<script>ノードを生成して、サーバ呼び出し用のURLをsrc属性に設定することになりますが、jQueryを使うとJSOPの記述も簡単にできます。
<a href="#" id="test">test</a>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
$("#test").click(function() {
$.ajax({
url:"https://〜/hello.php",
data:{name:"taro"},
dataType:'jsonp',
success:function(json) {
alert(json.msg);
}
});
});
});
通常のAjax呼び出しと同様の記述ですが、dataTypeに'jsonp'を指定しています。この処理が呼ばれると、サーバ側には、https://〜/hello.php?callback=jQuery19100190601888772117332_1375546920618&name=taro
といったURLでリクエストが送られます。callbackパラメータの長い名前は、jQueryが生成した一時的な関数名です。
サーバ側もJSOPで返すため修正が必要です。
<?php
header('Content-type: text/javascript; charset=UTF-8;');
echo $_GET['callback'] . '({"msg":"Hello, ' . urlencode($_GET['name']) . '"});';
?>
JSOPの場合はCORS用のhttpヘッダは不要です。Content-typeは、application/jsonではなく、test/javascriptになります。返す内容もjavaScriptであり、callbackパラメータとして渡された関数を、結果の引数を渡して呼び出す処理になります。クライアント側でこの処理が呼ばれることになります。
jQuery19100190601888772117332_1375546920618({"msg":"Hello, trao"});
jQueryが最終的にこの呼び出しをsuccessハンドラへ結びつけてくれます。
こうみると、クロスサイトリクエストへの対応はJSONPを使ったやり方が最も簡単で汎用的に思えますが、CORSのヘッダによるアクセスの制限ができず、データの受け渡しもGETメソッドのみになるため、セキュリティ的には最も脆弱で、JSONPで機密情報や個人情報のデータを扱うのは不適切とされているようです。
全てのブラウザがCORSに対応してくれればそれが一番良いのですが。この際、IE10未満はサポート対象から切り捨てるか…