CORS を分かってないから動くコード書いて理解する

CORS がわからない。

エンジニアになって1年以上たちますが、CORS について何も理解しないままやってきました。
何度か目にしたことはあったのですが、そのたびに "なんとなく" のコードを貼り付けて、やり過ごしてまいりました。

そんな僕は、あるとき "ひどい" 一日を過ごすことになります。CTOから「俺、明日休むからこのタスクおわらしといてねー」という案件がありました。普段使ってない言語とフレームワークだけど、なんとかなるだろうと取り組んだのですが、完全に迷子になって何も片付けることができませんでした。翌日はCTOに :poop: 呼ばわり。

もうこんな悲しみを味わいたくない、という思いから記事にまとめます。

# 理解するためのステップ

- まずは、Go で json を返すサーバをたてる
- PHP でクライアント用のサーバをたてる
- PHP から Go へ `XMLHttpRequest` を使ってリクエスト投げる
- GET をできるようにする
- ただの POST は GET と同じ設定で動くことを確認する
- POST かつ `application/json` で プリフライトリクエストになることを確認する
- POST かつ `application/json` で動くようにする
- Cookie が取得できないことを確認
- Cookie を取得できるようにする

# 定義的なもの


> Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to let a user agent gain permission to access selected resources from a server on a different origin (domain) than the site currently in use. 

from [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)

（ざっくり訳）
CORS = HTTP ヘッダーを使うことで、異なるドメインのリソースへアクセスできるようになる仕組み。


# なぜ理解できないのか？

「なんでいつまでたっても理解してないんだろう？」

と考えた結果、[ドキュメント](https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control)をちょこちょこ読んで、「なんとなく、こんな感じかぁ」では、ダメだと思いました。ちゃんとサーバ用意してクライアントからリクエストして、ヘッダーをひとつひとつ変えてみて、どんな挙動になるのか？どんなエラーがでるのか？を見ないと僕は理解できないんだ。と至りました。

ということで、上のステップをひとつずつ進めたいと思います。


# Step1. Go で json 返すサーバを書く

まずは、Go でサーバを書きます。

```go:server.go
package main

import (
	"net/http"
	"encoding/json"
	"log"
)

type Ping struct {
	Status int `json:"status"`
	Result string `json:"result"`
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
	ping := Ping{http.StatusOK, "ok"}
	res, _ := json.Marshal(ping)

	w.Header().Set("Content-Type", "application/json")
	w.Write(res)
}


func main() {
	var httpServer http.Server
	http.HandleFunc("/", rootHandler)
	httpServer.Addr = ":8888"
	log.Println(httpServer.ListenAndServe())
}
```

※ 上のコードはみやすさのため書いていませんが、実際はログとか見たかったので、[こんな感じ](https://github.com/mochizukikotaro/cors-sample/blob/master/server.go#L18)でやってました。


```bash
$ go run server.go

# または、realize など。ぼくは開発中に再起動がめんどうなのでコレをつかっています。
$ realize start
```

これで、カール叩くと以下を得ます。

```bash
$ curl http://localhost:8888
{"status":200,"result":"ok"}%
```

# Step2. PHP でクライアント用サーバをたてる

続いて、クライアント側として、PHP と JS を書きます。

```bash
$ tree .
.
├── index.php
└── script.js
```

`XMLHttpReques` でのリクエストの状況は、Chrome のデベロパーツールで見る気概なので、余計なことはしてません。

```php:index.php
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="./script.js"></script>
    <title>CORS PHP Client</title>
  </head>
  <body>
    Hello PHP
  </body>
</html>
```

まずは、GET で json をとりに行くだけです。

```script.js
var xhr = new XMLHttpRequest()
var url = 'http://localhost:8888/'

const handler = () => {
  // コンソールに出力
  console.log(xhr.responseText)
}

const getRequest = () => {
  xhr.open('GET', url)
  xhr.onloadend = handler
  xhr.send()
}

document.addEventListener('DOMContentLoaded', () => {
  getRequest()
})

```

```bash:起動します。
$ php -S localhost:9999
```

これで準備ができました。

# Step3. さっそく localhost:9999 (PHP) にアクセスして、エラーを眺める

では、意気揚々と `http://localhost:9999/` へアクセスしてみます。 xhr で Go 側へのリクエストが走っていますので、基本的にデベロッパーツールを眺めます。 :eyes: 

![スクリーンショット 2017-11-03 20.30.31.png](https://qiita-image-store.s3.amazonaws.com/0/45822/b4c95906-cb39-95ca-e3ce-b9454a6c76fa.png)


無事に、 以下のエラーを発生させることに成功しました。

```js
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'http://localhost:9999' is therefore not allowed access.
```

`Access-Control-Allow-Origin` ヘッダーが無いぞ。と言われていますので、まずはそれを追加してみるところからです。


# Step4. GET をできるようにする

```diff:server.go
func rootHandler(w http.ResponseWriter, r *http.Request) {
	ping := Ping{http.StatusOK, "ok"}
	res, _ := json.Marshal(ping)

	w.Header().Set("Content-Type", "application/json")

+	// ここを追加
+	w.Header().Set("Access-Control-Allow-Origin", "*")

	w.Write(res)
}
```

これで、再度 `http://localhost:9999/` へアクセスすると以下のようになります。

![スクリーンショット 2017-11-03 20.38.56.png](https://qiita-image-store.s3.amazonaws.com/0/45822/9980da24-283a-c3f2-4668-c5c7b65a1ecd.png)

無事できました。

# Step5. POST もできるようになってる

`script.js` を以下のようにして、今度は `POST` で Go へリクエストを送ってみます。

```js:script
const postRequest = () => {
  xhr.open('POST', url)
  xhr.onloadend = handler
  xhr.send()
}

document.addEventListener('DOMContentLoaded', () => {
  postRequest()
})
```

これはなにもしなくても、Step4. と同じように成功します。

# Step.6 POST で、`application/json` にしてみる。そしてエラーを眺める。

[プリフライトリクエスト MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control#Preflighted_requests) に書いてあるように、

> GET、HEAD、POST 以外のメソッドを使用した場合。また application/x-www-form-urlencoded、multipart/form-data、または text/plain 以外の Content-Type とともに POST を使用してリクエストを行う場合、例えば application/xml または text/xml を使用して POST で XML のペイロードをサーバーへ送るときは、リクエストでプリフライトを行います。

> カスタムヘッダをリクエストに設定した場合 (例えば、X-PINGOTHER のようなヘッダを用いるリクエスト)。

`POST`でも、たとえば、 `Content-Type: application/json` としてリクエストすると、プリフライトリクエストが走ります。

ところで、

**プリフライトリクエストってなんだよ！！**


簡単にいいますと...

`POST` メソッドのリクエストの前に、`OPTIONS` メソッドが走ります...。（説明不足


とにかく！`Content-Type` を `application/json` に変更して動かしてみます。 :runner_tone5:


```js:script.js
const postJsonRequest = () => {
  xhr.open('POST', url)
  xhr.onloadend = handler
  xhr.setRequestHeader('Content-Type', 'application/json')
  xhr.send()
}

document.addEventListener('DOMContentLoaded', () => {
  postJsonRequest()
})
```

これで、`http://localhost:9999/` へアクセスすると。

![スクリーンショット 2017-11-03 20.59.42.png](https://qiita-image-store.s3.amazonaws.com/0/45822/3f36e5fe-19ee-7ae6-b5a4-8d5906bfbcce.png)

`OPTIONS` メソッドが飛んでいます。そしてエラーも。

```js
Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
```

リクエストの `Content-Type` が許可されてないそうです。

# Step.7 プリフライトのエラーを解消する

```diff:server.go
func rootHandler(w http.ResponseWriter, r *http.Request) {
	ping := Ping{http.StatusOK, "ok"}
	res, _ := json.Marshal(ping)

	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	
+	// ここを追加
+	w.Header().Set("Access-Control-Allow-Headers","Content-Type")

	w.Write(res)
}
```

これで、無事通ります。

もしも、JS からリクエストするときに、カスタムヘッダーなども追加している場合は、例えば、`X-Hoge`というカスタムヘッダーだとすると、以下のようにそれも追記しておく必要があります。

```go
w.Header().Set("Access-Control-Allow-Headers","Content-Type, X-Hoge")
```

# Step.8 Cookie を取得しようとして、エラーに遭遇しよう

JS 側で Cookie をセットするようにしてみます。

```js:script.js
const getCookieRequest = () => {
  xhr.open('GET', url)
  xhr.onloadend = handler
  xhr.send()
}

document.addEventListener('DOMContentLoaded', () => {
  document.cookie = 'hoge=123'
  getCookieRequest()
})
```

Go 側で、Cookie を読んで、それを json に入れて返却するようなコードを書きます。（もちろんまだちゃんと取得できない）

```go:server.go
func rootHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Headers","Content-Type")

	// Cookie 取得
	cookie, _ := r.Cookie("hoge")
	var result string
	if cookie != nil {
		result = cookie.Value
	} else {
		result = "Cookie 取得できませんでした"
	}
	ping := Ping{http.StatusOK, result}
	res, _ := json.Marshal(ping)
	w.Write(res)
}
```

この状態で、`http://localhost:9999/` へアクセスすると。

![スクリーンショット 2017-11-03 21.23.14.png](https://qiita-image-store.s3.amazonaws.com/0/45822/1a91695d-3450-b9f2-8c93-38d27a0343ab.png)

エラーにはならないけど、無事、Cookie が取れていない。

# Step.9 Cookie をちゃんと取得する

やることは 3 つです。

- script.js 側で、`withCredentials` を true にする
- server.go 側で、`Access-Control-Allow-Credentials` ヘッダーを設定する
- server.go 側で、`Access-Control-Allow-Origin`のワイルドカード指定をやめる


```diff:script.js
const getCookieRequest = () => {
  xhr.open('GET', url)
+  xhr.withCredentials = true
  xhr.onloadend = handler
  xhr.send()
}
```

```diff:server.go
	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
+	w.Header().Set("Access-Control-Allow-Origin", "http://localhost:9999")

	w.Header().Set("Access-Control-Allow-Headers","Content-Type")

+	// これを追加すると Cookie が取得できる
+	w.Header().Set("Access-Control-Allow-Credentials", "true")
```

これで、`http://localhost:9999/` へアクセスすると無事とれた。

![スクリーンショット 2017-11-03 21.28.26.png](https://qiita-image-store.s3.amazonaws.com/0/45822/cd7794b2-167e-37fb-b805-4bf51f3df28c.png)


以上です。


# コード

あげておきました。

[mochizukikotaro/cors-sample](https://github.com/mochizukikotaro/cors-sample)


:pear: