【HTTPヘッダー】CORSの仕組みとGo+GinによるCors設定の実践


はじめに

localhost:80で立てたWebサーバー(nginx)から
localhost:8080に立てたAPIサーバー(golang)にリクエストを送った時に、
CORSエラーが発生し、APIが叩けないところでハマったので、
学びとしてまとめます。

そもそもHTTPとは?

HTTP は、 HTML 文書などのリソースを取り出すことを可能にするプロトコルです。これはウェブにおけるデータ交換の基礎をなし、クライアントサーバープロトコルであり、リクエストは受け取り者 (一般にはウェブブラウザー) が生成します。文書全体は、テキスト、レイアウトの定義、画像、動画、スクリプトなど、取り込まれたさまざまなサブ文書から再構成されます。
参考元:HTTP の概要

  • プロトコルの一種
  • ウェブにおけるデータ交換の基礎
  • シンプルで人間が読めるように設計されている
  • HTTPヘッダーで、プロトコルの拡張や検証が簡単
  • ステートレスだけど、HTTP Cookie によってステートフルなセッションを実現
  • HTTP は TCP 標準に依存。
    • TCPはハンドシェイクによって通信の信頼性を担保、つまり通信相手の応答があってはじめて通信を開始するプロトコルを採用

HTTPで制御できること

ざっくりと

  • キャッシュ: html documentsとか
  • オリジン制約のコントロール: CORS(Cross Origin Resource Sharing)の話
  • 認証: Basic認証とか
  • プロキシとトンネリング: World Wide Webにアクセスする時にプロキシや別のネットワークを経由してデータを送ったりできるようにする
  • セッション:クッキーを使ってセッションを保てる

HTTPヘッダーの役割

HTTP ヘッダーにより、クライアントやサーバーが HTTP リクエストやレスポンスで追加情報を渡すことができます。 HTTP ヘッダーは、大文字小文字を区別しないヘッダー名とそれに続くコロン (:)、 値で構成されます。値の前にあるホワイトスペースは無視されます。
参考元:HTTP ヘッダー

なるほど。
HTTPヘッダーはそもそも拡張用のもので、
情報を追加して、独自の制約を持たせたり、
通信のルールを定めて、安心安全かつ用途に合わせたやりとりを実現できる
ってことなんだろうな。

種類 説明
一般ヘッダー /
Generals
リクエストとレスポンスの両方に適用される。
本文で転送されるデータとは関係ない
リクエストヘッダー /
Request Headers
読込むリソースやリクエストしているクライアントに関する詳細な情報を保持
レスポンスヘッダー /
Response Headers
レスポンスに関する追加情報
例えば場所や提供しているサーバーに関するものを保持

CORSとHTTPヘッダーの関係

オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。
参考元:オリジン間リソース共有 (CORS)

つまり、CORSに関連する専用のHTTPヘッダー情報があるってことで、
その情報を使って、CORSを実現、かつ制御できると言うことだな。

セキュリティ上の理由から、ブラウザーは、スクリプトによって開始されるオリジン間 HTTP リクエストを制限しています。例えば、 XMLHttpRequestや Fetch API は同一オリジンポリシーsame-origin policyに従います。つまり、これらの API を使用するウェブアプリケーションは、そのアプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンの場合は正しい CORS ヘッダーを含んでいることが必要です。
参考元:オリジン間リソース共有 (CORS)

知らなかった〜〜〜〜〜orz
自分は、JavascriptのFetch APIを使ってHTTPリクエストを投げたから、
same-origin policyに反して、CORS制限が発動し、
かつ、HTTPヘッダーに適切な設定をしてなかったから、
APIサーバーはリクエストを拒否ってきたのか。

CORSリクエストを成功させるには?

各種HTTPヘッダー情報を適切に設定する必要がある。

種類 説明
Access-Control-Allow-Origin リクエストの送信元の指定 http://localhost
Access-Control-Allow-Credentials 資格情報(Cookie、認証ヘッダー、TLSクライアント証明書)の送信をOKするか true
Access-Control-Allow-Headers リクエスト間に使用できるHTTPヘッダーを指定 Accept, Content-Type
Access-Control-Allow-Methods 使用できるメソッドを指定 GET, POST, HEAD
Access-Control-Expose-Headers ヘッダー名を羅列して、レスポンスの一部として開示するものを指定
既定のセーフリストは7つだけだから
Content-Length
Access-Control-Max-Age プリフライトリクエストの結果をキャッシュしてよい期間を指定 86400
Access-Control-Request-Headers 実際のリクエストで使うHTTPヘッダーをサーバーに知らせる目的
プリフライトリクエストで使用
Accept, Content-Type
Access-Control-Request-Method 実際のリクエストで使うHTTPメソッドをサーバーに知らせる目的
プリフライトリクエストで使用
GET, POST, HEAD

CORSでプリフライトを引き起こさないためには

そもそもプリフライトとは

「プリフライト」リクエストは始めに OPTIONS メソッドによる HTTP リクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうかを確かめます。サイト間リクエストがユーザーデータに影響を与える可能性があるような場合に、このようにプリフライトを行います。
参考元:プリフライトリクエスト

安全確認のためのリクエストってことすな。

プリフライトしない条件

以下のすべての条件を満たすもの。

  • メソッド
    • GET
    • HEAD
    • POST
  • HTTPヘッダー
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • XMLHttpRequestUpload にもイベントリスナーが登録されていない
  • ReadableStream オブジェクトが使用されていない

実際に確認してみる。

とりあえずgin+corsで確認してみる。
※下記、コードは色々、省略してるので、コピーでは動きません。

fetch
//一部の記述です
fetch(url_string + queryParams,
        {
          method: 'GET',//<-これをいじって検証
          mode: 'cors',
          headers: {
            'Content-Type': 'application/json',//<-これをいじって検証
          }
        })
main.go
import (
    "github.com/gin-contrib/cors"
    "github.com/gin-gonic/gin"
)

func main() {
    // Start HTTP server
    r := gin.Default()

    // ここからCorsの設定
    // *****CORS設定をいじくって検証********
    r.Use(cors.New(cors.Config{
        // アクセス許可するオリジン
        AllowOrigins: []string{
            "http://localhost",
        },
        // アクセス許可するHTTPメソッド
        AllowMethods: []string{
            "POST",
            "GET",
            "OPTIONS",
        },
        // 許可するHTTPリクエストヘッダ
        AllowHeaders: []string{
            "Content-Type",
        },
        // cookieなどの情報を必要とするかどうか
        AllowCredentials: false,
        // preflightリクエストの結果をキャッシュする時間
        MaxAge: 24 * time.Hour,
    }))
    r.GET("/scrape", scrapeText)

    if err := r.Run(":8080"); err != nil {
        log.Fatal(err)
    }
}

リクエストではContent-Typeを送るけど、サーバーサイドでは許していないパターン

Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response

ヘッダーを許可してないよと怒られた。

リクエストではPUTで送るけど、サーバーサイドでは、PUTメソッドを許していないパターン

Method PUT is not allowed by Access-Control-Allow-Methods in preflight response

PUTメソッド許可してないよと怒られた。

"リクエストではContent-Typeを送るけど、サーバーサイドでは許していないパターン

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

Originを許可してないよって怒られた。
※オリジンとリファラーって最後の/があるかないかで微妙に違うから気をつけないと。。。

ちなみに、条件に合わせたらプリフライトが飛ばなくなった

'Content-Type': 'text/plain',に変更してみたら、リクエストが1回のみになった。

変更前は、プリフライトある場合は、2回APIがたかれている。

application/jsonは許可されていないので、
いわゆるSimple requests(プリフライトが怒らないやつ)の条件から外れてしまうわけですねぇ

まとめ

CORSでハマって、なんやねんなんやねん、パソコンたたき割ったろかぁぁぁあああ!!!
って数時間ずっと苦しみましたが、なかなかの学びと成果につながりました。

検証したから、しっかりと身を持って体験できたことで、実感が湧きました。

変毒為薬っすな。

HTTPヘッダーさんとCORSさんが、こんな仕組みになってるなんて知らなかったワイ✨

以上、ありがとうございました。

引き続き頑張ります٩( ᐛ )و