echoとginでCORS対応するときの違いについて


この記事は、Makuake Development Team Advent Calendar 2019 18日目の記事でございます。

この記事でやりたいこと

Golangにおける主要なWebサーバーフレームワークである echogin において、CORS対策を行うときの注意点をまとめたい。

この記事を読む人の想定レベル

  • 他の言語やフレームワークでCORS対策をしたことがある人
  • HTTPリクエストヘッダとかHTTPレスポンスヘッダと言われてピンとくるひと
  • ginのCORS対策はうまくいったのにechoに変えたらうまくいかないとかそういう経験がある人

ちなみにこの記事を書いた人はチームメンバーがechoに乗り換えようとしたタイミングでCORS効かない、って困っていろいろ調べたことがある、みたいな背景があります。

TL; DR;

echoは用意されているmiddleware (labstack/echo/middleware ) を使っても403は返さず、許可しているドメインの場合に

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: 対象のOrigin情報

を追加するだけの仕様になっている。
必要なら自分で403を返させるミドルウェアを追加する。

ginは403を返すところも gin-contrib/cors のデフォルトでやってくれるので、考慮不要。

CORSとは

CORS1Cross-Origin Resource Sharing のことで、複数ドメイン間でデータを共有することをさします。

CORSそのものや、それに紐づくHTTPリクエストに関わる情報は詳しい記事がありますので、

などを参照していただくとよいかと思います。

とはいえ参考記事読んでね、ではちょっと雑すぎるので、自分なりにもCORSについて整理しておきます。

CORS対応のざっくりしたまとめ

XHRなどのブラウザ上のJavaScriptで実行されるHTTPリクエストには、デフォルトで Origin というリクエストヘッダが追加されます。
この Origin には、現在表示しているページ(アクセス元)の

  • HTTP Protocol ( https または http )
  • ホスト ( www.example.com のような情報 )
  • ポート ( :8080 など。80番/443番ポートでは省略されることもあります )

を組み合わせた、 https://www.example.com:8080 のような情報が記載されます。

このOrigin付きリクエストに対して、サーバー側でまず判定を行います。

サーバー側が許可しているアクセス元だった場合には、レスポンスヘッダに Access-Control-Allow-Origin に該当のアクセス元情報を含んだ値を返却することで、ブラウザ側で受け取った値を使用することができるようになります。

サーバー側で許可していないアクセス元の場合、サーバー側で403を返却するのが一般的ですが、 Access-Control-Allow-Origin にアクセス元の情報が含まれていなければ、ブラウザ側でもエラーを返却してくれます。

また、 Access-Control-Allow-Origin を単に返せばよいというものではなく、APIサーバーは preflight request に対応する必要があります。細かい条件は参考記事に委ねますが、ブラウザから送られてくる preflight request を受けて、「APIにアクセス可能なアクセス元」であることを事前に伝えた上で、本命のリクエストを受け付ける、という流れになります。

ginにおけるCORS対応

main.go
package main

import (
    "log"
    "time"
    "github.com/gin-contrib/cors"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    // r.Use() で使用したいミドルウェアなどの指定ができる。
    // https://godoc.org/github.com/gin-gonic/gin#RouterGroup.Use
    r.Use(cors.New(cors.Config{
        // 許可したいHTTPメソッドの一覧
        AllowMethods: []string{
            "POST",
            "GET",
            "OPTIONS",
            "PUT",
            "DELETE",
        },
        // 許可したいHTTPリクエストヘッダの一覧
        AllowHeaders: []string{
            "Access-Control-Allow-Headers",
            "Content-Type",
            "Content-Length",
            "Accept-Encoding",
            "X-CSRF-Token",
            "Authorization",
        },
        // 許可したいアクセス元の一覧
        AllowOrigins: []string{
            "https://www.example.com:8080",
        },
        // 自分で許可するしないの処理を書きたい場合は、以下のように書くこともできる
        // AllowOriginFunc: func(origin string) bool {
        //  return origin == "https://www.example.com:8080"
        // },
        // preflight requestで許可した後の接続可能時間
        // https://godoc.org/github.com/gin-contrib/cors#Config の中のコメントに詳細あり
        MaxAge: 24 * time.Hour,
    }))

    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "status": "success",
        })
    })

    if err := r.Run(); err != nil {
        log.Fatalf("main error: %s", err.Error())
    }
}

オレオレ設定がけっこう入っているので細かい指定についてはコメントなどを参考に洗い直してもらえれば、と思うんですが、一応この1ファイルで試せます。
お手元の任意のディレクトリに main.go という名前で保存して、おもむろに go run main.go してください。

サーバーが立ち上がった状態でAPIを叩くと以下のような挙動になります。

^_^ (fuji:~/Sandbox/go/gin-cors)
* curl -i -H "Origin:https://www.example.com:8080" localhost:8080
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com:8080
Content-Type: application/json; charset=utf-8
Vary: Origin
Date: Sat, 28 Dec 2019 14:48:12 GMT
Content-Length: 21

{"status":"success"}

^_^ (fuji:~/Sandbox/go/gin-cors)
* curl -i -H "Origin:https://www.example.com" localhost:8080
HTTP/1.1 403 Forbidden
Date: Sat, 28 Dec 2019 14:48:20 GMT
Content-Length: 0

スクリーンショットだとこんな感じ。2

ginは割と気が利く(?)厚めのフレームワークなので、公式の派生ライブラリである gin-contrib/cors に沿って実装すると403まで返してくれます。

echoによるCORS対応

echoはちょっとクセがあって、ginと同じやり方だとハマります。

以下は動作するサンプルコードです。

main.go
package main

import (
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
    "net/http"
)

func main() {
    e := echo.New()

    // 許可したいアクセス元の一覧
    var allowedOrigins = []string{
        "https://www.example.com:8080",
    }

    // CORS用のmiddlewareはあるものの、403を勝手に返してくれるわけではない。
    // 以下のレスポンスヘッダを付与する役割がある。
    // Access-Control-Allow-Credentials: true
    // Access-Control-Allow-Origin: https://www.example.com:8080
    e.Use(
        middleware.CORSWithConfig(middleware.CORSConfig{
            AllowCredentials: true,
            AllowOrigins:     allowedOrigins,
            AllowHeaders: []string{
                echo.HeaderAccessControlAllowHeaders,
                echo.HeaderContentType,
                echo.HeaderContentLength,
                echo.HeaderAcceptEncoding,
                echo.HeaderXCSRFToken,
                echo.HeaderAuthorization,
            },
            AllowMethods: []string{
                http.MethodGet,
                http.MethodPut,
                http.MethodPatch,
                http.MethodPost,
                http.MethodDelete,
            },
            MaxAge: 86400,
        }),
    )

    // echoで起動しているAPIサーバーに、Originが不正な場合に403を返却させるには、自分でミドルウェアを書く必要がある
    e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // Originヘッダの中身を取得
            origin := c.Request().Header.Get(echo.HeaderOrigin)
            // 許可しているOriginの中で、リクエストヘッダのOriginと一致するものがあれば処理を継続
            for _, o := range allowedOrigins {
                if origin == o {
                    return next(c)
                }
            }
            // 一致しているものがなかった場合は403(Forbidden)を返却する
            // ginと処理を合わせるなら return c.NoContent(http.StatusForbidden) のがいいかも。
            return c.String(http.StatusForbidden, "forbidden")
        }
    })

    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "success")
    })
    e.Logger.Fatal(e.Start(":1323"))
}

ginと同じ感じでAPIを叩いてもらうと以下のような挙動になります。

^_^ (fuji:~/Sandbox/go/echo-cors)
* curl -i -H "Origin:https://www.example.com:8080" localhost:1323
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://www.example.com:8080
Content-Type: text/plain; charset=UTF-8
Vary: Origin
Date: Sat, 28 Dec 2019 15:10:18 GMT
Content-Length: 7

success
^_^ (fuji:~/Sandbox/go/echo-cors)
* curl -i -H "Origin:https://www.example.com" localhost:1323
HTTP/1.1 403 Forbidden
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: 
Content-Type: text/plain; charset=UTF-8
Vary: Origin
Date: Sat, 28 Dec 2019 15:10:22 GMT
Content-Length: 9

forbidden

蛇足

echoが403を返さない仕様なのは
https://github.com/labstack/echo/blob/master/middleware/cors.go#L67
この辺りから読んでいくとだいたい書いてある感じです。

ginが403を返す仕様になっているのは、
この辺から追っかけていって、
https://github.com/gin-contrib/cors/blob/master/cors.go
最終的にここで書いてあります。
https://github.com/gin-contrib/cors/blob/master/config.go#L72

サンプルがほぼこの記事における語りたいことの全てなので、だいたいこんなところです。
(echoで403返させるところはもっとかっこいい書き方があるかもしれませんので、思いつく方はぜひ教えてください!)

--

いつもの

さて、Makuakeでは、一緒に働きたいエンジニアを募集しています。
いろいろと新しく面白い試みも続けておりますので、Advent calendarを読んで興味が湧いた方は、ぜひチームの募集にもお目通しください!


  1. W3Cでも仕様策定されているようですね。  

  2. 顔文字のところはbashを微妙にカスタマイズしてるだけなので特に気にしなくて大丈夫です。