CORSへの対応

最終更新日: 2021年7月28日
R8 | R9

CORSとは

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 に反映されます。

注意

URLの末尾に '/' はつけないようにしてください。上の例では http://example01.com/ と記述しないということです。

XMLHttpRequestオブジェクトの操作

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. http://localhost:18921/wagby/ にアクセスし、admin にログオンします。つまり wagbyapp1 にログオンした状態とします。ログオン後、メニュー画面が表示されることを確認します。

8. 同じブラウザ(Google Chrome)を使って http://localhost:18921/wagby/test.html を開きます。

図2 test.htmlを開く

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

10. 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>

CSRFへの対応

Wagby は標準で CookieのSameSite属性にLaxを指定しています。これによって REST API の CSRF 対策を行なっています。

そのため、CORSで許可したサイトからCSRF攻撃があった場合には、Wagbyで防ぐことはできません。この点を踏まえて、CORSで許可するサイトには信頼できるサイトのみを設定するようにしてください。