ども、シローです。
今回はサーバサイドとフロントサイドをマイクロサービスで別々のドメインにして開発するときのCORS問題と僕なりの解決策を紹介します。
マイクロサービス化について
フロントサイドとサーバサイドを1つのリポジトリで管理していたものを、フロントサイドとサーバサイドで別々のリポジトリで分けて開発していくのをマイクロサービス化と解釈しています。
マイクロサービス化するとフロントサイド、サーバサイドで別々で開発・デプロイすることができるメリットがあります。
ただ同時に実装の難易度は高くなります。
その1つとして、フロントサイドとサーバサイドで通信をするときのCORS制約があります。
CORS制約とは
CORSはCross-Origin-Resource-Sharingの略で、CORS制約とはブラウザが現在のドメインとは違うドメインに対してHTTPリクエストを送ることを制限する機能です。
例えば、sample-A.comというサイトがsample-B.comというドメインに対してAPIを実行すると「Access to fetch at 'http://sample-B.com/api/hello' from origin 'http://sample-A.com' has been blocked by CORS policy・・・」
というエラーが出てリクエストが通らないことがあります。
CORS制約でエラーになる原因
現在のドメインとは違うドメインに対してリクエストを送るときには事前にプリフライトリクエスト(preflight request)を送ります。
サーバサイドでpreflight requestに対して正常なステータスを返さないとその後のリクエストは実行されないのがエラーの実態です。
preflight requestとは
送り先(サーバサイド)に別のドメインからのアクセスを許すかどうかのチェックするリクエストです。
HTTPメソッドは「OPTIONS」で、リクエストヘッダーにAccess-Control-Request-Method、Originを含みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
> OPTIONS /api/hello HTTP/1.1 > Host: api.sample.com > Accept-Encoding: deflate, gzip > Connection: keep-alive > Pragma: no-cache > Cache-Control: no-cache > Accept: */* > Access-Control-Request-Method: GET > Access-Control-Request-Headers: content-type > Origin: http://sample.com > User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36 > Sec-Fetch-Mode: cors > Referer: http://sample.com/ > Accept-Language: en,en-US;q=0.9,ja;q=0.8 |
サーバサイドではそれぞれのリクエストヘッダーの値を許容するためにAccess-Control-Allow-Methods, Access-Control-Allow-OriginにそれぞれAccess-Control-Request-Method, Originの値を含む結果を返す必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
< HTTP/1.1 200 OK < Server: nginx/1.21.6 < Date: Sun, 27 Feb 2022 11:20:20 GMT < Content-Type: application/octet-stream < Content-Length: 0 < Connection: keep-alive < Access-Control-Allow-Origin: http://sample.com < Access-Control-Allow-Credentials: true < Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS < Access-Control-Max-Age: 1728000 < Access-Control-Allow-Headers: Origin, Authorization, Accept, Content-Type < Content-Length: 0 < Content-Type: text/plain; charset=utf-8 < * Connection #0 to host api.sample.com left intact * Closing connection 0 |
CORSエラーの対応策
さて、本題のどうやってCORSエラーを回避するかについて入ります。
preflight requestを受けて正常な結果を返す方法としては
- サーバサイドのスクリプトでpreflight requestに対応する
- サーバサイドにアクセスする前のミドルウェアでprefight requestに対応する
が候補としてあります。今回は後者のミドルウェアNginxを使った方法を紹介します。
構成
サーバサイドのドメイン:api.sample.com
フロントサイドのドメイン:sample.com
/etc/hostsの設定:
1 |
127.0.0.1 sample.com api.sample.com |
nginxの設定:
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 39 40 41 42 43 44 45 46 |
server { listen 80; server_name api.sample.com; location / { # どのオリジンからのアクセスを許可するかの一覧 add_header Access-Control-Allow-Origin 'http://sample.com' always; # Request.credentialsがincludeの場合cookieをレスポンスとして返す add_header Access-Control-Allow-Credentials true always; # preflightリクエストのときに実行 if ($request_method = 'OPTIONS') { # どのオリジンからのアクセスを許可するかの一覧 add_header Access-Control-Allow-Origin 'http://sample.com' always; # Request.credentialsがincludeの場合cookieをレスポンスとして返す add_header Access-Control-Allow-Credentials true always; # preflight requestで許可するメソッドの一覧 add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; # preflight requestのresponseをキャッシュする時間(s) add_header Access-Control-Max-Age 1728000; # preflight requestで許可するヘッダー一覧 add_header Access-Control-Allow-Headers "Origin, Authorization, Accept, Content-Type" always; add_header Content-Length 0; add_header Content-Type 'text/plain; charset=utf-8'; return 200; } proxy_pass http://api:3000; } } server { listen 80; server_name sample.com; root /var/www; location / { try_files $uri $uri/ =404; } } server { listen 80 default_server; root /var/www; location / { try_files $uri $uri/ =404; } } |
構成図
解説
Nginxの設定でOPTIONSのリクエストの場合の処理を記述しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# preflightリクエストのときに実行 if ($request_method = 'OPTIONS') { # どのオリジンからのアクセスを許可するかの一覧 add_header Access-Control-Allow-Origin 'http://sample.com' always; # Request.credentialsがincludeの場合cookieをレスポンスとして返す add_header Access-Control-Allow-Credentials true always; # preflight requestで許可するメソッドの一覧 add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; # preflight requestのresponseをキャッシュする時間(s) add_header Access-Control-Max-Age 1728000; # preflight requestで許可するヘッダー一覧 add_header Access-Control-Allow-Headers "Origin, Authorization, Accept, Content-Type" always; add_header Content-Length 0; add_header Content-Type 'text/plain; charset=utf-8'; return 200; } |
add_headerディレクティブはレスポンスヘッダーを追加します。
Access-Control-Allow-Origin
どのオリジンからのリクエストを許可するかを指定します。
リクエストヘッダーのOriginの値を指定する必要があります。また、'*'を指定するとどのドメインからのアクセスを許可することになります。
Access-Control-Allow-Methods
許可するリクエストメソッドを指定します。
リクエストヘッダーのAccess-Control-Request-Methodを含む値を指定する必要があります。
Cookieも送信されるようにしたい場合
クロスドメイン間でAPIを介してフロントサイドでユーザの認証をCookieとSessionを使って実装する場合、
サーバサイドからCookieをフロントサイドに送信できるようにしないといけません。
リクエストを送る側
リクエストを送信するツールごとによって対応は異なるかもですが、fetch APIを例にするとcredentialsを'include'にする必要があります。
以下実装例(JS)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const executeGetCookie = async () => { const dom = document.querySelector('#execute-get-cookie > .result') const response = await fetch( apiHost + '/api/cookie', { method: 'GET', credentials: 'include', headers } ) const data = await response.json() dom.innerHTML = JSON.stringify(data) } |
レスポンスを送る側
Access-Control-Allow-Credentials をtrueにする必要があります。
以下実装例(Nginx)
1 2 |
# Request.credentialsがincludeの場合cookieをレスポンスとして返す add_header Access-Control-Allow-Credentials true always; |
サンプルコード
僕の環境で検証したソースコードを以下のリポジトリにアップしました。
https://github.com/smithshiro/cors-test
まとめ
- CORSは異なるドメインに対するリクエストを制限するブラウザの機能
- CORSのリクエストは直前のOPTIONSのpreflightリクエストを送信する、サーバはこのリクエストに対して正常に返す必要がある
- レスポンスヘッダーにはAccess-Control-Allow-Origin、Access-Control-Allow-Methodsを指定する
- Cookieを受け取りたい場合は、Access-Controll-Allow-Credentailsをtrueにする