CORSを説明できるようになる


毎回調べるのをいい加減やめたい！ということで、ぱっと説明できるようになることを目指してCORSについてまとめました。


# CORSとは

Cross-Origin Resource Sharing: オリジン間リソース共有

オリジンを跨いだリクエストのレスポンスに対して、**フロントエンドのJavaScriptコードへアクセス権を与えるようにブラウザに指示するための仕組み**を指します。この指示にはHTTPヘッダーを利用します。
参考: [オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
参考: [CORS](https://developer.mozilla.org/ja/docs/Glossary/CORS)

XMLHttpRequestやFetch APIなどは同一オリジンポリシーに従うため、オリジンを跨いだリクエストのレスポンスにフロントエンドのJavaScriptコードは(そのままでは)アクセスできません。
参考: [同一オリジンポリシー](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy)

## 同一オリジン

Webコンテンツの世界でのオリジン(Origin)とは、Webコンテンツにアクセスするために使われるURLのスキーム(プロトコル)、ホスト(ドメイン)、ポート番号によって定義されます。これら3つ全て一致した場合に、同じオリジンである(同一オリジン)といえます。
参考: [Origin](https://developer.mozilla.org/ja/docs/Glossary/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ヘッダーを使ってクライアント側に知らせてやる必要があります。

参考: [オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)

# 試してみる

単純な例を使って、ローカル環境で試してみます。
今回はクライアントアプリとサーバーサイドアプリを別のポートとして立ち上げることで、オリジンを跨いだリクエストが発生するようにしてみます。

以下を参考にしています。
[単純リクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#%E5%8D%98%E7%B4%94%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88)

### 実施環境

- Apple M1 Pro
- バージョン13.1
- Chrome


### 利用した技術、フレームワークなど

#### クライアントアプリ
- React
    - [create-react-app](https://github.com/facebook/create-react-app) を使ってアプリを作り、 `npm start` でサーバーを立ち上げる方式
- バックエンドとの通信には [axios](https://github.com/axios/axios) を利用
- localhost:3000 で起動

画面真ん中に「Request」ボタンを置いています。
このボタンを押すと、バックエンドへリクエスト(`http://localhost:8080/ping`)を投げます。

![スクリーンショット 2023-03-17 9.27.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45528/ad9dd18c-c2a6-34cb-b6d6-01e61459870f.png)

#### サーバーサイドアプリ

- Go, Gin
- localhost:8080 で起動

`Get` メソッドで `/ping` にアクセスすると、 `pong`　と返ってきます。こちらのパスをクライアントアプリから叩く想定です。

`main` のコードは以下です。

```golang
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
```

レスポンスも返ってきています。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45528/2564e2c1-bc4f-7b50-f4c2-97d832db40b3.png">

特に問題はありません。

## 対策をせずにリクエストをしてみる

続いて、クライアントアプリに用意したボタンを押してみます。クライアントは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 リクエストヘッダー](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/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対策用のモジュールがあるのですが、今回は純粋にヘッダーを追加してみます。

```golang
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` というヘッダーに対し `*` という値をつけてレスポンスを返すようにします。

[Access-Control-Allow-Origin](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/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を返せています。

### ブラウザ
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45528/98e8675e-45a9-8043-cb82-e13b967f3de5.png">

結果を表示できています！

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