Help us understand the problem. What is going on with this article?

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:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away