Protokol HTTP tidak punya memori: setiap permintaan diperlakukan sebagai tamu baru, padahal aplikasi perlu tahu bahwa peminta sudah login. Session management menambal kelemahan itu dengan menyimpan status login di server, lalu menyerahkan satu penanda — session ID — kepada browser lewat cookie. Setiap permintaan berikutnya membawa penanda tersebut, dan server mencocokkannya dengan data tersimpannya. Chapter ini membangun penyimpanan sesi in-memory lengkap dengan login, pemeriksaan sesi, logout, kedaluwarsa, dan regenerasi ID.
HTTP yang Stateless dan Kebutuhan Sesi #
Login yang berhasil tidak mengubah permintaan HTTP berikutnya apa pun. Solusinya: saat Budi login, server membuat entri sesi — siapa pemiliknya dan kapan berakhir — lalu mengirim ID unik entri itu sebagai cookie. Browser melampirkan cookie secara otomatis pada setiap permintaan ke domain yang sama, sehingga handler cukup membaca ID, mencari entri di server, dan mengetahui pemilik permintaan. Status tetap di server; klien hanya memegang kuncinya.
Session ID Harus Acak dan Panjang #
Session ID adalah kredensial: siapa pun yang memegang ID tersebut dapat mengaku sebagai pemiliknya, sehingga ID harus tidak mungkin ditebak. ID berbasis nomor urut, username, atau timestamp jatuh oleh tebakan sederhana. Standar praktiknya memakai crypto/rand sebanyak 32 byte — 256 bit entropi — yang disandikan menjadi 64 karakter heksadesimal, seperti pada program di bawah. Angka acak math/rand tidak memenuhi syarat karena generatornya dapat diprediksi.
Program: Sesi Login In-Memory #
Program session-demo.go membangun alur lengkap: handler login memverifikasi kredensial lalu membuat sesi, handler profil berperan sebagai middleware penolak permintaan tanpa sesi sah, dan handler logout menghapus sesi. Seluruh interaksi berjalan lewat httptest dengan dua klien: satu memakai cookie jar seperti browser, satu lagi polos tanpa cookie.
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"strings"
"sync"
"time"
)
// waktuSekarang dibekukan agar contoh deterministik; produksi memakai time.Now().
var waktuSekarang = time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)
type sesiPengguna struct {
nama string
berakhir time.Time
}
// penyimpananSesi menyimpan sesi di memori. Mutex wajib karena map
// tidak aman dibaca dan ditulis banyak goroutine secara bersamaan.
type penyimpananSesi struct {
mu sync.Mutex
sesi map[string]sesiPengguna
}
// buat membuat session ID acak 32 byte lalu menyimpan datanya.
func (p *penyimpananSesi) buat(nama string, umur time.Duration) (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
id := hex.EncodeToString(b)
p.mu.Lock()
p.sesi[id] = sesiPengguna{nama: nama, berakhir: waktuSekarang.Add(umur)}
p.mu.Unlock()
return id, nil
}
// ambil mengembalikan data sesi hanya bila ID ada dan belum kedaluwarsa.
func (p *penyimpananSesi) ambil(id string) (sesiPengguna, bool) {
p.mu.Lock()
defer p.mu.Unlock()
data, ok := p.sesi[id]
if !ok || !waktuSekarang.Before(data.berakhir) {
return sesiPengguna{}, false
}
return data, true
}
func (p *penyimpananSesi) hapus(id string) {
p.mu.Lock()
delete(p.sesi, id)
p.mu.Unlock()
}
type aplikasi struct {
store penyimpananSesi
}
// kredensial contoh; produksi memverifikasi hash bcrypt, bukan pembandingan langsung.
var kredensial = map[string]string{
"budi": "sandiRahasia123",
"siti": "sandiSiti456",
}
func (a *aplikasi) login(w http.ResponseWriter, r *http.Request) {
nama := r.PostFormValue("username")
password := r.PostFormValue("password")
if kredensial[nama] == "" || kredensial[nama] != password {
http.Error(w, "kredensial salah", http.StatusUnauthorized)
return
}
// Regenerasi: sesi lama dibuang supaya ID lama tidak dipakai ulang
// (pertahanan terhadap session fixation).
if c, err := r.Cookie("session_id"); err == nil {
a.store.hapus(c.Value)
}
id, err := a.store.buat(nama, 30*time.Minute)
if err != nil {
http.Error(w, "gagal membuat sesi", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: id,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/profil", http.StatusSeeOther)
}
func (a *aplikasi) profil(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "belum login", http.StatusUnauthorized)
return
}
data, ok := a.store.ambil(c.Value)
if !ok {
http.Error(w, "sesi tidak valid", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "pengguna: %s", data.nama)
}
func (a *aplikasi) logout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("session_id"); err == nil {
a.store.hapus(c.Value)
}
// MaxAge negatif memerintahkan browser menghapus cookie.
http.SetCookie(w, &http.Cookie{Name: "session_id", Value: "", Path: "/", MaxAge: -1})
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// kirim menjalankan satu permintaan lalu mengembalikan status dan isi respons.
func kirim(client *http.Client, method, target, form string) (int, string) {
var b io.Reader
if form != "" {
b = strings.NewReader(form)
}
req, err := http.NewRequest(method, target, b)
if err != nil {
log.Fatal(err)
}
if form != "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
isi, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(isi)
}
func cookieSesi(jar *cookiejar.Jar, serverURL string) string {
u, err := url.Parse(serverURL)
if err != nil {
log.Fatal(err)
}
for _, c := range jar.Cookies(u) {
if c.Name == "session_id" {
return c.Value
}
}
return ""
}
func main() {
a := &aplikasi{store: penyimpananSesi{sesi: map[string]sesiPengguna{}}}
mux := http.NewServeMux()
mux.HandleFunc("POST /login", a.login)
mux.HandleFunc("GET /profil", a.profil)
mux.HandleFunc("POST /logout", a.logout)
server := httptest.NewServer(mux)
defer server.Close()
jar, err := cookiejar.New(nil)
if err != nil {
log.Fatal(err)
}
// Jangan ikuti redirect agar kode 303 terlihat pada output.
teruskan := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// Klien dengan cookie jar seperti browser, dan klien polos tanpa cookie.
clientBudi := &http.Client{Jar: jar, CheckRedirect: teruskan}
clientPolos := &http.Client{CheckRedirect: teruskan}
kode, _ := kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=passwordSalah")
fmt.Printf("skenario 1: login dengan password salah -> %d\n", kode)
kode, _ = kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=sandiRahasia123")
fmt.Printf("skenario 2: login dengan kredensial benar -> %d\n", kode)
idLama := cookieSesi(jar, server.URL)
fmt.Printf(" cookie session_id terbit, panjang ID: %d karakter hex\n", len(idLama))
kode, isi := kirim(clientBudi, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 3: akses /profil dengan cookie sesi -> %d (%s)\n", kode, isi)
kode, _ = kirim(clientPolos, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 4: akses /profil tanpa cookie -> %d\n", kode)
reqPalsu, err := http.NewRequest("GET", server.URL+"/profil", nil)
if err != nil {
log.Fatal(err)
}
reqPalsu.AddCookie(&http.Cookie{Name: "session_id", Value: strings.Repeat("61", 32)})
respPalsu, err := clientPolos.Do(reqPalsu)
if err != nil {
log.Fatal(err)
}
respPalsu.Body.Close()
fmt.Printf("skenario 5: akses /profil dengan ID dipalsukan -> %d\n", respPalsu.StatusCode)
kode, _ = kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=sandiRahasia123")
fmt.Printf("skenario 6: login ulang, ID sesi diregenerasi -> %d\n", kode)
idBaru := cookieSesi(jar, server.URL)
fmt.Printf(" ID baru berbeda dari ID lama: %v\n", idBaru != idLama)
reqLama, err := http.NewRequest("GET", server.URL+"/profil", nil)
if err != nil {
log.Fatal(err)
}
reqLama.AddCookie(&http.Cookie{Name: "session_id", Value: idLama})
respLama, err := clientPolos.Do(reqLama)
if err != nil {
log.Fatal(err)
}
respLama.Body.Close()
fmt.Printf("skenario 7: akses /profil dengan ID lama -> %d\n", respLama.StatusCode)
kode, _ = kirim(clientBudi, "POST", server.URL+"/logout", "")
fmt.Printf("skenario 8: logout -> %d\n", kode)
kode, _ = kirim(clientBudi, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 9: akses /profil setelah logout -> %d\n", kode)
kirim(clientBudi, "POST", server.URL+"/login", "username=siti&password=sandiSiti456")
waktuSekarang = waktuSekarang.Add(31 * time.Minute)
kode, _ = kirim(clientBudi, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 10: sesi Siti kedaluwarsa (waktu maju) -> %d\n", kode)
}Output yang Dihasilkan:
skenario 1: login dengan password salah -> 401
skenario 2: login dengan kredensial benar -> 303
cookie session_id terbit, panjang ID: 64 karakter hex
skenario 3: akses /profil dengan cookie sesi -> 200 (pengguna: budi)
skenario 4: akses /profil tanpa cookie -> 401
skenario 5: akses /profil dengan ID dipalsukan -> 401
skenario 6: login ulang, ID sesi diregenerasi -> 303
ID baru berbeda dari ID lama: true
skenario 7: akses /profil dengan ID lama -> 401
skenario 8: logout -> 303
skenario 9: akses /profil setelah logout -> 401
skenario 10: sesi Siti kedaluwarsa (waktu maju) -> 401Mari kita telaah baris demi baris program di atas:
- Baris 18-19:
waktuSekarangdibekukan pada tanggal tetap agar output contoh dapat diprediksi; aplikasi produksi memakaitime.Now(). - Baris 28-31: struct
penyimpananSesimembungkusmapdengansync.Mutexkarena map tidak aman saat dibaca dan ditulis banyak goroutine bersamaan. - Baris 33-44: method
buatmengisi 32 byte daricrypto/rand, menyandikannya menjadi heksadesimal, lalu menyimpan sesi dengan waktu berakhirumurke depan. - Baris 46-55: method
ambilmengembalikan data hanya bila ID ditemukan danwaktuSekarangmasih sebelumberakhir— sesi kedaluwarsa diperlakukan seperti tidak ada. - Baris 73-98: handler
loginmenolak kredensial salah dengan 401, membuang sesi lama bila cookie lama ikut terkirim (baris 82-84, regenerasi anti session fixation), membuat ID baru, lalu mengirim ID itu sebagai cookieHttpOnlydenganSameSite=Lax. - Baris 100-112: handler
profilmembaca cookie; cookie yang hilang atau ID yang tidak ditemukan sama-sama dibalas 401, dan hanya sesi sah yang sampai kefmt.Fprintf. - Baris 230-233: setelah Siti login, jam dimajukan 31 menit sehingga melewati umur sesi 30 menit — skenario 10 membuktikan permintaan berikutnya ditolak karena kedaluwarsa.
Regenerasi ID dan Logout #
Serangan session fixation menempatkan ID yang sudah diketahui penyerang di browser korban — lewat tautan atau cookie dari subdomain — lalu menunggu korban login memakai ID tersebut. Bila server mempertahankan ID itu, penyerang langsung memiliki kredensial sah setelah login korban. Regenerasi pada baris 82-84 memutus rantainya: login selalu membuang ID lama dan menerbitkan ID segar. Logout bekerja sebaliknya: sesi dihapus dari penyimpanan (baris 115-117) dan cookie dibersihkan dengan MaxAge: -1 (baris 119).
Konkurensi dan Penyimpanan di Luar Memori #
Penyimpanan berbasis map memerlukan sync.Mutex karena net/http menjalankan tiap permintaan pada goroutine sendiri; dua permintaan bersamaan dapat menulis map yang sama, dan kondisi balapan seperti itu terdeteksi oleh race detector. Map in-memory punya dua batasan: seluruh sesi hilang saat proses restart, dan instance kedua tidak melihat sesi milik instance pertama. Aplikasi satu proses cukup memakai map; begitu berjalan di beberapa instance di belakang load balancer, pindahkan penyimpanan ke Redis atau Memcached agar semua instance berbagi sesi. Pola handler profil — baca cookie lalu cari sesi — juga dapat diangkat menjadi middleware agar seluruh endpoint terlindungi sekaligus, seperti dibahas pada chapter Middleware
.
Session ID aman selama cookie pembawanya tidak dibocorkan; pengaturan cookie yang benar itulah topik chapter berikutnya.
Referensi #
- OWASP Session Management Cheat Sheet — aturan pembuatan, rotasi, dan kedaluwarsa session ID.
- Package crypto/rand — generator acak kriptografis untuk session ID.
- Package net/http — dokumentasi Cookie, SetCookie, dan ServeMux.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
session-demo.go
Unduh berkaspackage main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"strings"
"sync"
"time"
)
// waktuSekarang dibekukan agar contoh deterministik; produksi memakai time.Now().
var waktuSekarang = time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)
type sesiPengguna struct {
nama string
berakhir time.Time
}
// penyimpananSesi menyimpan sesi di memori. Mutex wajib karena map
// tidak aman dibaca dan ditulis banyak goroutine secara bersamaan.
type penyimpananSesi struct {
mu sync.Mutex
sesi map[string]sesiPengguna
}
// buat membuat session ID acak 32 byte lalu menyimpan datanya.
func (p *penyimpananSesi) buat(nama string, umur time.Duration) (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
id := hex.EncodeToString(b)
p.mu.Lock()
p.sesi[id] = sesiPengguna{nama: nama, berakhir: waktuSekarang.Add(umur)}
p.mu.Unlock()
return id, nil
}
// ambil mengembalikan data sesi hanya bila ID ada dan belum kedaluwarsa.
func (p *penyimpananSesi) ambil(id string) (sesiPengguna, bool) {
p.mu.Lock()
defer p.mu.Unlock()
data, ok := p.sesi[id]
if !ok || !waktuSekarang.Before(data.berakhir) {
return sesiPengguna{}, false
}
return data, true
}
func (p *penyimpananSesi) hapus(id string) {
p.mu.Lock()
delete(p.sesi, id)
p.mu.Unlock()
}
type aplikasi struct {
store penyimpananSesi
}
// kredensial contoh; produksi memverifikasi hash bcrypt, bukan pembandingan langsung.
var kredensial = map[string]string{
"budi": "sandiRahasia123",
"siti": "sandiSiti456",
}
func (a *aplikasi) login(w http.ResponseWriter, r *http.Request) {
nama := r.PostFormValue("username")
password := r.PostFormValue("password")
if kredensial[nama] == "" || kredensial[nama] != password {
http.Error(w, "kredensial salah", http.StatusUnauthorized)
return
}
// Regenerasi: sesi lama dibuang supaya ID lama tidak dipakai ulang
// (pertahanan terhadap session fixation).
if c, err := r.Cookie("session_id"); err == nil {
a.store.hapus(c.Value)
}
id, err := a.store.buat(nama, 30*time.Minute)
if err != nil {
http.Error(w, "gagal membuat sesi", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: id,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/profil", http.StatusSeeOther)
}
func (a *aplikasi) profil(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "belum login", http.StatusUnauthorized)
return
}
data, ok := a.store.ambil(c.Value)
if !ok {
http.Error(w, "sesi tidak valid", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "pengguna: %s", data.nama)
}
func (a *aplikasi) logout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("session_id"); err == nil {
a.store.hapus(c.Value)
}
// MaxAge negatif memerintahkan browser menghapus cookie.
http.SetCookie(w, &http.Cookie{Name: "session_id", Value: "", Path: "/", MaxAge: -1})
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// kirim menjalankan satu permintaan lalu mengembalikan status dan isi respons.
func kirim(client *http.Client, method, target, form string) (int, string) {
var b io.Reader
if form != "" {
b = strings.NewReader(form)
}
req, err := http.NewRequest(method, target, b)
if err != nil {
log.Fatal(err)
}
if form != "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
isi, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(isi)
}
func cookieSesi(jar *cookiejar.Jar, serverURL string) string {
u, err := url.Parse(serverURL)
if err != nil {
log.Fatal(err)
}
for _, c := range jar.Cookies(u) {
if c.Name == "session_id" {
return c.Value
}
}
return ""
}
func main() {
a := &aplikasi{store: penyimpananSesi{sesi: map[string]sesiPengguna{}}}
mux := http.NewServeMux()
mux.HandleFunc("POST /login", a.login)
mux.HandleFunc("GET /profil", a.profil)
mux.HandleFunc("POST /logout", a.logout)
server := httptest.NewServer(mux)
defer server.Close()
jar, err := cookiejar.New(nil)
if err != nil {
log.Fatal(err)
}
// Jangan ikuti redirect agar kode 303 terlihat pada output.
teruskan := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
// Klien dengan cookie jar seperti browser, dan klien polos tanpa cookie.
clientBudi := &http.Client{Jar: jar, CheckRedirect: teruskan}
clientPolos := &http.Client{CheckRedirect: teruskan}
kode, _ := kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=passwordSalah")
fmt.Printf("skenario 1: login dengan password salah -> %d\n", kode)
kode, _ = kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=sandiRahasia123")
fmt.Printf("skenario 2: login dengan kredensial benar -> %d\n", kode)
idLama := cookieSesi(jar, server.URL)
fmt.Printf(" cookie session_id terbit, panjang ID: %d karakter hex\n", len(idLama))
kode, isi := kirim(clientBudi, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 3: akses /profil dengan cookie sesi -> %d (%s)\n", kode, isi)
kode, _ = kirim(clientPolos, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 4: akses /profil tanpa cookie -> %d\n", kode)
reqPalsu, err := http.NewRequest("GET", server.URL+"/profil", nil)
if err != nil {
log.Fatal(err)
}
reqPalsu.AddCookie(&http.Cookie{Name: "session_id", Value: strings.Repeat("61", 32)})
respPalsu, err := clientPolos.Do(reqPalsu)
if err != nil {
log.Fatal(err)
}
respPalsu.Body.Close()
fmt.Printf("skenario 5: akses /profil dengan ID dipalsukan -> %d\n", respPalsu.StatusCode)
kode, _ = kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=sandiRahasia123")
fmt.Printf("skenario 6: login ulang, ID sesi diregenerasi -> %d\n", kode)
idBaru := cookieSesi(jar, server.URL)
fmt.Printf(" ID baru berbeda dari ID lama: %v\n", idBaru != idLama)
reqLama, err := http.NewRequest("GET", server.URL+"/profil", nil)
if err != nil {
log.Fatal(err)
}
reqLama.AddCookie(&http.Cookie{Name: "session_id", Value: idLama})
respLama, err := clientPolos.Do(reqLama)
if err != nil {
log.Fatal(err)
}
respLama.Body.Close()
fmt.Printf("skenario 7: akses /profil dengan ID lama -> %d\n", respLama.StatusCode)
kode, _ = kirim(clientBudi, "POST", server.URL+"/logout", "")
fmt.Printf("skenario 8: logout -> %d\n", kode)
kode, _ = kirim(clientBudi, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 9: akses /profil setelah logout -> %d\n", kode)
kirim(clientBudi, "POST", server.URL+"/login", "username=siti&password=sandiSiti456")
waktuSekarang = waktuSekarang.Add(31 * time.Minute)
kode, _ = kirim(clientBudi, "GET", server.URL+"/profil", "")
fmt.Printf("skenario 10: sesi Siti kedaluwarsa (waktu maju) -> %d\n", kode)
}