Edited at

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

More than 1 year has passed since last update.

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

(ざっくり訳)

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


なぜ理解できないのか?

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

と考えた結果、ドキュメントをちょこちょこ読んで、「なんとなく、こんな感じかぁ」では、ダメだと思いました。ちゃんとサーバ用意してクライアントからリクエストして、ヘッダーをひとつひとつ変えてみて、どんな挙動になるのか?どんなエラーがでるのか?を見ないと僕は理解できないんだ。と至りました。

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


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

まずは、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())
}


※ 上のコードはみやすさのため書いていませんが、実際はログとか見たかったので、こんな感じでやってました。

$ go run server.go

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

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

$ curl http://localhost:8888

{"status":200,"result":"ok"}%


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

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

$ tree .

.
├── index.php
└── script.js

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


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()
})



起動します。

$ php -S localhost:9999


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


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

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

スクリーンショット 2017-11-03 20.30.31.png

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

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 をできるようにする


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

無事できました。


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

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


script

const postRequest = () => {

xhr.open('POST', url)
xhr.onloadend = handler
xhr.send()
}

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


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


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

プリフライトリクエスト MDN に書いてあるように、


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-Typeapplication/json に変更して動かしてみます。 :runner_tone5:


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

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

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

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


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


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というカスタムヘッダーだとすると、以下のようにそれも追記しておく必要があります。

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


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

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


script.js

const getCookieRequest = () => {

xhr.open('GET', url)
xhr.onloadend = handler
xhr.send()
}

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


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


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

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


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

やることは 3 つです。


  • script.js 側で、withCredentials を true にする

  • server.go 側で、Access-Control-Allow-Credentials ヘッダーを設定する

  • server.go 側で、Access-Control-Allow-Originのワイルドカード指定をやめる


script.js

const getCookieRequest = () => {

xhr.open('GET', url)
+ xhr.withCredentials = true
xhr.onloadend = handler
xhr.send()
}


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

以上です。


コード

あげておきました。

mochizukikotaro/cors-sample

:pear: