Carpe Diem

備忘録

サーバサイドのCORS対応

概要

No 'Access-Control-Allow-Origin' header is present on the requested resource.というエラーが出た際に外部API(サーバ側)でどう対応すべきかをまとめました。

CORSでググると幸せになれます。

環境

  • go 1.7.4
  • gin 1.1.4

CORSが必要になるのはどんな時か

シンプルに言うと以下の条件のときです。

  • ブラウザからの外部APIへのリクエストである
  • 今開いてるページのURLとは別ドメイン、別ポートのAPIをページ内で叩こうとしている

ref: オリジン間リソース共有 (CORS) - HTTP | MDN

CORSのリクエストは2種類

以下の2つがあります。

  1. そのままAPIへリクエス
  2. 先にpreflightリクエストを送ってから、その後でAPIへリクエス

preflightリクエストを送るかどうかはブラウザが決めており、以下の条件全てを満たすリクエスト(単純リクエストと呼ばれる)以外は自動で送られます

  • HTTPメソッドがGET, POST, HEADのいずれか
  • HTTPヘッダにAccept, Accept-Language, Content-Language, Content-Type以外のフィールドが含まれない
  • Content-Typeの値はapplication/x-www-form-urlencoded, multipart/form-data, text/plainのいずれか

a. そのままAPIへリクエス

この場合は外部APIからのレスポンスヘッダに

レスポンスヘッダ 説明 具体例
Access-Control-Allow-Origin 許可するリクエスト元のURL "*", "http://www.example.com"など

が設定されていればOKです。

b. 先にpreflightリクエストを送ってから、その後でAPIへリクエス

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource

というエラーが出てる場合はこちらです。

この場合はまずはpreflightリクエストにレスポンスを返す口を用意する必要があります。
またそのレスポンスでは

レスポンスヘッダ 説明 具体例
Access-Control-Allow-Origin 許可するリクエスト元のURL "*", "http://www.example.com"など
Access-Control-Allow-Methods 次に送る実際のAPIコールで使用するメソッドを含ませる "GET,POST,PUT,DELETE"など
Access-Control-Allow-Headers 次に送る実際のAPIコールで使用するヘッダを含ませる "content-type,X-User-Id"など

という各種ヘッダをつけて返す必要があります。

Access-Control-Allow-Headersについてはpreflightリクエストが自動で次に使うヘッダの一覧をAccess-Control-Request-Headersというリクエストヘッダに追加するので、サーバ側はこれをみて返せば大丈夫です。

Access-Control-Allow-Headersのセーフリスト

Access-Control-Allow-HeadersにはCORS セーフリストリクエストヘッダーというものがあり、

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type

は常に許可されており、このヘッダーに含める必要はありません。

しかしこれらのヘッダーを含めることで追加の制約の適用を回避することができます。
Content-Type: application/jsonを使っていたり、値の長さが非常に長かったりするとこの制約にひっかかるのでその場合は明示的に書くようにしましょう。

Authorization ヘッダーはワイルドカードの対象にならない

Access-Control-Allow-Headersワイルドカード値を使うこともできますが、

Access-Control-Allow-Headers: *

Authorization ヘッダーはワイルドカードの対象にならないので明示的に列挙する必要があります。

Access-Control-Allow-Headers: *, Authorization

具体的な実装方針

ミドルウェアとして挟むのが疎に設計できて良いと思います。

github.com

こちらを参考に作ります。

前提として

  • OPTIONSは全てpreflightリクエストとみなす
  • xxx.example.comのサービスからならアクセス可能
  • ↑であれば"Access-Control-Allow-Origin"をリクエストのOriginヘッダの値にする

とします。
ginというgolangのwebフレームワークのミドルウェアとして用意すると以下のようになります。

var reg = regexp.MustCompile("https?:\\/\\/(.+)\\.example\\.com:?.*")

func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        origin := c.Request.Header.Get("Origin")

        // for preflight
        if c.Request.Method == "OPTIONS" {
            r := reg.Copy()
            if r.MatchString(origin) {
                headers := c.Request.Header.Get("Access-Control-Request-Headers")

                c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
                c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
                c.Writer.Header().Set("Access-Control-Allow-Headers", headers)

                c.Data(200, "text/plain", []byte{})
                c.Abort()
            } else {
                c.Data(403, "text/plain", []byte{})
                c.Abort()
            }
        } else {
            // for actual response
            c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
            //c.Writer.Header().Set("Access-Control-Expose-Headers", "")
            c.Next()
        }

        return
    }
}

その他

よく見るレスポンスヘッダ

レスポンスヘッダ 説明 具体例
Access-Control-Allow-Credentials cookieの送信をOKするか。
ただし"Access-Control-Allow-Origin", "*"の場合は使えない
true
Access-Control-Expose-Headers APIから返ってくるレスポンスヘッダの内、Javascriptでアクセスしてよいとするヘッダ "X-User-Id"

Access-Control-Expose-Headers

セーフリスト

以下のレスポンスヘッダに関してはAccess-Control-Expose-Headersで指定せずとも、クライアントはアクセスすることができます。

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

Authorization ヘッダーはワイルドカードの対象にならない

Access-Control-Expose-Headersワイルドカード値を使うこともできますが、

Access-Control-Expose-Headers: *

Authorization ヘッダーはワイルドカードの対象にならないので明示的に列挙する必要があります。

Access-Control-Expose-Headers: *, Authorization

CORS とキャッシング

Access-Control-Allow-Origin の値が"*"ではなく、具体的なOriginである場合、レスポンスにVaryレスポンスヘッダーに Origin という値を設定して、 Origin リクエストヘッダーの値によって値が変わることをCDNやブラウザに示してください。

Access-Control-Allow-Origin: https://xxx.example.com
Vary: Origin

ref: オリジン間リソース共有 (CORS) - HTTP | MDN

ソース