Cookie adalah kurir kredensial: session ID dari chapter sebelumnya berpindah di dalamnya pada setiap permintaan. Kurir tanpa pengawalan justru membocorkan muatan — cookie tanpa HttpOnly dapat dibaca JavaScript jahat, tanpa Secure ikut terkirim lewat HTTP polos, tanpa SameSite dikirim oleh situs lain tanpa sepengetahuan pengguna. Struct http.Cookie menyediakan seluruh atribut pengaman itu; chapter ini membedah atributnya lalu membuktikan bentuknya lewat respons httptest.

Fungsi http.SetCookie(w, cookie) menerima http.ResponseWriter dan pointer http.Cookie, lalu menulis satu header Set-Cookie yang terformat benar; pemanggilan berikutnya menambah baris baru, jadi satu respons boleh membawa banyak cookie. Pembacaan kembali memakai r.Cookie("nama") untuk satu cookie atau resp.Cookies() untuk semua cookie dalam respons.

Atribut Keamanan pada http.Cookie #

Setiap field pada http.Cookie mengendalikan satu perilaku browser. Tabel berikut merangkum atribut yang menentukan keamanan:

AtributEfek pada browserNilai aman untuk cookie sesi
HttpOnlyJavaScript tidak dapat membaca cookie lewat document.cookietrue
SecureCookie hanya dikirim melalui HTTPStrue
SameSiteMembatasi pengiriman cookie lintas situsLax atau Strict
DomainMenentukan host mana yang menerima cookiekosong (host saat ini)
PathMembatasi path URL yang menerima cookie/ atau sempit
MaxAgeUmur cookie dalam detiksesuai kebutuhan
ExpiresBatas waktu absolut cookiesesuai kebutuhan

HttpOnly menutup jalur pencurian lewat XSS: skrip yang tersisipkan ke halaman tetap tidak bisa membaca session ID, sehingga serangan tinggal memakai cookie dari dalam browser — penanganannya di chapter CSRF. Secure menjamin cookie tidak pernah melintas di jaringan terbuka yang bisa dibaca siapa pun. Domain dan Path mempersempit jangkauan: cookie dengan Path: "/admin" tidak dikirim ke endpoint lain, dan Domain yang terlalu luas membuat cookie terkirim ke semua subdomain.

SameSite Lax, Strict, dan None #

SameSite mengatur apakah browser tetap melampirkan cookie ketika permintaan berasal dari situs lain. Strict tidak pernah mengirimnya — tautan dari situs lain pun tiba tanpa login. Lax berkompromi: navigasi GET tingkat atas tetap membawa cookie, sedangkan POST lintas situs tidak. None mengirim cookie di semua konteks dan hanya sah untuk kasus seperti pembayaran terarah ke domain lain.

Cookie tanpa MaxAge dan tanpa Expires adalah cookie sesi: tersimpan hanya di memori dan musnah saat jendela ditutup. Cookie dengan salah satu batas waktu menjadi cookie persisten di disk. MaxAge menyatakan umur relatif dalam detik, Expires menyatakan waktu absolut, dan bila keduanya diisi MaxAge yang menang. MaxAge negatif atau Expires di masa lalu memerintahkan browser menghapus cookie — pola yang dipakai handler logout pada chapter sebelumnya.

Prefix __Host- #

Nama cookie dapat menjadi kontrak keamanan. Prefix __Host- — misalnya __Host-tema — memaksa browser menolak cookie kecuali disertai Secure, Path=/, dan tanpa Domain; cookie itu dijamin berasal dari host yang tepat dan tidak dapat ditimpa subdomain jahat. Aturannya bekerja murni lewat penamaan.

Program cookie-demo.go mengatur empat cookie berbeda, mencetak header Set-Cookie mentah beserta parsing atributnya, lalu menyimulasikan kunjungan kedua tempat seluruh cookie dikirim balik ke server.

go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"
)

// aturCookie menulis empat cookie dengan konfigurasi berbeda ke respons.
func aturCookie(w http.ResponseWriter, r *http.Request) {
	// Cookie sesi: tanpa MaxAge/Expires, hilang saat browser ditutup.
	http.SetCookie(w, &http.Cookie{
		Name:     "sesi",
		Value:    "sesi-budi-aktif",
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
	// Cookie persisten: bertahan 3600 detik di penyimpanan browser.
	http.SetCookie(w, &http.Cookie{
		Name:   "preferensi",
		Value:  "tema-gelap",
		Path:   "/",
		MaxAge: 3600,
	})
	// Prefix __Host-: wajib Secure, Path=/, tanpa Domain.
	http.SetCookie(w, &http.Cookie{
		Name:   "__Host-tema",
		Value:  "biru",
		Path:   "/",
		Secure: true,
	})
	// Cookie dengan batas waktu absolut lewat Expires.
	http.SetCookie(w, &http.Cookie{
		Name:    "kupon",
		Value:   "DISKON20",
		Path:    "/",
		Expires: time.Date(2026, 1, 16, 8, 0, 0, 0, time.UTC),
	})
}

func bacaCookie(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("sesi")
	if err != nil {
		http.Error(w, "cookie sesi tidak ditemukan", http.StatusBadRequest)
		return
	}
	fmt.Fprintf(w, "nilai cookie sesi: %s", c.Value)
}

func namaSameSite(s http.SameSite) string {
	switch s {
	case http.SameSiteLaxMode:
		return "Lax"
	case http.SameSiteStrictMode:
		return "Strict"
	case http.SameSiteNoneMode:
		return "None"
	default:
		return "default"
	}
}

func deskripsi(c *http.Cookie) string {
	maxAge := fmt.Sprintf("%d", c.MaxAge)
	if c.MaxAge == 0 {
		maxAge = "0 (cookie sesi)"
	}
	expires := c.Expires.Format("2006-01-02 15:04 MST")
	if c.Expires.IsZero() {
		expires = "tidak diset"
	}
	return fmt.Sprintf("HttpOnly=%-5v Secure=%-5v SameSite=%-7s Path=%s MaxAge=%s Expires=%s",
		c.HttpOnly, c.Secure, namaSameSite(c.SameSite), c.Path, maxAge, expires)
}

func main() {
	// Permintaan pertama: server mengatur empat cookie sekaligus.
	req := httptest.NewRequest("GET", "/masuk", nil)
	rec := httptest.NewRecorder()
	aturCookie(rec, req)
	resp := rec.Result()

	fmt.Println("header Set-Cookie mentah dari respons:")
	for _, v := range resp.Header["Set-Cookie"] {
		fmt.Println("  " + v)
	}

	// Parsing kembali atribut setiap cookie dari respons yang sama.
	fmt.Println()
	fmt.Println("atribut hasil parsing setiap cookie:")
	cookies := resp.Cookies()
	for _, c := range cookies {
		fmt.Printf("  %-12s %s\n", c.Name, deskripsi(c))
	}

	// Kunjungan kedua: browser mengirim semua cookie kembali ke server.
	fmt.Println()
	fmt.Println("kunjungan kedua, cookie yang terkirim balik:")
	reqKedua := httptest.NewRequest("GET", "/baca", nil)
	for _, c := range cookies {
		fmt.Printf("  %-12s = %s\n", c.Name, c.Value)
		reqKedua.AddCookie(c)
	}

	recKedua := httptest.NewRecorder()
	bacaCookie(recKedua, reqKedua)
	fmt.Println()
	fmt.Println("handler /baca membalas :", recKedua.Body.String())
}

Output yang Dihasilkan:

text
header Set-Cookie mentah dari respons:
  sesi=sesi-budi-aktif; Path=/; HttpOnly; SameSite=Lax
  preferensi=tema-gelap; Path=/; Max-Age=3600
  __Host-tema=biru; Path=/; Secure
  kupon=DISKON20; Path=/; Expires=Fri, 16 Jan 2026 08:00:00 GMT

atribut hasil parsing setiap cookie:
  sesi         HttpOnly=true  Secure=false SameSite=Lax     Path=/ MaxAge=0 (cookie sesi) Expires=tidak diset
  preferensi   HttpOnly=false Secure=false SameSite=default Path=/ MaxAge=3600 Expires=tidak diset
  __Host-tema  HttpOnly=false Secure=true  SameSite=default Path=/ MaxAge=0 (cookie sesi) Expires=tidak diset
  kupon        HttpOnly=false Secure=false SameSite=default Path=/ MaxAge=0 (cookie sesi) Expires=2026-01-16 08:00 UTC

kunjungan kedua, cookie yang terkirim balik:
  sesi         = sesi-budi-aktif
  preferensi   = tema-gelap
  __Host-tema  = biru
  kupon        = DISKON20

handler /baca membalas : nilai cookie sesi: sesi-budi-aktif

Mari kita telaah baris demi baris program di atas:

  1. Baris 12-19: cookie sesi tanpa MaxAge/Expires menjadi cookie sesi; HttpOnly dan SameSite=Lax melindunginya dari JavaScript dan POST lintas situs.
  2. Baris 20-26: cookie preferensi memakai MaxAge: 3600 sehingga bertahan satu jam di disk.
  3. Baris 27-33: cookie __Host-tema memenuhi kontrak prefix — Secure dan Path: "/" tanpa Domain.
  4. Baris 34-40: cookie kupon memakai Expires absolut dengan time.Date beku agar output contoh deterministik.
  5. Baris 80-88: httptest.NewRecorder menampung respons tanpa jaringan; resp.Header["Set-Cookie"] memperlihatkan format mentah tulisan http.SetCookie.
  6. Baris 93-96: resp.Cookies() mem-parsing header kembali menjadi struct, lalu deskripsi mencetak atributnya — perhatikan cookie tanpa SameSite terbaca sebagai default.
  7. Baris 101-105: permintaan kedua memanggil AddCookie untuk setiap cookie, meniru browser yang melampirkan seluruh cookie domain.
  8. Baris 107-110: handler bacaCookie membaca cookie sesi dari permintaan kedua dan membuktikan nilainya utuh.

Referensi #

Cookie yang terkonfigurasi baik menjaga kredensial selama perjalanan bolak-balik, tetapi pendekatan ini tetap menuntut server menyimpan status setiap pengguna. Bila Anda ingin server memverifikasi identitas tanpa menyimpan apa pun, kita masuk ke token bertanda tangan pada chapter Token-Based Authentication berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

cookie-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"
)

// aturCookie menulis empat cookie dengan konfigurasi berbeda ke respons.
func aturCookie(w http.ResponseWriter, r *http.Request) {
	// Cookie sesi: tanpa MaxAge/Expires, hilang saat browser ditutup.
	http.SetCookie(w, &http.Cookie{
		Name:     "sesi",
		Value:    "sesi-budi-aktif",
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
	// Cookie persisten: bertahan 3600 detik di penyimpanan browser.
	http.SetCookie(w, &http.Cookie{
		Name:   "preferensi",
		Value:  "tema-gelap",
		Path:   "/",
		MaxAge: 3600,
	})
	// Prefix __Host-: wajib Secure, Path=/, tanpa Domain.
	http.SetCookie(w, &http.Cookie{
		Name:   "__Host-tema",
		Value:  "biru",
		Path:   "/",
		Secure: true,
	})
	// Cookie dengan batas waktu absolut lewat Expires.
	http.SetCookie(w, &http.Cookie{
		Name:    "kupon",
		Value:   "DISKON20",
		Path:    "/",
		Expires: time.Date(2026, 1, 16, 8, 0, 0, 0, time.UTC),
	})
}

func bacaCookie(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("sesi")
	if err != nil {
		http.Error(w, "cookie sesi tidak ditemukan", http.StatusBadRequest)
		return
	}
	fmt.Fprintf(w, "nilai cookie sesi: %s", c.Value)
}

func namaSameSite(s http.SameSite) string {
	switch s {
	case http.SameSiteLaxMode:
		return "Lax"
	case http.SameSiteStrictMode:
		return "Strict"
	case http.SameSiteNoneMode:
		return "None"
	default:
		return "default"
	}
}

func deskripsi(c *http.Cookie) string {
	maxAge := fmt.Sprintf("%d", c.MaxAge)
	if c.MaxAge == 0 {
		maxAge = "0 (cookie sesi)"
	}
	expires := c.Expires.Format("2006-01-02 15:04 MST")
	if c.Expires.IsZero() {
		expires = "tidak diset"
	}
	return fmt.Sprintf("HttpOnly=%-5v Secure=%-5v SameSite=%-7s Path=%s MaxAge=%s Expires=%s",
		c.HttpOnly, c.Secure, namaSameSite(c.SameSite), c.Path, maxAge, expires)
}

func main() {
	// Permintaan pertama: server mengatur empat cookie sekaligus.
	req := httptest.NewRequest("GET", "/masuk", nil)
	rec := httptest.NewRecorder()
	aturCookie(rec, req)
	resp := rec.Result()

	fmt.Println("header Set-Cookie mentah dari respons:")
	for _, v := range resp.Header["Set-Cookie"] {
		fmt.Println("  " + v)
	}

	// Parsing kembali atribut setiap cookie dari respons yang sama.
	fmt.Println()
	fmt.Println("atribut hasil parsing setiap cookie:")
	cookies := resp.Cookies()
	for _, c := range cookies {
		fmt.Printf("  %-12s %s\n", c.Name, deskripsi(c))
	}

	// Kunjungan kedua: browser mengirim semua cookie kembali ke server.
	fmt.Println()
	fmt.Println("kunjungan kedua, cookie yang terkirim balik:")
	reqKedua := httptest.NewRequest("GET", "/baca", nil)
	for _, c := range cookies {
		fmt.Printf("  %-12s = %s\n", c.Name, c.Value)
		reqKedua.AddCookie(c)
	}

	recKedua := httptest.NewRecorder()
	bacaCookie(recKedua, reqKedua)
	fmt.Println()
	fmt.Println("handler /baca membalas :", recKedua.Body.String())
}