CORSへの対応
最終更新日: 2021年7月28日
R8 | R9
CORS(Cross-Origin Resource Sharing)とは、WebブラウザがHTMLを読み込んだ以外のサーバからデータを取得する仕組みです。
利用者のWebブラウザが、あるWebアプリケーションを使っているが、Wagbyアプリケーション(REST API提供)からAjaxでデータを取得したいという場合があります。このとき、WagbyアプリケーションをCORSに対応させる必要があります。
CORSはREST APIを提供しているサーバ(例 http://localhost:8921)が異なるドメインからの呼び出しについて、どのように対応するかをHTTPヘッダにて返す仕組みです。詳細は下記をご覧ください。
IE10以上、Google Chrome、Firefoxなどが対応しています。詳細は下記URLをご覧下さい。
IE9はPartially supportedとなっていますが、withCredentialsの指定によるCookieの扱いに対応していないため、未対応となります。
3rd party cookieを許可してください。
適切な設定を行わない場合、JavaScriptエラーが発生するなどで動作しません。
Wagby に同梱されている Spring Security の CORS 設定を有効にします。
myapplication.properties に次のように記載します。アクセスを許可する URL を指定します。
変更したファイルを customize/resources フォルダに保存します。ビルド時にこの内容が含まれます。
URLの末尾に '/' はつけないようにしてください。上の例では http://example01.com/ と記述しないということです。
JavaScriptコードにてAjax対応コードを作成します。XMLHttpRequest オブジェクトを用意します。
このオブジェクトに対して、クッキーを用いることを指定します。
その後、通信を行います。
CORS の動作を確認するテストコードを用意しました。
1. customize/resource/myapplication.properties を用意します。次の内容とします。
CORSとは
技術の詳細
対応ブラウザ
稼働するブラウザ
ブラウザへの設定
設定ファイル
設定方法
wagby.security.corsAllowedOrigins[0]=http://example01.com
wagby.security.corsAllowedOrigins[1]=http://example02.com
...
注意
XMLHttpRequestオブジェクトの操作
httpObj = new XMLHttpRequest();
httpObj.withCredentials = true;
httpObj.send(/*パラメータ*/);
簡易テスト
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 を開きます。
9. アクセスした画面で Chrome のデベロッパーツールを開き、Networkタブを開きます。
10. test.htmlの送信ボタンを押します。成功すると、取得されたJSONオブジェクトのテキストが画面に表示されます。
また、デベロッパーツールで、CORSフィルタに対応する Access-Control-... のレスポンスヘッダを確認することができます。
"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で許可するサイトには信頼できるサイトのみを設定するようにしてください。