โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 6 ยท Keamanan

Sesi & cookie

Sesi berbasis cookie sederhana, bisa dicabut seketika, dan sudah cukup untuk sebagian besar aplikasi web. Yang membuatnya rawan bukan konsepnya, melainkan empat atribut yang sering salah disetel.

Sumber asli pkg.go.dev Resmi Rangkuman ~7 menit baca

Intisari

  • HttpOnly, Secure, SameSite, dan Path โ€” keempatnya wajib, dan defaultnya tidak aman.
  • ID sesi dibuat dari crypto/rand, minimal 32 byte. Jangan pernah math/rand.
  • Sesi server bisa dicabut seketika; itu keunggulan utamanya atas JWT.
  • Putar ID sesi setelah login untuk mencegah session fixation.
  • Simpan sesi di Redis begitu ada container kedua โ€” sesi di memori berarti pengguna ter-logout acak.

Cookie yang benar

http.SetCookie(w, &http.Cookie{
	Name:     "sesi",
	Value:    idSesi,
	Path:     "/",
	MaxAge:   int((24 * time.Hour).Seconds()),

	HttpOnly: true,                    // JavaScript tidak bisa membacanya
	Secure:   true,                    // hanya lewat HTTPS
	SameSite: http.SameSiteLaxMode,    // pertahanan CSRF lapis pertama
})
AtributKalau salah
HttpOnly: falseSatu XSS = seluruh sesi pengguna dicuri lewat document.cookie
Secure: falseCookie terkirim lewat HTTP polos dan bisa disadap
SameSite: NoneSitus lain bisa memicu permintaan terautentikasi (CSRF)
Path terlalu luas / domain indukCookie ikut ke subdomain yang mungkin dikelola orang lain
Tanpa MaxAgeCookie sesi browser โ€” hilang saat tutup, tapi tidak pernah kedaluwarsa di server
SameSitePerilakuUntuk
StrictTidak pernah ikut dari situs lain โ€” termasuk saat pengguna mengklik tautan ke situsmuPerbankan, panel admin
LaxIkut pada navigasi GET teratas, tidak pada POSTDefault yang tepat
NoneSelalu ikut; wajib disertai SecureHanya kalau memang lintas situs

Membuat ID sesi

import "crypto/rand"

func idSesiBaru() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {   // crypto/rand: gagal = jangan lanjut
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}

math/rand bisa ditebak. Ia deterministik dari benihnya; dengan beberapa keluaran, penyerang bisa menghitung ID sesi berikutnya. Ini bukan kelemahan teoretis โ€” ia sudah dipakai untuk membajak akun di sistem nyata. Untuk apa pun yang bersifat rahasia โ€” ID sesi, token, kode verifikasi, nama berkas unggahan โ€” selalu crypto/rand.

Penyimpanan sesi

type Sesi struct {
	ID          string
	PenggunaID  int64
	Kedaluwarsa time.Time
	IP          string
	UserAgent   string
}

type PenyimpanSesi interface {
	Buat(ctx context.Context, s Sesi) error
	Ambil(ctx context.Context, id string) (Sesi, error)
	Hapus(ctx context.Context, id string) error
	HapusSemuaMilik(ctx context.Context, penggunaID int64) error   // "keluar dari semua perangkat"
}
PenyimpananCocok kalauCatatan
MemoriSatu container, pengembanganPengguna ter-logout acak begitu ada task kedua
PostgresSesi sedikit, sudah ada databaseSatu kueri per permintaan โ€” perlu index
RedisPilihan default di AWSTTL otomatis, cepat, bersama antar task
Cookie terenkripsiTanpa penyimpanan sama sekaliTidak bisa dicabut โ€” dan ukuran cookie terbatas 4 KB

Sesi di memori adalah bug yang menunggu autoscaling. Dengan dua task di belakang ALB, permintaan pengguna mendarat di task acak โ€” dan hanya satu dari keduanya yang mengenali sesinya. Gejalanya: pengguna ter-logout "kadang-kadang", dan tidak bisa direproduksi di laptop. Begitu ada container kedua, sesi harus bersama.

Middleware autentikasi

func (a *Auth) Wajib(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := r.Cookie("sesi")
		if err != nil {
			a.tolak(w, r)
			return
		}

		s, err := a.simpan.Ambil(r.Context(), c.Value)
		if err != nil || time.Now().After(s.Kedaluwarsa) {
			a.hapusCookie(w)
			a.tolak(w, r)
			return          // โ† tanpa return, handler tetap jalan (Fase 3)
		}

		// Perpanjang sesi yang aktif โ€” tapi jangan menulis ke Redis
		// di setiap permintaan; cukup kalau sisanya di bawah separuh.
		if time.Until(s.Kedaluwarsa) < 12*time.Hour {
			s.Kedaluwarsa = time.Now().Add(24 * time.Hour)
			_ = a.simpan.Buat(r.Context(), s)
			a.setCookie(w, s)
		}

		ctx := DenganPengguna(r.Context(), s.PenggunaID)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

Session fixation

func (h *Handler) Masuk(w http.ResponseWriter, r *http.Request) {
	u, err := h.auth.Masuk(r.Context(), req.Email, req.Sandi)
	if err != nil {
		h.tulisError(w, r, err)
		return
	}

	// WAJIB: buang sesi lama, buat ID baru. Tanpa ini, penyerang yang
	// berhasil menanamkan ID sesi pilihannya sebelum korban login akan
	// memegang sesi yang SAMA setelah korban login.
	if c, err := r.Cookie("sesi"); err == nil {
		_ = h.simpan.Hapus(r.Context(), c.Value)
	}

	s, err := h.auth.BuatSesi(r.Context(), u.ID, r)
	if err != nil {
		h.tulisError(w, r, err)
		return
	}
	h.setCookie(w, s)
	...
}

Keluar yang benar

func (h *Handler) Keluar(w http.ResponseWriter, r *http.Request) {
	if c, err := r.Cookie("sesi"); err == nil {
		_ = h.simpan.Hapus(r.Context(), c.Value)   // hapus di SERVER
	}

	// Dan hapus di klien: nilai kosong + MaxAge negatif.
	http.SetCookie(w, &http.Cookie{
		Name: "sesi", Value: "", Path: "/",
		MaxAge: -1, HttpOnly: true, Secure: true,
		SameSite: http.SameSiteLaxMode,
	})
	w.WriteHeader(http.StatusNoContent)
}

Menghapus cookie saja bukan logout. Kalau sesinya masih hidup di server, siapa pun yang sempat menyalin nilai cookie itu tetap bisa memakainya. Logout harus menghapus di kedua sisi โ€” dan itu persis kemampuan yang tidak dimiliki JWT tanpa penyimpanan tambahan (materi berikutnya).

Latihan: bangun login berbasis sesi dengan penyimpanan Redis dan keempat atribut cookie di atas. Lalu jalankan dua instans aplikasimu di port berbeda dan buktikan sesinya berlaku di keduanya. Terakhir, hapus baris pemutaran ID sesi saat login dan tunjukkan pada dirimu sendiri bahwa ID sebelum dan sesudah login jadi sama.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.