CORS がわからない。
エンジニアになって1年以上たちますが、CORS について何も理解しないままやってきました。
何度か目にしたことはあったのですが、そのたびに "なんとなく" のコードを貼り付けて、やり過ごしてまいりました。
そんな僕は、あるとき "ひどい" 一日を過ごすことになります。CTOから「俺、明日休むからこのタスクおわらしといてねー」という案件がありました。普段使ってない言語とフレームワークだけど、なんとかなるだろうと取り組んだのですが、完全に迷子になって何も片付けることができませんでした。翌日はCTOに 呼ばわり。
もうこんな悲しみを味わいたくない、という思いから記事にまとめます。
理解するためのステップ
- まずは、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 でサーバを書きます。
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 のデベロパーツールで見る気概なので、余計なことはしてません。
<!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 をとりに行くだけです。
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 側へのリクエストが走っていますので、基本的にデベロッパーツールを眺めます。
無事に、 以下のエラーを発生させることに成功しました。
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 をできるようにする
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/
へアクセスすると以下のようになります。
無事できました。
Step5. POST もできるようになってる
script.js
を以下のようにして、今度は POST
で Go へリクエストを送ってみます。
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-Type
を application/json
に変更して動かしてみます。
const postJsonRequest = () => {
xhr.open('POST', url)
xhr.onloadend = handler
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
}
document.addEventListener('DOMContentLoaded', () => {
postJsonRequest()
})
これで、http://localhost:9999/
へアクセスすると。
OPTIONS
メソッドが飛んでいます。そしてエラーも。
Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
リクエストの Content-Type
が許可されてないそうです。
Step.7 プリフライトのエラーを解消する
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 をセットするようにしてみます。
const getCookieRequest = () => {
xhr.open('GET', url)
xhr.onloadend = handler
xhr.send()
}
document.addEventListener('DOMContentLoaded', () => {
document.cookie = 'hoge=123'
getCookieRequest()
})
Go 側で、Cookie を読んで、それを json に入れて返却するようなコードを書きます。(もちろんまだちゃんと取得できない)
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/
へアクセスすると。
エラーにはならないけど、無事、Cookie が取れていない。
Step.9 Cookie をちゃんと取得する
やることは 3 つです。
- script.js 側で、
withCredentials
を true にする - server.go 側で、
Access-Control-Allow-Credentials
ヘッダーを設定する - server.go 側で、
Access-Control-Allow-Origin
のワイルドカード指定をやめる
const getCookieRequest = () => {
xhr.open('GET', url)
+ xhr.withCredentials = true
xhr.onloadend = handler
xhr.send()
}
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/
へアクセスすると無事とれた。
以上です。
コード
あげておきました。