Help us understand the problem. What is going on with this article?

なんとなく CORS がわかる...はもう終わりにする。

概要

Access to XMLHttpRequest at 'http://localhost:8081' from origin 'http://localhost:8080' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'http://localhost:8080' 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.

( エラー...? CORS policy ... あー前も見たな、あの同一生成なんとかに引っかかってるやつだっけ?ドメインまたぐとダメなやつだっけ...? )

要するに、「なんとなくわかる」の状態で放置していて、結局何もわかっていないからエラーの対処がよくわからない自分が嫌いになりそうだったので、ここに CORS についてまとめておきます。
同じように自分を嫌いになりかけている誰かのお役に立てれば幸いです。

CORS の読み方

まずは読み方から。

読み方: コルス or シーオーアールエス

Cross-Origin Resource Sharing の略、日本語訳すると「オリジン間リソース共有」。

オリジンとは?

多分、この オリジン (origin) というワードが理解速度を遅くしている気がします :thinking:
オリジンに似ている概念に ドメイン (domain) があります。ドメインとオリジンの違いを知ることがイメージ湧きやすいと思います。
それぞれの具体例は次のような感じです。

ドメインとの違いは、プロトコルとポート番号を含んでいるという点ですね。

origin == protocol + domain + port number

CORS とは?

オリジンの定義を理解したところで本題の CORS について理解します。

CORS は日本語訳すると オリジン間リソース共有 でした。つまり CORS とは、あるオリジンで動いている Web アプリケーションに対して、別のオリジンのサーバーへのアクセスをオリジン間 HTTP リクエストによって許可できる仕組みのことを言います。
許可できるようになるまでの仕組みとしては、サーバー(下の図で言うと domain-b.com)からのレスポンスにリソースの共有を許可するためのヘッダーを追加して実現するという感じです。

CORS_principle.png

CORS の必要性

Same-Origin Policy

Web セキュリティの重要なポリシーの一つに Same-Origin Policy (同一オリジンポリシー)があります。
これは、オリジン間のリソース共有に制限をかけるもので、次のような脆弱性を防ぐことを目的としたものです。

  • XSS (Cross Site Scripting)

ユーザーが Web サイトにアクセスすることで不正なスクリプトが Client (Web ブラウザ) で実行されてしまう脆弱性。
被害例は、Cookie 内のセッション情報を抜き取られて不正ログインを行われる、など。

  • CSRF (Cross-Site Request Forgeries)

Web アプリケーションのユーザーが、意図しない処理を Web アプリケーション (Web Server) 上で実行される脆弱性。通称「しーさーふ」。
被害例は、本来はログインしたユーザーしか実行できない記事の投稿処理を勝手にされる、など。

JavaScript の組み込み API で、Ajax 通信を実現する XMLHttpRequest (XHR)Fetch API などは、これらの脆弱性を回避するため、Same-Origin Policy に従います。

XSS と CSRF についてはこちらの記事に詳しくまとめましたので、お時間がある時にでもどうぞ :tea:
https://qiita.com/att55/items/a50ca43adde206017525

具体的な実装

CORS の概念は一通り頭に入ったと思いますので、ここからは具体的な実装について書きます。

まず、クライアントサイドですがこちらでは特にやることはなく、異なるオリジンへのリクエストは以下のような Origin というフィールドが追加されます。

Origin: https://trusted-one.co.jp

※ XHR は特にやることはないですが、Fetch API では cors mode を設定する必要があります。

fetch('https://trusted-api.co.jp', {
  mode: 'cors'
});

サーバーサイドで以下のレスポンスヘッダーを追加する必要があります。

Access-Control-Allow-Origin: https://trusted-one.co.jp  // 特定のサイトを許可する
Access-Control-Allow-Origin: *   // 全てのサイトを許可する
Access-Control-Allow-Headers "X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept"  // この辺は使うフレームワークにより異なるが許可するヘッダーを定義しておく。

Simple Request と Preflight Request について

CORS ではリクエストの種類が Simple RequestPreflight Request の2種類に分けられています。

Simple Request

単純リクエスト (Simple Request) と言われているのは以下のメソッドです。

  • GET
  • POST
  • HEAD

Preflight Request

単純リクエストとは異なり、プリフライトリクエスト (Preflight Request) は、リクエストの始めに OPTIONS メソッドで対象の異なるオリジンにリクエストを送り、実際のリクエストを送っても問題ないか確認します。
該当するリクエストは以下になります。

  • PUT
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH

プリフライトリクエストのレスポンスとして、アクセスを許可するメソッドをレスポンスヘッダーに含める必要があります。

Access-Control-Allow-Methods: PUT, DELETE, PATCH

Cookie も許可する

a.com という js のページを開いた状態で b.com へ XMLHttpRequest を送る際に b.com の Cookie も含めてリクエストを送りたいという場合、デフォルトでは異なる Origin に対して Cookie は送信されません。 (自分はこれにハマってしまったのでお気をつけて... :confounded: )

Origin をまたいだ XMLHttpRequest で Cookie を送りたい場合、
Cookie の送受信を許可するために、クライアントサイド・サーバーサイドに実装が必要になります。

Client Side

XHR を使う場合

const xhr = new XMLHttpRequest();
xhr.withCredentials = true; // ここを追加。

Fetch API を使う場合

fetch('https://trusted-api.co.jp', {
  mode: 'cors',
  credentials: 'include' // ここを追加。
});

axios を使う場合

詳しい説明は不要かと思うのでしませんが、クライアントサイドの HTTP リクエストのデファクトスタンダードとして 皆さん axios を使っているかと思います。

https://github.com/axios/axios

axios を使う場合はこんな感じになります。

axios.get('https://trusted-api.co.jp', { 
  withCredentials: true
});

axios.defaults.withCredentials = true; // global に設定してしまう場合

Server Side

サーバーサイドの実装では一つ注意点があります。

Access-Control-Allow-Origin*(ワイルドカード) を設定していると以下のようなエラーが返されてしまいます。

Access to XMLHttpRequest at 'http://b.com' from origin 'a.com' has been blocked by CORS policy:
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'.
The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

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'.

credentials mode (withCredentials パラメータを着けている場合) では Access-Control-Allow-Origin*(ワイルドカード) だとダメとのこと。

なので、このように Origin を明示的に指定する必要があります。

Access-Control-Allow-Origin: https://trusted-one.co.jp // CORS を許可する Origin を明示的にする
Access-Control-Allow-Credentials: true

ちなみに、Node.js で cors module を使う場合は以下のような実装で解決できました。

https://expressjs.com/en/resources/middleware/cors.html

import cors from 'cors';

const app = express();
app.use(cors({ origin: true, credentials: true }));

origin: true は OK で origin: '*' は NG ... 違いはあまりなさそうですけどね... 詳しい方解説募集中です :bow:

※ cors module のパラメータの説明。

origin: Configures the Access-Control-Allow-Origin CORS header. Possible values:
Boolean - set origin to true to reflect the request origin, as defined by req.header('Origin'), or set it to false to disable CORS.
String - set origin to a specific origin. For example if you set it to "http://example.com" only requests from “http://example.com” will be allowed.
RegExp - set origin to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern /example\.com$/ will reflect any request that is coming from an origin ending with "example.com".

CORS に対応するための具体的な実装方法の説明は以上になります。

まとめ

※ 2020/2/10 追記

これだけ色々書いておいて、クライアントサイドもサーバーサイドも実装したし大丈夫!と意気込んでテストを行った結果、以下のようなエラーに遭遇しました...
API の CORS 許可設定はしっかりしていたはずなのに... :tired_face:

Access to XMLHttpRequest at 'https://authentication.com/auth?client_id=12345&scope=openid&profile&email&response_type=code&redirect_uri=http://api.com/auth/cb&state=abcdefg'
(redirected from 'http://api.com') from origin 'http://front-end.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

テスト時のリクエストのフローとしては以下のようになっていました。

スクリーンショット 2020-02-12 11.58.20.png

エラーの内容をよーく見てみると... :eyes:

(redirected from 'http://api.com') from origin 'http://front-end.com' has been blocked by CORS policy

これですね... API の CORS の許可設定していたので問題ないかと思っていたのですが、

ブラウザが最初にアクセスする URL は Front End (front-end.com) であり、API でリダイレクトされて認証サーバにリクエストが飛ばされた結果、認証サーバでは Front End (front-end.com) の Origin の許可設定がされていないためエラーを返されてしまったわけです...。

たとえ API に CORS の許可設定を入れていたとしても、リダイレクトされてしまうとその先のサーバが CORS の許可設定を入れていないとエラーになるという観点が抜け漏れていました... :sob:

  • 教訓

CORS エラーに遭遇した時は冷静にリクエストの流れを辿り、どのサーバーで CORS の許可設定がされていないのかを確認するようにしましょう。エラー内容から読み解くことができるはずです。

おまけ

Fetch API の mode について

  • 'cors': クロスオリジンリソース共有を実行する。
  • 'same-origin': 同一オリジン以外のアクセスはエラーになる。
  • 'no-cors': クロスオリジンリソース共有ができない場合に、エラーとはならず空のレスポンスが返却される。

express で CORS を許可する

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Credentials', true);

  if ('OPTIONS' == req.method) {
    res.send(204); // 204: No Content
  } else {
    next();
  }
});

cors モジュールを使うともっと簡単に書けます。

import cors from 'cors';
app.use(cors());

Cookie を送信するところでも軽く書きましたが、app.use(cors()); だと Full で CORS 周りの設定を許可していることになります。

Cookie を受け取りたい場合はパラメータの設定をお忘れなく。

app.use(cors({ origin: true, credentials: true }));

参照

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

att55
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした