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' ]; 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 = {};const headers = new Headers();headers.append('Content-Type' , 'text/plain' ); headers.append('X-Custom-Header' , 'custom-header' ); fetchOption['headers' ] = headers; 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' ]; 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(); }); app.options('*' , (req, res) => { res.sendStatus(200 ); }); 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)