今更ですが、**CORS (Cross-Origin Resource Sharing)**を色々試していたら、思っていた以上に色々パターンがあることに気づいたので、改めてその扱い方についてまとめてみました。
そもそも
現在のWebブラウザでは、あるWebサイトが持つ情報が別の悪意あるWebサイトに悪用されるのを防ぐために、Same-Origin Policy(日本語では同一生成元ポリシー)が適用されます。
例えば、あるWebサイト https://guiltysite.com をブラウザで表示している時に、このWebページからXMLHttpRequest(以下、XHR)やFetch APIで別のWebサイト https://innocentsite.net からHTTP(S)でデータを読み込もうとすると、エラーになる、というわけです。
しかし、アクセス元が悪意あるWebサイトならともかく、データの連携をする相手として信頼関係ができているWebサイトにまで制限をかけてしまうと不便ですので、データのアクセスを許可できるWebサイトに対してはOriginを越えたアクセスを可能にするための仕組みとして、CORSがあります。
CORSの使い方の例
ここでは、あるWebサイト https://trustedsite.com に対して、別のWebサイト https://usefulapis.net へのHTTP(S)でのアクセスを許可したい場合を例として述べます。
シンプルにデータの読み込みを許可したい場合
単純にXHRやFetch APIでのGETやPOSTを許可したい場合は、次のようにします。まず、クライアントサイドでは、XHRの場合は特段の工夫は必要なく、Fetch APIの場合はオプションによってCORSを使うことを宣言します。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);
fetch('https://usefulapis.net/api', {
mode: 'cors'
}).then(onLoadFunc);
一方、Webサーバ側では、Originを越えたアクセスを許可することをブラウザに明示的に知らせるために、HTTPレスポンスヘッダに適切な情報を付加する必要があります。
まず、ブラウザからサーバに送られるHTTPリクエストヘッダには、Originを越えるアクセスの場合はOrigin
というフィールドが含まれます。
GET /api HTTP/1.1
Origin: https://trustedsite.com
もし、Origin
の内容が信頼できるWebサイトのOriginであれば、HTTPレスポンスヘッダに、
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
といった内容を追加すれば、ブラウザ側でアクセスが許可されるようになります。なお、このようなシンプルな例に限り、どのWebサイトにもOriginを越えるアクセスを許可することをワイルドカードで指定することが可能です(サブドメイン等の部分指定はできません)。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Cookieも許可したい場合
HTTP(S)通信時にCookieの送受信も許可したい場合は、ブラウザとサーバの両方で、もう少々細工が必要となります。まず、ブラウザのJavaScriptでは次のようにします。なお、この例以降では、Access-Control-Allow-Origin
においてワイルドカード指定が許可されなくなりますので、注意が必要です。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.withCredentials = true;
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);
fetch('https://usefulapis.net/api', {
mode: 'cors',
credentials: 'include'
}).then(onLoadFunc);
これに対し、サーバ側ではHTTPレスポンスヘッダに次のような内容を追加します。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
さらに手の込んだHTTP通信を使いたい場合
CORSの仕様では、次の条件に一つでも当てはまる場合は、実際のHTTPリクエスト(GETやPOST)を行う前に、preflight requestとしてOPTIONSリクエストを行うことが定められています。この場合、サーバ側ではGETやPOSTに加えてOPTIONSでも同様のCORS対応が必要になりますので、注意が必要です。
- HTTPリクエストのメソッドがGET, POST, HEAD以外である。
- HTTPリクエストヘッダにAccept, Accept-Language, Content-Language以外のフィールドが含まれている、あるいは、Content-Typeフィールドにapplication/x-www-form-urlencoded, multipart/form-data, text/plain以外の内容が指定されている。
preflight requestには、次のようなHTTPリクエストヘッダが含まれます。
OPTIONS /api HTTP/1.1
Access-Control-Request-Method: (この後に行うリクエストのHTTPメソッド(GET, POSTなど))
このpreflight requestに対するレスポンスとしては、例えば、少なくとも次のような要領で、Originを越えるアクセスとして許可するHTTPリクエストのメソッドを指定する必要があります。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
リクエストに独自のHTTPリクエストヘッダを追加したい場合
例えば、ブラウザ側でX-MyRequestとX-MyOptionというヘッダを追加したとします。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.withCredentials = true;
xhr.setRequestHeader('X-MyRequest', 'this-is-cors-test');
xhr.setRequestHeader('X-MyOption', 'my-option');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);
fetch('https://usefulapis.net/api', {
method: 'GET',
mode: 'cors',
credentials: 'include',
headers: {
'X-MyRequest': 'this-is-cors-test',
'X-MyOption': 'my-option'
}
}).then(onLoadFunc);
この場合、まず次のようなHTTPリクエストヘッダを含むpreflight requestがブラウザからサーバに送られます。
OPTIONS /api HTTP/1.1
Origin: https://trustedsite.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-MyRequest,X-MyOption
サーバ側では、これらのリクエストヘッダに示されているメソッドとヘッダを許可するかどうかを判断して、レスポンスヘッダを返します。Access-Control-Allow-Methodsで指定されたメソッドと、Access-Control-Allow-Headersで指定されたヘッダが、この後ブラウザが実際に送るHTTPリクエストに許可されます。(該当するヘッダはpreflightと実際のリクエストの両方で必要になります。)
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption
レスポンスに独自のHTTPレスポンスヘッダを追加してブラウザから読み出したい場合
Originを越えるアクセスの場合、例えば、ブラウザ側のコードが、
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.withCredentials = true;
xhr.setRequestHeader('X-MyRequest', 'this-is-cors-test');
xhr.setRequestHeader('X-MyOption', 'my-option');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);
function onLoadFunc() {
var myResponse = xhr.getResponseHeader('X-MyResponse');
var myOption = xhr.getResponseHeader('X-MyOption');
}
fetch('https://usefulapis.net/api', {
method: 'GET',
mode: 'cors',
credentials: 'include',
headers: {
'X-MyRequest': 'this-is-cors-test',
'X-MyOption': 'my-option'
}
}).then(onLoadFunc);
function onLoadFunc(response) {
var myResponse = response.headers.get('X-MyResponse');
var myOption = response.headers.get('X-MyOption');
}
となっていて、これに対してサーバ側からは、
HTTP/1.1 200 OK
X-MyResponse: this-is-successful-response
X-MyOptions: good-result
のような独自レスポンスヘッダをブラウザに返そうとしている場合、ブラウザがこれらのレスポンスヘッダの内容を取得しようとすると、セキュアではないヘッダにアクセスしようとしたものとみなされて、アクセスが許可されないようになっています。(アクセスが許可されるレスポンスヘッダは、Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragmaとなっているようです。)
このような独自レスポンスヘッダへのアクセスをブラウザに許可するには、許可したいレスポンスヘッダをAccess-Control-Expose-Headersで指定します。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption
Access-Control-Expose-Headers: X-MyResponse,X-MyOption
なお、当然ながら、Set-CookieとSet-Cookie2はAccess-Control-Expose-Headersで指定してもXHRやFetch APIで読むことができません。
ところで、preflight requestは毎回行われるのか?
preflight requestには、サーバ側からブラウザにキャッシュさせる有効期限を指定することが出来ます。この期限内であれば、最初のpreflight requestがこの後の同じURLに対するHTTPリクエストにも適用されるようになります。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption
Access-Control-Expose-Headers: X-MyResponse,X-MyOption
Access-Control-Max-Age: 864000
Access-Control-Max-Ageには有効期限を秒単位で指定します。上記の例では、10日間(10(日)×24(時間)×60(分)×60(秒) = 864,000(秒))となっています。