REST API を CORS に対応させるための手順を説明します。R8.1.2

CORS(Cross-Origin Resource Sharing)とは、WebブラウザがHTMLを読み込んだ以外のサーバからデータを取得する仕組みです。

利用者のWebブラウザが、あるWebアプリケーションを使っているが、Wagbyアプリケーション(REST API提供)からAjaxでデータを取得したいという場合があります。このとき、WagbyアプリケーションをCORSに対応させる必要があります。

図1 CORSのイメージ

技術の詳細

CORSはREST APIを提供しているサーバ(例 http://localhost:8921)が異なるドメインからの呼び出しについて、どのように対応するかをHTTPヘッダにて返す仕組みです。詳細は下記をご覧ください。

稼働するブラウザ

IE10以上、Google Chrome、Firefoxなどが対応しています。詳細は下記URLをご覧下さい。

IE9はPartially supportedとなっていますが、withCredentialsの指定によるCookieの扱いに対応していないため、未対応となります。

ブラウザへの設定

3rd party cookieを許可してください。

IE 11
http://windows.microsoft.com/ja-jp/internet-explorer/delete-manage-cookies#ie=ie-11
IE 10
インターネットオプションのセキュリティタブにて(図1にある)「A:オリジン」をローカルイントラネットに設定する。
Google Chrome
https://support.google.com/chrome/answer/95647?hl=ja
Firefox
http://support.mozilla.org/ja/kb/disable-third-party-cookies

適切な設定を行わない場合、JavaScriptエラーが発生するなどで動作しません。

Wagby に同梱されている Spring Security の CORS 設定を有効にします。

設定方法

myapplication.properties に次のように記載します。アクセスを許可する URL を指定します。

wagby.security.corsAllowedOrigins[0]=http://example01.com
wagby.security.corsAllowedOrigins[1]=http://example02.com
...

変更したファイルを customize/resources フォルダに保存します。ビルド時にこの内容が含まれます。

wagbyapp/webapps/<プロジェクト識別子>/WEB-INF/classes/application.properties に反映されます。

JavaScriptコードにてAjax対応コードを作成します。XMLHttpRequest オブジェクトを用意します。

httpObj = new XMLHttpRequest();

このオブジェクトに対して、クッキーを用いることを指定します。

httpObj.withCredentials = true;

その後、通信を行います。

httpObj.send(/*パラメータ*/);
具体的なAjaxコードの書き方は割愛します。

CORS の動作を確認するテストコードを用意しました。

1. customize/resource/myapplication.properties を用意します。次の内容とします。

wagby.security.corsAllowedOrigins[0]=http://localhost:18921

2. プロジェクト識別子 wagby (デフォルト) でビルドします。

3. ビルドされた wagbyapp をコピーした、wagbyapp1 を用意します。この時点で二つのアプリケーション wagbyapp, wagbyapp1 が存在します。

4. wagbyapp1/conf/server.xml を手動で編集します。変更する場所は以下のとおりで、ポート番号に 10000 を加算したものとなります。

...
<Server port="18005" shutdown="SHUTDOWN">
...
  <Connector port="18921" protocol="HTTP/1.1"
...
    enableLookups="false" redirectPort="18443" acceptCount="100"
...
  <Connector port="18009" protocol="AJP/1.3" redirectPort="18443"
...

5. wagbyapp1/webapps/wagby/に test.html (後述) をコピーします。この HTML に書かれている JavaScript では、http://localhost:8921にログオンし、jnewsの一覧を取得するREST APIを呼び出すようになっています。

6. wagbyapp, wagbyapp1を起動する。ポート番号を書き換えたため、二つのアプリケーションを起動することができます。(ここで起動エラーとなった場合、ポート番号が重複しています。上の設定ファイルを見直してください。)

7. Google Chromeで http://localhost:18921/wagby/test.html を開きます。

図2 test.html を開く

8. アクセスした画面で Chrome のデベロッパーツールを開き、Networkタブを開きます。

9. test.htmlの送信ボタンを押します。成功すると、取得されたJSONオブジェクトのテキストが画面に表示されます。

図3 JSONの値が表示された

また、デベロッパーツールで、CORSフィルタに対応する Access-Control-... のレスポンスヘッダを確認することができます。

図4 デベロッパーツールでの確認
cors.allowed.origins の設定が間違っている場合、デベロッパーツールの JavaScript コンソールに下記のようなエラーメッセージが表示されます。
"Failed to load http://localhost:8921/wagby/rest/session: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:18921' is therefore not allowed access."

test.html

ここで用いた test.html は次のとおりです。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Wagby get jnews rest test</title>
<script type="text/javascript" src="system/common.js" charset="UTF-8"></script>
<script type="text/javascript">
function createXMLHttpRequest() {
    var httpObj;
    try {
        if(window.XMLHttpRequest) {
            httpObj = new XMLHttpRequest();
        } else if(window.ActiveXObject) {
            httpObj = new ActiveXObject("Microsoft.XMLHTTP");
        } else {
            httpObj = false;
        }
    } catch(e) {
        httpObj = false;
    }
    return httpObj;
}
function httpXMLRequestText(
    httpObj, target_url, param, method, functionReference, failedFunctionReference)
{
    // タイマーが動作中の場合は、タイマーをストップする。
    if (timerId != '') {
        clearInterval(timerId);
        timerId = '';
    }
    // HTTPリクエストを送信中の場合は、HTTPリクエストを中断する
    if (httpObj != false) {
        httpObj.abort();
        httpObj = false;
    }
    // 処理失敗時の関数参照を格納する
    failedFuncRef = failedFunctionReference;
    // httpObjの作成
    try {
        if(window.XMLHttpRequest) {
            httpObj = new XMLHttpRequest();
        } else if(window.ActiveXObject) {
            httpObj = new ActiveXObject("Microsoft.XMLHTTP");
        } else {
            httpObj = false;
        }
    } catch(e) {
        httpObj = false;
    }
    if(! httpObj) {
        // httpObjの作成に失敗した場合
        alert('not supported your web browser!!');
        failedFuncRef();
        return false;
    }
    // タイマーをセット
    timerTimeoutSec = httpXMLRequest_DefaultTimerTimeoutSec;
    timerBeginTime = new Date().getTime();
    timerId = setInterval('timeoutCheck()', 1000);

    // HTTPリクエストを送信
    httpObj.open(method, target_url, true);
    httpObj.onreadystatechange = function() {
        if (typeof(httpObj) != 'undefined' && httpObj.readyState == 4) {
            // タイマーをストップする
            if (timerId != '') {
                clearInterval(timerId);
                timerId = '';
            }
            var timerEndTime = new Date().getTime();

            if (httpObj.status == 200) {
                // リクエストの受信に成功した場合
                functionReference(httpObj);
                if (httpXMLRequest_IsShowProcessTime) {
                    alert('process time '+(timerEndTime-timerBeginTime)+'ms');
                }
                httpObj = false;
            } else {
                // リクエストの受信に失敗した場合
                failedFuncRef();
                var alertmsg = 'httpXMLRequest '+httpObj.status + ' : ' + httpObj.statusText;
                if (httpXMLRequest_IsShowProcessTime) {
                    alertmsg =
                        alertmsg + '\n' +
                        'process time '+(timerEndTime-timerBeginTime)+'ms';
                }
                window.status = alertmsg;
                httpObj = false;
                return false;
            }
        }
    }
    if (param != '') {
        httpObj.setRequestHeader(
            'Content-Type', 'application/x-www-form-urlencoded');
    }
    httpObj.withCredentials = true;
    httpObj.send(param);
    return true;
}

function clear(elem) {
    var childs = elem.childNodes;
    for (i=0;i<childs.length; i++) {
        elem.removeChild(childs[i]);
    }
}
function failedCall(httpObj) {
    alert("failed call "+httpObj.status + ' : ' + httpObj.statusText);
}
function clearAll() {
    var jsonstrelem = document.getElementById("jsonstr");
    clear(jsonstrelem);
    var elem = document.getElementById("getjnews_return");
    clear(elem);
}
function appendResponseTable1(jsData, elem) {
    var jsonstrelem = document.getElementById("jsonstr");
    clear(jsonstrelem);
    jsonstrelem.appendChild(document.createTextNode(jsData));
    clear(elem);
    elem.appendChild(document.createTextNode(jsData));
}
</script>
</head>
<body>
<p>
<input type="button" value="クリア" onclick="clearAll()"/><br>
JSON文字列:
<input type="button" value="表示" onclick="document.getElementById('jsonstr').style.display='block'"/>
<input type="button" value="非表示" onclick="document.getElementById('jsonstr').style.display='none'"/><br>
<div id="jsonstr" style="display:none">なし</div>
</p>

<script language="JavaScript">
function callGetJnews() {
    var target_url = "http://localhost:8921/wagby/rest/session";
    // R7
    /*
    var param = "user=admin&pass=admin";
    httpXMLRequestText(
            createXMLHttpRequest(),
            target_url, param, 'PUT', successCallLogon,
            failedCall);
    */
    // R8
    var param = "user=admin&pass=wagby";
    httpXMLRequestText(
            createXMLHttpRequest(),
            target_url, param, 'POST', successCallLogon,
            failedCall);
}
function successCallLogon(httpObj) {
    var target_url = "http://localhost:8921/wagby/rest/jnews/list";
    var param = "";
    httpXMLRequestText(
            createXMLHttpRequest(),
            target_url, param, 'GET', successCallGetJnews,
            failedCall);
}
function successCallGetJnews(httpObj) {
    var elem = document.getElementById("getjnews_return");
    clear(elem);
    appendResponseTable1(httpObj.responseText, elem);
}
</script><p>
GET jnews/list:
返り値:<div id="getjnews_return">なし</div>
<input type="button" value="送信" onclick="callGetJnews()"/>
</p>
</body>
</html>
Wagby Developer Day 2018