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 構造体のフィールドはそれぞれセキュリティに関わる重要な設定です。

NameCookie の名前
Value格納する値
PathCookie が有効なパス
MaxAge有効期限(秒)。0 でセッション Cookie
HttpOnlyJavaScript からのアクセスを禁止
SecureHTTPS 通信でのみ送信
SameSiteクロスサイトリクエストでの送信制御

HttpOnlytrue にすると、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 やデータベースに保存するのが一般的です。

メモリセッション

実装が簡単で高速。ただしサーバー再起動で消え、複数サーバーでの共有ができない。

Redis / DB セッション

永続化と複数サーバー間の共有が可能。外部依存が増えるが、本番運用には必須。

また、gorilla/sessions のようなライブラリを使えば、セッションの暗号化やストア切り替えがより簡単に行えます。まずは標準ライブラリで仕組みを理解し、要件に応じてライブラリの導入を検討するのがよいでしょう。