毎回調べるのをいい加減やめたい!ということで、ぱっと説明できるようになることを目指してCORSについてまとめました。
CORSとは
Cross-Origin Resource Sharing: オリジン間リソース共有
オリジンを跨いだリクエストのレスポンスに対して、フロントエンドのJavaScriptコードへアクセス権を与えるようにブラウザに指示するための仕組みを指します。この指示にはHTTPヘッダーを利用します。
参考: オリジン間リソース共有 (CORS)
参考: CORS
XMLHttpRequestやFetch APIなどは同一オリジンポリシーに従うため、オリジンを跨いだリクエストのレスポンスにフロントエンドのJavaScriptコードは(そのままでは)アクセスできません。
参考: 同一オリジンポリシー
同一オリジン
Webコンテンツの世界でのオリジン(Origin)とは、Webコンテンツにアクセスするために使われるURLのスキーム(プロトコル)、ホスト(ドメイン)、ポート番号によって定義されます。これら3つ全て一致した場合に、同じオリジンである(同一オリジン)といえます。
参考: Origin
同一オリジンの例
スキーム(http)、ホスト名(example.com)、ポート番号(80)が同じなので同一オリジンと言える。ファイルパス(app1とapp2)が異なるのは関係ない。
http://example.com/app1/index.html
http://example.com/app2/index.html
異なるオリジンの例
異なるスキーム(httpとhttps)を使用しているため、同一オリジンとは言えない。
http://example.com/app1
https://example.com/app2
異なるポート番号(80と8080)を使用しているため、同一オリジンとは言えない。
http://example.com
http://example.com:8080
オリジン間リクエスト
オリジン間リクエストとは、オリジンを跨いだリクエストのことです。例えば、 https://domain-a.com
で提供されているWebアプリのフロンドエンドのJavaScriptコードがXMLHttpRequestを使用して https://domain-b.com
へリクエストを行うような場合を指します。
XMLHttpRequestは同一オリジンポリシーに従うため、https://domain-b.com
へのリクエストのレスポンスには(そのままでは)フロントエンドのJavaScriptコードでアクセスできません。
許可するためにはサーバーサイド側から「オリジン間リクエストOKだよ」とHTTPヘッダーを使ってクライアント側に知らせてやる必要があります。
試してみる
単純な例を使って、ローカル環境で試してみます。
今回はクライアントアプリとサーバーサイドアプリを別のポートとして立ち上げることで、オリジンを跨いだリクエストが発生するようにしてみます。
以下を参考にしています。
単純リクエスト
実施環境
- Apple M1 Pro
- バージョン13.1
- Chrome
利用した技術、フレームワークなど
クライアントアプリ
- React
-
create-react-app を使ってアプリを作り、
npm start
でサーバーを立ち上げる方式
-
create-react-app を使ってアプリを作り、
- バックエンドとの通信には axios を利用
- localhost:3000 で起動
画面真ん中に「Request」ボタンを置いています。
このボタンを押すと、バックエンドへリクエスト(http://localhost:8080/ping
)を投げます。
サーバーサイドアプリ
- Go, Gin
- localhost:8080 で起動
Get
メソッドで /ping
にアクセスすると、 pong
と返ってきます。こちらのパスをクライアントアプリから叩く想定です。
main
のコードは以下です。
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
r.Run(":8080")
}
以下では、Chromeを使ってアプリを実行し、ChromeのDevToolsを使って通信を確認します。
サーバーサイドアプリに直接アクセスしてみる
素の状態でどんなもんなのか、ということを確かめるためにも、サーバーサイドアプリにブラウザから直接アクセスしてみます。
Requestヘッダー
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: ja
Connection: keep-alive
Host: localhost:8080
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
レスポンスも返ってきています。

特に問題はありません。
対策をせずにリクエストをしてみる
続いて、クライアントアプリに用意したボタンを押してみます。クライアントは3000ポート、サーバーサイドは8080ポートで動いているため、オリジン間リクエストになります。
Requestヘッダー
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost:8080
Origin: http://localhost:3000
Referer: http://localhost:3000/
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
「サーバーサイドアプリに直接アクセスしてみる」の場合と大きく異なるのは「Origin」ヘッダーがあることです。
オリジン間通信の場合、自動的にリクエストヘッダーにOriginヘッダーが追加されます。
Responseヘッダー
Content-Length: 4
Content-Type: text/plain; charset=utf-8
Date: Mon, 27 Mar 2023 00:55:58 GMT
ブラウザのコンソール
Access to XMLHttpRequest at 'http://localhost:8080/ping' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
ブラウザ側ではエラーが表示されており、レスポンスを受け取れていません。
サーバーサイドのログ
[GIN] 2023/03/27 - 09:55:58 | 200 | 111.75µs | ::1 | GET "/ping"
サーバーサイドのログを見てみると、200を返せています。よって「クライアントアプリのJavaScriptコードがレスポンスにアクセスできていない」ということが言えると思います。
CORS対策を行なってリクエストをしてみる
サーバーサイド側で、他オリジンからのアクセスを許可するヘッダーを指定するようにします。
サーバーサイドアプリのコードを以下のように書き換えます。
※今回使っているGinというフレームワークで使える便利なCORS対策用のモジュールがあるのですが、今回は純粋にヘッダーを追加してみます。
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.String(http.StatusOK, "pong")
})
r.Run(":8080")
}
Access-Control-Allow-Origin
というヘッダーに対し *
という値をつけてレスポンスを返すようにします。
*
なので、全てのオリジンに対して許可しています。
この状態で再度、ブラウザからボタンを押してオリジン間アクセスを発生させてみます。
Requestヘッダー
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost:8080
Origin: http://localhost:3000
Referer: http://localhost:3000/
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
オリジン間アクセスとなるため、「Origin」ヘッダーが自動的に追加されています。先ほどと変わりはありません。
Responseヘッダー
Access-Control-Allow-Origin: *
Content-Length: 4
Content-Type: text/plain; charset=utf-8
Date: Mon, 27 Mar 2023 00:58:25 GMT
サーバーサイドアプリで実装した通り、Access-Control-Allow-Origin: *
ヘッダーが付与されています。
ブラウザのコンソール
エラー等、何も表示されません。
サーバーサイドのログ
[GIN] 2023/03/27 - 09:58:25 | 200 | 93.75µs | ::1 | GET "/ping"
変わらず200を返せています。
ブラウザ

結果を表示できています!
だいぶシンプルな例ですが、CORS対策について順を追って試してみました。