Go で Cookie とセッションを扱う
HTTP はステートレスなプロトコルなので、リクエストをまたいで状態を保持するには Cookie やセッションの仕組みが必要になります。Go の標準ライブラリだけで Cookie の読み書きができ、セッション管理もシンプルに実装できます。
Cookie を設定する
http.SetCookie でレスポンスに Cookie を付与します。
http.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "username",
Value: "alice",
Path: "/",
MaxAge: 3600,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, cookie)
fmt.Fprintln(w, "Cookie を設定しました")
})http.Cookie 構造体のフィールドはそれぞれセキュリティに関わる重要な設定です。
| Name | Cookie の名前 |
| Value | 格納する値 |
| Path | Cookie が有効なパス |
| MaxAge | 有効期限(秒)。0 でセッション Cookie |
| HttpOnly | JavaScript からのアクセスを禁止 |
| Secure | HTTPS 通信でのみ送信 |
| SameSite | クロスサイトリクエストでの送信制御 |
HttpOnly を true にすると、JavaScript の document.cookie から Cookie にアクセスできなくなります。XSS 攻撃による Cookie の窃取を防ぐための基本的な対策なので、セッション用の Cookie では必ず設定しましょう。
Cookie を読み取る
クライアントから送られてきた Cookie は r.Cookie で取得します。
http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("username")
if err != nil {
if err == http.ErrNoCookie {
fmt.Fprintln(w, "Cookie が見つかりません")
return
}
http.Error(w, "Cookie の読み取りエラー", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "こんにちは、%s さん\n", cookie.Value)
})指定した名前の Cookie が存在しない場合は http.ErrNoCookie が返ります。このエラーを明示的にチェックすることで、「Cookie がない」のと「別のエラーが起きた」のを区別できます。
Cookie を削除する
Cookie を削除するには、同じ名前の Cookie を MaxAge: -1 で上書きします。
http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "username",
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, cookie)
fmt.Fprintln(w, "Cookie を削除しました")
})HTTP の仕様上、サーバーからクライアントの Cookie を直接削除する手段はありません。「有効期限切れの Cookie を送る」ことで、ブラウザに削除させるのが唯一の方法です。
メモリベースのセッション管理
Cookie にはセッション ID だけを保存し、実際のデータはサーバー側に持つのがセッション管理の基本パターンです。簡易的な実装を見てみましょう。
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"sync"
)
var (
sessions = make(map[string]map[string]string)
mu sync.Mutex
)
func generateSessionID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func getSession(r *http.Request) (string, map[string]string) {
cookie, err := r.Cookie("session_id")
if err == nil {
mu.Lock()
data, ok := sessions[cookie.Value]
mu.Unlock()
if ok {
return cookie.Value, data
}
}
return "", nil
}crypto/rand で暗号学的に安全なランダムバイトを生成し、16 進文字列に変換してセッション ID としています。math/rand ではなく crypto/rand を使うのが重要で、推測可能なセッション ID はセッションハイジャックの原因になります。
ログインとセッション作成
ユーザーがログインしたら、セッションを作成して Cookie にセッション ID を保存します。
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
if username == "" {
http.Error(w, "username は必須です", http.StatusBadRequest)
return
}
sessionID := generateSessionID()
mu.Lock()
sessions[sessionID] = map[string]string{"username": username}
mu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
MaxAge: 86400,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
fmt.Fprintf(w, "%s としてログインしました\n", username)
})ユーザーがログイン情報を送信
サーバーがセッション ID を生成
セッションデータをメモリに保存
セッション ID を Cookie で返却
セッションを使った認証チェック
保護されたエンドポイントでは、Cookie からセッション ID を取得し、サーバー側のセッションデータと照合します。
http.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
_, data := getSession(r)
if data == nil {
http.Error(w, "ログインしてください", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "プロフィール: %s\n", data["username"])
})この実装はメモリ上にセッションを保存しているため、サーバーを再起動するとすべてのセッションが消えます。本番環境では Redis やデータベースに保存するのが一般的です。
実装が簡単で高速。ただしサーバー再起動で消え、複数サーバーでの共有ができない。
永続化と複数サーバー間の共有が可能。外部依存が増えるが、本番運用には必須。
また、gorilla/sessions のようなライブラリを使えば、セッションの暗号化やストア切り替えがより簡単に行えます。まずは標準ライブラリで仕組みを理解し、要件に応じてライブラリの導入を検討するのがよいでしょう。