CORS とは?
ここでは CORS (Cross-Origin Resource Sharing, クロスオリジンリソース共有) の概要を説明します。
CORS とは?
まずは、「同一生成元ポリシー」とは?
CORS というのは、「同一生成元ポリシー (Same-Origin Policy)」というポリシーによって設けられた制限を緩めるものです。
CORS の読み方は?
アメリカ英語では CORS の発音はカタカナで言えば「コーズ」に近い発音です。伸ばすところは R の発音なので単純に「こーず」ではありませんが。
ですから、CORS を理解するにはまずは、「同一生成元ポリシー」というのがどういうものか知る必要があります。
同一生成元ポリシーというのは、すごく簡単に言うと 「JavaScript で自由にやりとりできるところは、その JavaScript をとってきたところと同一の場所だけに制限する」 ということです。
同一生成元かどうか判断する時には、ホスト名、スキーム、ポート番号がチェックされます。これらが同じ場合、同一生成元へのアクセスとみなされます。
この制限があるために、あるサーバーから JavaScript をダウンロードし、そのスクリプトから全く別のサーバーにアクセスしてそこから情報を取得する、ということができなくなります。 これによって、セキュリティが高まります。
実際には、単純な要求の場合は、ネットワークレイヤではデータの行き来は発生します。JavaScript の API では、それらのデータを取得して利用できません。
現代の主要なブラウザは全て、同一生成元ポリシーに基づく制限を実装しています。
CORS は、同一生成元でないところへの要求を安全に許可する仕組み
同一生成元ポリシーに基づく制限によって、特定の状況のセキュリティは高まりますが、その反面、厄介な面もでてきます。
例えば、ある企業 A が REST 方式のウェブサービスを一般公開しているとします。 企業 A は、誰でもどこからでも、自由にウェブサービスを使ってもらいたいと思っているとします。
そしてユーザーも、そのサービスを任意のタイミングで利用したいと考えています。
しかし、同一生成元ポリシーがあるので、どの JavaScript からでも自由にそのウェブサービスを使える、ということにはなりません。
企業 A のウェブサービスにアクセスして情報を取得できるのは、基本的に企業 A のサーバーに配置されていてユーザーが取得した JavaScript 内からに限られてしまいます。
そこで登場したのが CORS です。CORS は同一生成元ポリシーに基づく制限を解除する方法を提供します。
CORS で定義された方法に従えば、たとえ同一生成元でなくても、JavaScript による自由なアクセスが許可されます。
同一生成元というのは英語で Same-Origin といいます。同一ではない場合をクロスオリジン (Cross-Origin) といいます。
では、具体的にどのような方法で CORS は実現されるのでしょうか。
CORS の仕組み
上で説明したように CORS (Cross-Origin Resource Sharing) というのは、クロスオリジンのリソースにアクセスする仕組みです。
ここでは JavaScript を用いてソフトウェアを開発するにあたり、開発者が知っておくべき CORS のポイントについて説明します。
CORS についての詳細は W3C Cross-Origin Resource Sharing をみてください。 ブラウザ上の JavaScript エンジン自体を開発しているのでもなければ、CORS の詳細まで完全に知る必要はありません。
JavaScript の開発者としては、通信エラーが発生したときの原因究明やセキュリティの制限事項・許可事項として理解することが大事です。
「単純な要求」と「単純ではない要求」に区別する
JavaScript で XMLHttpRquest や Fetch API を使って、サーバーに非同期的な HTTP 要求 (リクエスト) を送信する状況を考えます。
もしこのとき、HTTP のメソッドとして GET、POST、HEAD を使い、かつ Accept、 Accept-Language、Content-Type などのいくつか基本的な HTTP ヘッダーがセットされただけのリクエストであれば、この HTTP リクエストを 「単純な HTTP リクエスト」(simple Cross-Origin request) とします。
Content-Type は特に、 application/x-www-form-urlencoded、 multipart/form-data、text/plain の値がセットされたものを「単純」とみなします。
それ以外のリクエストは全て「単純ではない要求」として区別します。
単純な要求の場合、後述のプリフライト (preflight) が不要です。
「単純な要求」の場合、そのまま要求を送信して、サーバーからの応答をチェックする
「単純な要求」の場合は、クロスオリジンの HTTP 要求であっても、直ちにサーバーに送信されます。
このとき Origin HTTP ヘッダーには、要求の送信元の情報がセットされます。
要求を受け取ったウェブサーバーは、その要求を許可するかどうか、要求にセットされた Origin ヘッダーをチェックするなりして判断することができます。
サーバーが HTTP 要求を許可する場合、Access-Control-Allow-Origin HTTP ヘッダーをセットして応答を返します。
ブラウザ上の JavaScript ランタイムは、サーバーから適切な許可があることを確認してから、レスポンスデータをユーザーのコードに渡します。許可が確認できなければ、エラーとします。
それ以外では事前にサーバーに問い合わせ、許可を確認する
上記以外の「単純な要求ではない場合」、ブラウザ上の JavaScript ランタイムは、サーバーに事前に自動的に問い合わせ、 アクセスが許可されるかどうか確認します。この事前問合せのことをプリフライト (Preflight) といいます。
プリフライトは、 HTTP の OPTIONS リクエストを使って行われます。
Preflight の要求時には、これから使う HTTP メソッドや HTTP ヘッダーの種類を申告します。
このとき Origin ヘッダーの他、 Access-Control-Request-Method ヘッダや Access-Control-Request-Headers ヘッダを使います。
サーバーはこれらをチェックして、アクセスを許可する場合、Access-Control-Allow-Origin ヘッダの他、 Access-Control-Request-Method ヘッダやAccess-Control-Request-Headers ヘッダを返します。
さらにこのプリフライトの有効期間となる Access-Control-Max-Age を設定します。
こうして、サーバーからのアクセス許可が確認できてから、実際の HTTP 要求が送信されます。
CORS は JavaScript での開発時にどう関わるか?
クロスオリジンの要求を行うときには、ブラウザは自動的に CORS の手順に従います。 セキュリティのチェックを行って、それに違反していればエラーとなり終了します。
CORS の手続きは、ランタイムが裏で自動的に行いますので、基本的に開発者はあまり意識しなくて大丈夫です。
デバッガのコンソールを見て、「CORS が原因で通信エラーが出た」と言われたときに、いったい何のことか理解できれば良いでしょう。
クロスオリジンの要求を行う
それでは、 CORS に関わるサンプルプログラムを実際に動かして、プログラムがどのような動きをするか見てみましょう。
【サーバー側】Access-Control-Allow-Origin を返さない場合
まずはサーバー側のコードです。Node.js + Express をウェブサーバーにします。
クロスドメインのリクエストを許可する場合は、上で説明したように Access-Control-Allow-Origin (ACAO) HTTP ヘッダーをセットして、応答しなければなりません。
しかし、まずは ACAO ヘッダーを返さないとどうなるか、みてみましょう。
Node.js で次のコマンドでプロジェクトを作成します。
% mkdir cors-test-server % cd cors-test-server % npm init % npm install express connect-multiparty
ここで作成したフォルダ cors-test-server 内に server.js という名前でファイルを作り、 内容を次にします。
const express = require('express');
const multipart = require('connect-multiparty');
const app = express();
app.use(multipart());
app.post('/test1', (req, res) => {
console.log('/test1');
console.log(req.body);
const obj = {
message: 'Hello from server!'
};
res.status(200).json(obj);
});
app.listen(3002, () => console.log('Listeninig on port 3002...'));
これで次のコマンドを実行すると、サーバーの出来上がりです。
% node server.js Listeninig on port 3002...
3002番ポートで HTTP リクエストをリスニングしています。
Node.js については Node.js 入門 も参考にしてください。
XMLHttpRequest でクロスオリジンの要求を行う
実際に XMLHttpRequest を用いて、クロスオリジンの HTTP 要求を行ってみましょう。
ファイルを返すために 3001番ポートでウェブサーバーをもうひとつ作ります。
% mkdir cors-test-server2 % cd cors-test-server2 % npm init % npm install express % mkdir public_html
今回は HTML や JavaScript など、静的ファイルを保存するためのフォルダ public_html を作っています。
server.js という名前でファイルを作成します。内容は次の通りです。
const express = require('express');
const app = express();
const path = require('path');
app.use(express.static(path.join(__dirname, './public_html')));
app.listen(3001, () => console.log('Listeninig on port 3001...'));
public_html フォルダ内に、次の内容で cors1.js という名前のファイルを作ります。
'use strict';
window.addEventListener('load', () => {
document.getElementById('button1').addEventListener('click', (evt) => {
evt.preventDefault();
let form1 = document.getElementById('form1');
let fd = new FormData(form1);
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:3002/test1");
xhr.addEventListener('load', (evt) => {
console.log('** xhr: load');
let response = JSON.parse(xhr.responseText);
console.log(response);
});
xhr.addEventListener('error', (evt) => {
console.log('** xhr: error');
});
xhr.send(fd);
});
});
ここでは localhost 内でテストを行いますが、ポート番号が違うので、クロスオリジンとみなされます。
public_html フォルダ内に次を cors1.html という名前で作ります。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Test foo</title>
<script src="cors1.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<form id="form1" class="mt-3">
<input type="text" name="text1"
class="form-control"
value="Hello, CORS!" />
</form>
<button type="button" id="button1"
class="btn btn-primary mt-1">OK</button>
</div>
</body>
</html>
HTML の画面は次のようになります。
CORS を試すだけなら、特に入力を受け付ける必要もないのですが、データの行き来がわかりやすくなるようにしています。
これで HTML と JS を返す側の Node + Express サーバーも起動して、ブラウザから HTTP 要求を送信して試してみましょう。
% node server.js Listeninig on port 3001...
ブラウザから http://localhost:3001/cors1.html を要求し、表示された OK ボタンをクリックします。
表面上は何も起きませんが、ブラウザのデバッガ・コンソールをみるとエラーが確認できます。
Access to XMLHttpRequest at 'http://localhost:3002/test1' from origin 'http://localhost:3001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Cross-Origin Read Blocking (CORB) blocked cross-origin response http://localhost:3002/test1 with MIME type application/json.
サーバーが Access-Control-Allow-Origin を適切に返さなかったので、クライアントではポリシー違反の通信エラーが発生しました。
【サーバー側】Access-Control-Allow-Origin を返す場合
次に 3002 番側のサーバーで Access-Control-Allow-Origin HTTP ヘッダを追加します。
...
app.post('/test1', (req, res) => {
console.log('/test1');
console.log(req.body);
// 次の 1 行を追加
res.set('Access-Control-Allow-Origin', '*');
const obj = {
message: 'Hello from server!'
};
...
Node + Express サーバーをリスタートして、もう一度 OK ボタンを押してクロスオリジンの要求を送信します。
すると、次のように問題なくサーバーからの応答を受け取れます。
確かにクロスオリジンの通信に成功し、サーバーからの応答がコンソールに表示されました。
プリフライトを必要とするリクエストの送信
次に、「単純ではないリクエスト」つまり「プリフライトを必要とするリクエスト」を行います。
まず、クライアント側のコードの、XMLHttpRequest オブジェクトで HTTP 要求を送信する箇所を書き換えます。
次の1行を追加して、カスタムの HTTP ヘッダーを追加します。
...
xhr.open("POST", "http://localhost:3002/test1");
// 次を追加
xhr.setRequestHeader("X-MY-ORIGINAL", "Yay!");
...
カスタムの HTTP ヘッダーが追加された HTTP 要求は「単純なリクエスト」ではありません。このため、プリフライト (Preflight) によるアクセス確認が必要です。
上で説明したように、プリフライトというのは、実際の HTTP リクエストを送信する前にサーバーに許可をチェックするための仕組みです。 JavaScript ランタイムが自動的に、サーバーに対して HTTP の OPTIONS リクエストを送ります。
今回は、カスタムの X-MY-ORIGINAL という名前の HTTP ヘッダーが許可されるかどうか、 サーバーに確認する必要があります。
【サーバー側】プリフライトに応答する
サーバーが X-MY-ORIGINAL という名前の HTTP ヘッダー付きのアクセスを許可する場合は、Access-Control-Allow-Headers ヘッダーに X-MY-ORIGINAL という値をセットして、 OPTIONS リクエストに応答する必要があります。
3002番の側のウェブサーバーコードを次のように書き換えます。
const express = require('express');
const multipart = require('connect-multiparty');
const app = express();
app.use(multipart());
// OPTIONS リクエストに応答
app.options('/test1', (req, res) => {
console.log('/test1 - OPTIONS');
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Headers', 'X-MY-ORIGINAL');
res.set('Access-Control-Max-Age', '600');
res.send();
});
app.post('/test1', (req, res) => {
console.log('/test1');
console.log(req.body);
// アクセス許可
res.set('Access-Control-Allow-Origin', '*');
const obj = {
message: 'Hello from server!'
};
res.status(200).json(obj);
});
app.listen(3002, () => console.log('Listeninig on port 3002...'));
これでカスタムの HTTP ヘッダー付きのクロスオリジンの要求にも応答できるようになります。
Access-Control-Max-Age で指定した値は、クライアント側がプリフライトの情報をキャッシュできる期限です。秒単位で指定します。
例えば、この値を 10 を指定したら、プリフライト (OPTIONS リクエスト) を一度送ってアクセス許可のチェックを行ったら、10秒間は プリフライトなしで HTTP リクエストを送ります。 10秒過ぎたらまたプリフライトを送ります。
サーバーで Access-Control-Max-Age を -1 としたら、クライアントは毎回プリフライトを送ります。
ブラウザのデバッガでキャッシュを無効にしている場合も、Access-Control-Max-Age の値にかかわらず、常にプリフライトを送ります。
これでカスタムの HTTP ヘッダー付きでも、ちゃんとクロスオリジンのアクセスが成功するはずです。
【サーバー側】cors パッケージの利用
上では、CORS の手続きを確認するために、サーバーの応答方法について、全て手書きで対応しました。
実際は cors パッケージというミドルウェアが公開されており、それを使うのが便利です。 ヘッダーを手書きで書いたりする手間が省けます。
cors を次のコマンドでインストールします。
% npm install cors
全てのクロスオリジン要求を許可する場合は、次のようにミドルウェアをセットします。
const express = require('express');
const multipart = require('connect-multiparty');
const cors = require('cors');
const app = express();
app.use(multipart());
app.use(cors());
app.post('/test1', (req, res) => {
console.log('/test1');
console.log(req.body);
const obj = {
message: 'Hello from server!'
};
res.status(200).json(obj);
});
app.listen(3002, () => console.log('Listeninig on port 3002...'));
これだけで、クロスオリジンの要求に応答することができます。OPTIONS のミドルウェアを書く必要もないので、簡単ですね。
以上、CORS の概要を説明し、実際にサンプルプログラムを実行して、動作を確認しました。