目次
はじめに
SPA で Session を使おうとしたら CORS の影響で Cookie を使うまでに苦労したので、簡単にまとめた
前提知識
Cross-Origin Resource Sharing(オリジン間リソース共有) 、略してCORS
Origin とは、例えば https://sat.ne.jp:8080 のような「スキーマ://ドメイン:ポート番号」のこと
CORS とは Origin が異なるリソースの共有を許可する仕組み
逆に言うと「リソースを共有する場合は制約がある」ということ
SPA の場合、フロントエンド(アプリ)とバックエンド(API)で Origin が異なる(場合が多いと思う)
例えば、React サーバを http://localhost:3000 、API サーバを http://localhost:3001 で開発していたら「CORS の制約で API が呼べない、Session や Cookie が使えない」で困った、というお話
検証環境
説明に関係するものだけ
| 環境 | ライブラリ | バージョン | 備考 |
|---|---|---|---|
| フロントエンド(アプリ) | React | 18.2.0 | typescript で使用 |
| axios | 0.27.2 | APIクライアント | |
| openapi-generator | 6.0.1 | コードジェネレータ | |
| バックエンド(API) | Go | 1.19 | |
| Chi | 5.0.7 | HTTPルータ | |
| oapi-codegen | 1.11.0 | コードジェネレータ |
対応内容
以下の4つの対応をした。
- axios で
withCredentials=trueを設定する - API サーバでプリフライトリクエストに対応する
- API サーバで Cookie のセキュリティ対応をする
- Go の
http.ResponseWriterには書き込み順序がある
axios で withCredentials=true を設定する
axios はデフォルトでは Cookie を扱わない。ブラウザの Cookie を API サーバに送らないし、レスポンスの Set-Cookie も処理しない
axios に Cookie を送受信させたいときは withCredentials=true を設定する
具体的には下のようにした
const res = await api.postLogin({
email: email,
password: hash,
}, {
withCredentials: true,
})
postLogin() は openapi-generator で生成したメソッド。axios を素で使う場合はこうなる?(やったことない)
axios.post(url, data, {
withCredentials: true
})
フロントエンドの対応はこれだけ
API サーバでプリフライトリクエストに対応する
プリフライトリクエストとは、Origin の異なる相手にリクエストを行う前に、安全な相手かどうかを確認するリクエストのこと
CORS の仕組みのひとつ(と思う)
フロントエンドから API を呼ぶと「プリフライトリクエスト → APIリクエスト」のふたつのリクエストが API サーバに送信される
API サーバがプリフライトリクエストにレスポンスしないと、ブラウザは危険な相手と思って API リクエストを送信しない。つまり API を使えない
なので、API サーバでプリフライトリクエストを処理しなければならない
具体的には下のようにした
import (
// 前略
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
)
func SetupHandler() {
r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"http://*", "https://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowCredentials: true,
MaxAge: 300,
}))
// 後略
}
Cookie を受け取るには API サーバも AllowCredentials: true で許可しなければならない
なお、AllowCredentials: true とする場合は AllowedOrigins をワイルドカード * だけにするのは禁止されている、という点だけ注意(http://* とかは構わないらしい)
API サーバで Cookie のセキュリティ対応をする
これも CORS の制約で、セキュアでない Cookie はブラウザに無視される
対応方法は以下のどちらか
- API サーバを HTTPS にする
- Cookie を
SameSite=NoneとSecure=trueにする
API サーバを HTTPS にする
API サーバを HTTPS にするにはhttp.Handle() ではなくて http.ListenAndServeTLS() でサーバを始める
なお、API のコード(codegen.HandlerFromMux() とか)は oapi-codegen で生成したもの
import (
"net/http"
// 中略
)
func SetupHandler() {
// 中略
// HTTP
// http.Handle("/", codegen.HandlerFromMux(&API{}, r))
// HTTPS
m := codegen.HandlerFromMux(&API{}, r)
if err := http.ListenAndServeTLS(":3001", "./cert.pem", "./key.pem", m); err != nil {
panic(err)
}
}
なお、証明書と鍵(cert.pem, key.pem)は ↓ で生成した
go run /usr/local/go/src/crypto/tls/generate_cert.go -rsa-bits 2048 -host localhost
Cookie を SameSite=None と Secure=true にする
HTTP の場合は Cookie に SameSite=None と Secure=true を設定しないと Set-Cookie がブラウザに無視される
http.SetCookie(w, &http.Cookie{
Name: "foo",
Value: "bar",
Path: "/",
SameSite: http.SameSiteNoneMode,
Secure: true,
})
Go の http.ResponseWriter には書き込み順序がある
w.Header().Set() → w.WriteHeader() → w.Write() の順番でないといけない
Set-Cookie も w.Header().Set() なので、w.WriteHeader() や w.Write() よりも前にしておかないと、レスポンスヘッダに書き込まれない
処理の最後に Cookie をしていたらレスポンスヘッダに Set-Cookie がなくて???となってたら順序の問題だと知って脱力した…
おわりに
以上の対応をすることで SPA で API を通じて Cookie を扱うことができるようになった
つまづく箇所が複数あってすべての正解にたどり着くのに苦労したので、ネットの情報の寄せ集めではあるが備忘録としてまとめた
