0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

世界一わかりやすいCORS入門 ― 「ビルの受付」で理解するブラウザのセキュリティ

0
Posted at

はじめに

Access-Control-Allow-Origin がどうこう…」というあのエラー、何度見ても胃が痛くなりませんか。

CORS(Cross-Origin Resource Sharing)は、Web 開発を始めたほぼ全員が一度はぶつかる壁です。ですが、仕組みさえ理解してしまえば、対処は驚くほど単純です。

本記事では、CORS を「ビルの受付」にたとえて、その目的・動作・解決方法まで、世界一やさしい言葉で解説します。

対象読者

  • Web 開発を始めて間もない方
  • fetchaxios で API を叩いたら CORS エラーが出て困っている方
  • 「とりあえずプロキシで回避」してきたけど、ちゃんと仕組みを理解したい方

この記事でわかること

  • CORS が「そもそも何のために存在するのか」
  • ブラウザの裏側で起きている 2 種類のリクエスト (シンプル / プリフライト)
  • エラーメッセージの読み方
  • 開発・本番で使える 正しい解決方法

TL;DR

  • CORS は 「他人のサイト(オリジン)から勝手にあなたのサイトの API を叩かれないようにする」 ブラウザのセキュリティ機能。
  • ブロックしているのは ブラウザ。サーバーは「OK だよ」というお墨付き(HTTP ヘッダー)を返すだけ。
  • 解決の本筋は 「API サーバー側で適切な Access-Control-Allow-* ヘッダーを返す」 こと。
  • 開発中の応急処置として プロキシ(Vite / Next.js / webpack-dev-server) が使える。
  • フロントエンド側でヘッダーを書き換えてもエラーは消えない(よくある勘違い)。

そもそも「オリジン」とは

CORS を語る前に、まず オリジン(Origin) という用語を押さえます。

オリジンは次の 3 点セット のことです。

スキーム  ://  ホスト       :  ポート
https     ://  example.com  :  443

この 3 つのうち 1 つでも違えば「別オリジン」 扱いになります。

URL 1 URL 2 同一オリジン?
https://example.com/a https://example.com/b ⭕ 同じ
https://example.com http://example.com ❌ スキームが違う
https://example.com https://api.example.com ❌ ホストが違う
https://example.com:443 https://example.com:8080 ❌ ポートが違う

パス(/a/b の部分)はオリジン判定に 関係しません


なぜ CORS が必要なのか ―「ビルの受付」のたとえ

ここからが本題です。CORS のキモは Same-Origin Policy(同一オリジンポリシー) という、ブラウザの基本ルールにあります。

たとえ話:あなたは A 社のビルにいる

  • あなた(ブラウザ)は A 社https://a.com)のオフィスにいます。
  • A 社のオフィスから、A 社の 社内資料サーバーhttps://a.com/api)には自由にアクセスできます。
  • しかしオフィスのパソコンを使って、B 社https://b.com/api)の社内サーバーに勝手に問い合わせるのは…おかしいですよね。

このように、「自分がいるオリジンとは違うオリジンへのアクセス」を、ブラウザは原則ブロック します。これが Same-Origin Policy です。

なぜブロックする必要があるのか

たとえばあなたが 銀行サイトにログイン中 だとします。同じブラウザの別タブで悪意のあるサイトを開いたとき、もし制限がなければ、その悪意あるサイトの JavaScript が 裏であなたの銀行 API を勝手に叩けてしまう からです。

Same-Origin Policy が守っているのは「ユーザーのログインセッション」と「Cookie」です。これがなければ、Web は今のように成立していません。

CORS は「例外的に許可するための仕組み」

とはいえ、現実には 正当な理由で別オリジンの API を叩きたい ことが山ほどあります(例:https://my-app.com から https://api.my-app.com を呼ぶ)。

そこで登場するのが CORS です。CORS は、「サーバーが OK を出したオリジンに限り、アクセスを許可する」 という、Same-Origin Policy のための公式な抜け道です。

CORS = Same-Origin Policy を 緩めるための仕組み

ここを誤解すると一生迷子になります。CORS は「制限」ではなく 「許可するための仕組み」 です。


受付のやりとり ― ブラウザとサーバーの会話

ビルの受付にたとえて、ブラウザとサーバーの実際のやりとりを見てみます。

シンプルリクエスト(顔パスで通す)

GET や、ヘッダーが最小限の POST などは シンプルリクエスト と呼ばれます。受付にたとえると 「顔パスで一旦通すけど、後でハンコのチェックはする」 イメージです。

ポイントは 「ブラウザはリクエスト自体は送る」 ことです。サーバーが許可ヘッダー(Access-Control-Allow-Origin)を返さなければ、ブラウザは レスポンスを JS に渡さずに破棄 します。

ここがハマりポイント。サーバーのログには 正常リクエストとして記録される のに、フロントには CORS エラーしか見えません。「サーバーまでは届いている」という事実を知っておくとデバッグが楽になります。

プリフライトリクエスト(先に予約確認する)

PUTDELETE や、Content-Type: application/json を使う POST、独自ヘッダー(Authorization 以外のカスタムヘッダーなど)を含む場合、ブラウザは 本リクエストの前に、許可を取りに行く 動作をします。これが プリフライト(preflight) です。

受付にたとえると 「いきなり訪問する前に、電話で『〇〇社の▲▲ですが、明日 10 時にお伺いしてもいいですか?』と先に予約確認する」 ような動作です。

プリフライトの返答に必要なヘッダーが揃っていないと、ブラウザは本リクエストを送りません

ヘッダー 意味
Access-Control-Allow-Origin どのオリジンを許可するか(* か、特定のオリジン)
Access-Control-Allow-Methods 許可する HTTP メソッド(例:GET, POST, PUT, DELETE
Access-Control-Allow-Headers 許可するリクエストヘッダー(例:Content-Type, Authorization
Access-Control-Max-Age プリフライト結果のキャッシュ秒数(再問い合わせを減らせる)

Cookie を送るとき ― credentials

ログインセッションなど Cookie を一緒に送りたい場合は、もう一段だけルールが厳しくなります。

必要なもの
クライアント側 fetch(url, { credentials: 'include' }) / axios なら withCredentials: true
サーバー側 ① Access-Control-Allow-Credentials: true
サーバー側 ② Access-Control-Allow-Origin* は使えない。具体的なオリジンを返す。

Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true組み合わせは禁止 です。ブラウザはエラーにします。Cookie を扱うときは必ず オリジンを 1 つに絞って 返してください。


エラーメッセージの読み方

代表的なエラーを 3 つだけ。ここを押さえれば 8 割は読めます。

No 'Access-Control-Allow-Origin' header is present

Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

サーバーが許可ヘッダーを返していない。サーバー側に CORS 設定を入れる必要があります。

The 'Access-Control-Allow-Origin' header has a value '...' that is not equal to the supplied origin

→ 返しているオリジンが 要求元と一致していないhttps://app.example.com を呼んでいるのにサーバーが https://www.example.com を返している、などです。

Response to preflight request doesn't pass access control check

プリフライトで弾かれている。多くの場合、OPTIONS メソッドのレスポンスが Access-Control-Allow-MethodsAccess-Control-Allow-Headers を返していません。サーバー側のフレームワーク設定を確認しましょう。


正しい解決方法

解決方法 1:サーバー側に CORS 設定を入れる(本筋)

これが 唯一の正攻法 です。代表的な実装例を挙げます。

server/express.js
import express from 'express';
import cors from 'cors';

const app = express();

app.use(cors({
  origin: 'https://app.example.com',   // 許可するオリジン
  credentials: true,                   // Cookie を扱うなら true
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));
server/fastapi_main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Content-Type", "Authorization"],
)

Access-Control-Allow-Origin: * は便利ですが、認証付き API には絶対に使わない でください。誰でもどこからでも叩ける状態になります。許可するオリジンは必要最小限に絞り込みましょう。

解決方法 2:開発中はプロキシで回避(応急処置)

ローカル開発で「CORS のためだけにサーバーをいじりたくない」場合、フロント側の開発サーバーで プロキシ を立てると、ブラウザから見た送信先がフロントと 同一オリジン になり、CORS が発生しなくなります。

vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});

フロントからは fetch('/api/users') のように 相対パス で叩けば、開発サーバーが裏で https://api.example.com/users に転送してくれます。

プロキシは 「開発中の便利機能」 であって、CORS の根本解決ではありません。本番では別のドメイン構成になるので、最終的にはサーバー側の CORS 設定が必要です。

解決方法 3:BFF / リバースプロキシ(本番向け)

本番で「フロントと API のドメインを分けたくない」場合、Next.js の Route Handlers や Nginx のリバースプロキシで 同一オリジンに統一する 手があります。これは CORS そのものを発生させない構成です。

サーバー間の通信には Same-Origin Policy が 適用されない ので、自由にやりとりできます。


よくある勘違いトップ 5

CORS のドキュメントを読む前に、まずこれを潰しておくと迷子になりません。

勘違い 1:「CORS はサーバーがブロックしている」

ブロックしているのはブラウザです。サーバーは「許可するよ」と書かれたヘッダーを返すだけで、判断するのはブラウザ側。実は curlPostman には CORS は関係ありません

勘違い 2:「フロントの fetch にヘッダーを付ければ直る」

直りませんAccess-Control-Allow-Originレスポンスヘッダー であってリクエストヘッダーではありません。フロントからは付けようがないので、headers をいくらいじっても無意味です。

勘違い 3:「mode: 'no-cors' を付ければ解決」

解決しませんno-cors モードはレスポンスを 「不透明(opaque)」 にするだけで、JS からはレスポンス本文を読めなくなります。「エラーは消えるけど何も取れない」という最悪の状態になります。

// ❌ よくある間違い
const res = await fetch('https://api.example.com/data', { mode: 'no-cors' });
const data = await res.json();   // 何も取れない(opaque レスポンス)

勘違い 4:「同じドメインだから同一オリジンのはず」

ポート番号が違うと別オリジン です。http://localhost:3000http://localhost:8080別オリジン 扱いになります。

勘違い 5:「ブラウザ拡張で CORS を無効化すればいい」

自分の開発機ではエラーが消えますが、ユーザーの環境では発生し続けます。根本対策にはなりません。一時的な切り分け用途に留めましょう。


デバッグの定石

CORS エラーに当たったら、次の順で切り分けると速いです。

  1. ブラウザの DevTools → Network タブを開く
  2. 失敗したリクエストを選び、OPTIONS(プリフライト)が出ているか確認する
  3. レスポンスヘッダーに Access-Control-Allow-* が含まれているか を見る
  4. 含まれていなければ サーバー側の設定不足、含まれているけど値が違うなら オリジンや許可ヘッダーの不一致
  5. それでも分からなければ curl -v -H "Origin: https://app.example.com" -X OPTIONS https://api.example.com/data で生レスポンスを確認

curl でリクエストして 200 が返るのに、ブラウザでだけ CORS エラーが出る場合、それは ブラウザが正しく仕事をしている証拠 です。CORS の知識が間違っているわけではありません。


まとめ

CORS の本質は、たった 3 つです。

  1. CORS は「許可するための仕組み」(制限する仕組みではない)
  2. 判断するのはブラウザ、サーバーは許可ヘッダーを返すだけ
  3. 正しい解決はサーバー側の Access-Control-Allow-* の設定

ここさえ分かっていれば、初見のエラーも「ああ、サーバーがあのヘッダーを返してないんだな」と落ち着いて対処できます。次に CORS エラーに遭遇したときは、ぜひ DevTools の Network タブ を開いて、ブラウザとサーバーの会話を眺めてみてください。

参考資料

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?