CORSまとめ

今更ですが、**[CORS (Cross-Origin Resource Sharing)](https://fetch.spec.whatwg.org/#http-cors-protocol)**を色々試していたら、思っていた以上に色々パターンがあることに気づいたので、改めてその扱い方についてまとめてみました。

# そもそも

現在のWebブラウザでは、あるWebサイトが持つ情報が別の悪意あるWebサイトに悪用されるのを防ぐために、**[Same-Origin Policy](http://www.rfc-editor.org/info/rfc6454)**(日本語では同一生成元ポリシー)が適用されます。

例えば、ある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を使うことを宣言します。

```js:クライアントのJavaScript(XHR)
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);
```

```js:クライアントのJavaScript(Fetch)
fetch('https://usefulapis.net/api', {
  mode: 'cors'
}).then(onLoadFunc);
```

一方、Webサーバ側では、Originを越えたアクセスを許可することをブラウザに明示的に知らせるために、HTTPレスポンスヘッダに適切な情報を付加する必要があります。

まず、ブラウザからサーバに送られるHTTPリクエストヘッダには、Originを越えるアクセスの場合は`Origin`というフィールドが含まれます。

```http
GET /api HTTP/1.1
Origin: https://trustedsite.com
```

もし、`Origin`の内容が信頼できるWebサイトのOriginであれば、HTTPレスポンスヘッダに、

```http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
```

といった内容を追加すれば、ブラウザ側でアクセスが許可されるようになります。なお、このようなシンプルな例に限り、どのWebサイトにもOriginを越えるアクセスを許可することをワイルドカードで指定することが可能です(サブドメイン等の部分指定はできません)。

```http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
```

## Cookieも許可したい場合

HTTP(S)通信時にCookieの送受信も許可したい場合は、ブラウザとサーバの両方で、もう少々細工が必要となります。まず、ブラウザのJavaScriptでは次のようにします。なお、この例以降では、`Access-Control-Allow-Origin`においてワイルドカード指定が許可されなくなりますので、注意が必要です。

```js:クライアントのJavaScript(XHR)
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.withCredentials = true;
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);
```

```js:クライアントのJavaScript(Fetch)
fetch('https://usefulapis.net/api', {
  mode: 'cors',
  credentials: 'include'
}).then(onLoadFunc);
```

これに対し、サーバ側ではHTTPレスポンスヘッダに次のような内容を追加します。

```http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
```

## さらに手の込んだHTTP通信を使いたい場合

[CORSの仕様](https://fetch.spec.whatwg.org/#cors-preflight-fetch)では、次の条件に一つでも当てはまる場合は、実際の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リクエストヘッダが含まれます。

```http
OPTIONS /api HTTP/1.1
Access-Control-Request-Method: (この後に行うリクエストのHTTPメソッド(GET, POSTなど))
```

このpreflight requestに対するレスポンスとしては、例えば、少なくとも次のような要領で、Originを越えるアクセスとして許可するHTTPリクエストのメソッドを指定する必要があります。

```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というヘッダを追加したとします。

```js:クライアントのJavaScript(XHR)
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);
```

```js:クライアントのJavaScript(Fetch)
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がブラウザからサーバに送られます。

```http
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
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を越えるアクセスの場合、例えば、ブラウザ側のコードが、

```js:クライアントのJavaScript(XHR)
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');
}
```

```js:クライアントのJavaScript(Fetch)
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
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
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
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(秒))となっています。