Cross-Origin Resource Sharing(CORS)を使用したHTTPリクエスト

Webブラウザで異なるWebサイトのリソースを取り扱う際には、HTTPリクエストはCross-Origin Resource Sharing (CORS)が適用される。

ということは知っている話ですが、詳細まで追えてなかったので改めて眺めてみた。

CORS(同一生成元)

CORSは異なるドメインへのアクセス方法を規定しており、これによりドメイン間の通信の安全性が保障される。
一般的なWebブラウザではXMLHttpRequestやFeatch APIなどHTTPリクエストでCORSが適用され、リソースの取得は許可されない。

CORSは以下のブラウザでサポートされている。

  • Chrome 3+
  • Firefox 3.5+
  • Internet Explorer 11+
  • Opera 12+
  • Safari 4+

ちなみにCORSは日本語で同一生成元などと言われる。
プロトコル、ドメイン、ポートの3つが同じである場合に同一生成元となる。

同じプロトコル、ドメインでもポートが違うと同一生成元となる。

Cross OriginのリソースをFetch APIを使って取得する

確認するにあたりExpressで簡単なアプリケーションを作成する。

https://127.0.0.1:3001 のホストから http://127.0.0.1:3000/no-cors/ にFetch APIでリクエストを行い、取得したリソースにアクセスをする。

Express routing

1
2
3
4
5
6
7
8
9
const ALLOWED_ORIGINS = [
'https://127.0.0.1:3000',
'https://127.0.0.1:3001'
];

// routes
app.get(ROUTES, (req, res) => {
res.render('index', {title: `access to ${req.url}`});
});

Viewファイルからリクエストをする

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fetchOption = {};

// Headerを作成
const headers = new Headers();
headers.append('Content-Type', 'text/plain');
headers.append('X-Custom-Header', 'custom-header');
fetchOption['headers'] = headers;

// Featch
fetch('https://127.0.0.1:3000/no-cors/', fetchOption)
.then(res => {
document.body.innerHTML = "statusCode: " + res.status + "<br>statusText: " + res.statusText + "<br>cookie: " + document.cookie;
})
.catch(error => {
const elem = document.querySelector('.response');
elem.innerHTML = JSON.stringify(error);
});

これはもちろんエラーとなる。Google Chrome の場合以下のようなエラーがコンソールに出力される。

1
2
3
4
127.0.0.1/:1 Fetch API cannot load https://127.0.0.1:3000/no-cros/.
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'https://127.0.0.1:3001' is therefore not allowed access.
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

実際のHTTPヘッダのやりとりを見てみる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Request Header
OPTIONS /no-cros/ HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive
Access-Control-Request-Method: GET
Origin: https://127.0.0.1:3001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Access-Control-Request-Headers: x-custom-header
Accept: */*
DNT: 1
Referer: https://127.0.0.1:3001/no-cros/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

# Response Header
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 2
ETag: W/"2-nOO9QiTIwXgNtWtBJezz8kv3SLc"
set-cookie: connect.sid=s%3AqFiO6ASvY-4Sc0IFI_bWSKZV2rbwBoDH.WIyBhxr31leCLd%2BSv532FQHUS1chxk2d3iv6zweCh8k; Path=/; HttpOnly
Date: Thu, 23 Mar 2017 13:26:23 GMT
Connection: keep-alive

エラー内容にあるように
preflight request(プリフライトリクエスト)の際に Access-Control-Allow-Origin ヘッダがない ということでエラーになっている。

ちなみにCORSに使われるHTTPリクエストヘッダは以下

HTTP リクエストヘッダ content
Origin リクエストしたオリジン
Access-Control-Request-Method リクエストを行うHTTPメソッド。プリフライトリクエストに使われる
Access-Control-Request-Headers リクエストを行う際に利用されるHTTPヘッダ

Access-Control-Allow-OriginとCROS

次に Access-Control-Allow-Origin ヘッダを返してあげるようにする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const ALLOWED_METHODS = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'HEAD',
'OPTIONS'
];

const ALLOWED_ORIGINS = [
'https://127.0.0.1:3000',
'https://127.0.0.1:3001'
];

// レスポンスHeaderを組み立てる
app.use((req, res, next) => {
const origin = req.headers.origin;
if(ALLOWED_ORIGINS.indexOf(req.headers.origin) > -1) {
sess.cookie.secure = true;
res.cookie('example', Math.random().toString(), {maxAge: 86400, httpOnly: true});
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', ALLOWED_METHODS.join(','));
res.setHeader('Access-Control-Allow-Headers', 'Content-type,Accept,X-Custom-Header');
}

next();
});

// HTTP OPTIONS
app.options('*', (req, res) => {
res.sendStatus(200);
});

// routes
app.get(ROUTES, (req, res) => {
res.render('index', {title: `access to ${req.url}`});
});

実際のHeaderのやりとりは以下のようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Request Header
OPTIONS /cros/ HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive
Access-Control-Request-Method: GET
Origin: https://127.0.0.1:3001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Access-Control-Request-Headers: x-custom-header
Accept: */*
DNT: 1
Referer: https://127.0.0.1:3001/cros/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

# Response Header
HTTP/1.1 200 OK
X-Powered-By: Express
set-cookie: example=0.6767570391231008; Max-Age=86; Path=/; Expires=Thu, 23 Mar 2017 13:49:13 GMT; HttpOnly
set-cookie: connect.sid=s%3AebdnoA5XfiJBSma9g9i-PMzsTbTzHH16.dlGmLYU26FIAgP8SYyhV9cOWJ%2FP0caflOexh5%2FmkZkk; Path=/; HttpOnly
Access-Control-Allow-Origin: https://127.0.0.1:3001
Access-Control-Max-Age:86400
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS
Access-Control-Allow-Headers: Content-type,Accept,X-Custom-Header
Content-Type: text/plain; charset=utf-8
Content-Length: 2
ETag: W/"2-nOO9QiTIwXgNtWtBJezz8kv3SLc"
Date: Thu, 23 Mar 2017 13:47:47 GMT
Connection: keep-alive

ということで、エラーもなく成功する。

HTTPヘッダを眺めてみると、リクエストHeaderはHTTP OPTINSメソッドが投げられている。
これは、プリフライトリクエストとなる。
プリフライトリクエストは一般的なシンプルなリクエストと異なり、CORSなどのリクエストを行う前に先行して送信される。
これによりリクエスト先のリソースにアクセス可能か、リクエストメソッドが実装されいるかを検証する。

ここでエラーとなった場合は実際にはリクエストしないことになる。

ちなみにプリフライトリクエストが送信される条件は以下となる。

  • GET、HEAD、POST 以外のHTTPメソッドでリクエストした場合
  • Content-Typeがapplication/x-www-form-urlencoded、multipart/form-data、text/plainのPOSTリクエストをした場合
  • カスタムヘッダをリクエストをした場合

ということで、CORSに対応する場合は、サーバー側では必ずOPTIONSメソッドを正しく返却ししないと、
本来のリクエストも失敗することになる。

つづいてレスポンスヘッダを眺めてみる。

1
2
3
4
Access-Control-Allow-Origin: https://127.0.0.1:3001
Access-Control-Max-Age:86400
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS
Access-Control-Allow-Headers: Content-type,Accept,X-Custom-Header
http レスポンスヘッダ content
Access-Control-Allow-Origin アクセスを許可するURI (* でワイルドカードとなる)
Access-Control-Allow-Methods 許可するHTTPメソッドを指定
Access-Control-Allow-Headers 実際のリクエストで使用できるHTTPヘッダを明示
Access-Control-Max-Age プリフライトの結果をキャッシュ時間
Access-Control-Expose-Headers 利用できるヘッダのホワイトリストを明示
Access-Control-Allow-Credentials Credentialsを含めたリクエストを取り扱えるか

ということで取得したリソースにアクセスできる。

CredentialsとCORS

認証情報やcookieなどを含むリクエストの場合のHTTP headerは以下の通り

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Request
OPTIONS /cros-with-credentials/ HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive
Access-Control-Request-Method: GET
Origin: https://127.0.0.1:3001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Access-Control-Request-Headers: x-custom-header
Accept: */*
DNT: 1
Referer: https://127.0.0.1:3001/cros-with-credentials/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

# Response
HTTP/1.1 200 OK
X-Powered-By: Express
set-cookie: example=0.654153584678103; Max-Age=86; Path=/; Expires=Thu, 23 Mar 2017 16:10:43 GMT; HttpOnly
set-cookie: connect.sid=s%3AM9NQy3hz5cLb3kW6htuybWE6nEX1_iL6.ENXqiTfVPMQVyP%2FGFZ9pshnC87D7rX5%2BM48mjVqwR7s; Path=/; HttpOnly
Access-Control-Allow-Origin: https://127.0.0.1:3001
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS
Access-Control-Max-Age: 86400
Access-Control-Allow-Headers: Content-type,Accept,X-Custom-Header
Access-Control-Allow-Credentials: true
Content-Type: text/plain; charset=utf-8
Content-Length: 2
ETag: W/"2-nOO9QiTIwXgNtWtBJezz8kv3SLc"
Date: Thu, 23 Mar 2017 16:09:17 GMT
Connection: keep-alive

ここではヘッダに以下を含まれるようにする必要がある。

1
Access-Control-Allow-Credentials: true

また、Credentialsを含めたリクエストの場合は、Access-Control-Allow-Originがワイルドカード(*を指定している)場合はリクエストが失敗する。

1
2
3
Fetch API cannot load https://127.0.0.1:3000/cros-with-credentials/.
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
Origin 'https://127.0.0.1:3001' is therefore not allowed access.

とうことで、CORSでのリクエストとその概要を改めて眺めた。
プリフライトリクエストの存在とその役割、若干曖昧だったCORSの正しい対応方法を理解することができた。

今回使ったサンプル

kazu69/cross-origin-resource-sharing-exmpress-example

参考しにしたページ

HTTP アクセス制御 (CORS)

Comments