Sesi & cookie di Astro
Perbedaan terbesar dari CI3 bukan API-nya, melainkan kapan sesi dibaca. Di CI3 selalu; di Astro hanya di tempat yang memang butuh — dan itulah yang membebaskan cache-mu.
Intisari
- Astro punya API sesi bawaan dengan driver penyimpanan yang bisa dipilih; untuk banyak kasus cookie bertanda tangan sudah cukup.
- Sesi hanya dibaca di server island dan rute personal — tidak pernah di halaman yang di-cache.
- Cookie sesi wajib
httpOnly,secure,sameSite: "lax", dan berawalan__Host-. - ID sesi wajib dari
crypto.randomUUID()ataurandomBytes— tidak pernahMath.random(). - Sesi di memori proses tidak bekerja dengan lebih dari satu task ECS: pembaca akan ter-logout acak.
Dua pendekatan
| Cookie bertanda tangan | Sesi tersimpan (DB/Redis) | |
|---|---|---|
| Isi | Data ada di dalam cookie | Hanya ID; data di server |
| Ukuran | Dibatasi ~4 KB, ikut tiap request | ~40 byte |
| Bisa dicabut seketika | Tidak | Ya |
| Butuh kueri per pembacaan | Tidak | Ya |
| Cocok untuk | Preferensi, pembatas gratis | Sesi login & langganan |
Untuk portal berlangganan, "bisa dicabut seketika" menentukan. Kalau pembaca melaporkan akunnya dipakai orang lain, kamu harus bisa mematikan sesinya sekarang juga — bukan menunggu cookie-nya kedaluwarsa sendiri. Jadi: sesi tersimpan, dengan tabel di MySQL.
Tabel sesi
CREATE TABLE sesi (
id CHAR(43) NOT NULL PRIMARY KEY, -- base64url dari 32 byte
member_id INT NOT NULL,
ip VARBINARY(16) NULL,
peramban VARCHAR(255) NULL,
dibuat_pada DATETIME NOT NULL,
dipakai_pada DATETIME NOT NULL,
kedaluwarsa DATETIME NOT NULL,
INDEX idx_sesi_member (member_id),
INDEX idx_sesi_exp (kedaluwarsa)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
idx_sesi_member yang membuat "keluarkan saya dari semua perangkat" jadi satu kueri.
idx_sesi_exp yang membuat pembersihan berkala tidak memindai seluruh tabel.
Membuat dan membaca sesi
// src/lib/auth.ts
import { randomBytes } from "node:crypto";
import type { AstroCookies } from "astro";
import { dbTulis, dbBaca } from "./db";
const NAMA_COOKIE = "__Host-sesi";
const UMUR_DETIK = 60 * 60 * 24 * 30; // 30 hari
export async function buatSesi(
cookies: AstroCookies,
memberId: number,
ip: string,
ua: string,
) {
// 32 byte acak kriptografis → 43 karakter base64url
const id = randomBytes(32).toString("base64url");
const now = new Date();
await dbTulis
.insertInto("sesi")
.values({
id,
member_id: memberId,
ip: Buffer.from(ip),
peramban: ua.slice(0, 255),
dibuat_pada: now,
dipakai_pada: now,
kedaluwarsa: new Date(Date.now() + UMUR_DETIK * 1000),
})
.execute();
cookies.set(NAMA_COOKIE, id, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: UMUR_DETIK,
});
}
Awalan __Host- bukan hiasan. Ia memberi jaminan yang ditegakkan browser: cookie
ini hanya bisa diset lewat HTTPS, harus path=/, dan tidak boleh punya atribut
Domain — sehingga subdomain tidak bisa menimpanya. Kalau portalmu punya subdomain
yang tidak sepenuhnya kamu kendalikan — blog., promo., atau subdomain lama
dari vendor — tanpa awalan ini subdomain itu bisa menyetel cookie sesi untuk domain utamamu. Itu jalur
session fixation yang nyata.
export async function memberDariCookie(cookies: AstroCookies) {
const id = cookies.get(NAMA_COOKIE)?.value;
if (!id || id.length !== 43) return null;
const baris = await dbBaca
.selectFrom("sesi as s")
.innerJoin("member as m", "m.id", "s.member_id")
.select([
"m.id", "m.nama", "m.email", "m.peran",
"s.id as sesiId", "s.kedaluwarsa",
])
.where("s.id", "=", id)
.where("s.kedaluwarsa", ">", new Date())
.where("m.status", "=", "aktif")
.executeTakeFirst();
return baris ?? null;
}
Pemeriksaan m.status = 'aktif' penting: akun yang dinonaktifkan harus kehilangan akses
seketika, meski sesinya belum kedaluwarsa. Tanpa syarat itu, "nonaktifkan akun" hanya berlaku untuk login
berikutnya.
Di mana sesi boleh dibaca
| Tempat | Boleh? | Alasan |
|---|---|---|
| Halaman artikel (di-cache) | Tidak pernah | Membuatnya personal; menghancurkan cache edge |
| Server island | Ya | Ini tempatnya |
/akun/* | Ya | Seluruh halamannya personal |
| Action & endpoint mutasi | Ya | Butuh tahu siapa yang bertindak |
| Middleware untuk semua rute | Tidak | Lihat Fase 1 — kueri sia-sia dan risiko kebocoran |
Kenapa sesi di memori tidak bekerja
Pembaca login → ALB → task #3 → sesi disimpan di memori task #3
Pembaca muat halaman → ALB → task #7 → tidak kenal sesi itu → ter-logout
Deploy → semua task diganti → SEMUA pembaca ter-logout
Gejalanya khas dan membingungkan: "kadang saya ter-logout sendiri". Ia tidak akan muncul saat pengembangan dengan satu proses. Simpan sesi di MySQL atau Redis — jangan pernah di memori proses.
Memperbarui dipakai_pada tanpa menulis tiap request
// Menulis di setiap pembacaan = satu UPDATE per request.
// Di trafikmu itu ratusan UPDATE per detik untuk informasi yang
// tidak ada yang benar-benar butuh presisi detik.
if (Date.now() - baris.dipakaiPada.getTime() > 60 * 60 * 1000) {
void dbTulis
.updateTable("sesi")
.set({ dipakai_pada: new Date() })
.where("id", "=", id)
.execute(); // sengaja tidak di-await
}
Perbarui paling sering sekali per jam, dan jangan await — pembaca tidak perlu menunggu tulisan
statistik. void di depannya adalah sinyal eksplisit bahwa promise-nya sengaja tidak ditunggu,
supaya linter tidak mengeluhkannya dan pembaca kode tahu itu disengaja.
Atribut cookie, lengkap
| Atribut | Nilai | Kalau salah |
|---|---|---|
httpOnly | true | XSS bisa mencuri sesi lewat document.cookie |
secure | true | Cookie terkirim lewat HTTP polos |
sameSite | "lax" | "none" membuka CSRF; "strict" memutus alur kembali dari Midtrans |
path | "/" | Diwajibkan oleh __Host- |
maxAge | Eksplisit | Tanpa itu ia jadi cookie sesi peramban dan hilang saat tab ditutup |
| Awalan nama | __Host- | Subdomain bisa menimpanya |
sameSite: "strict" terdengar paling aman dan justru merusak alur pembayaranmu.
Saat Midtrans mengalihkan pembeli kembali ke portalmu, itu navigasi lintas situs — dan dengan
strict, cookie sesi tidak ikut terkirim. Pembeli mendarat di halaman "sukses" dalam keadaan
ter-logout, dan mengira uangnya hilang. lax adalah pilihan yang benar.
Membersihkan sesi kedaluwarsa
-- Jalankan sebagai tugas terjadwal, BUKAN di dalam proses web.
DELETE FROM sesi WHERE kedaluwarsa < NOW() LIMIT 5000;
LIMIT-nya wajib. DELETE tanpa batas di tabel sesi yang sudah menumpuk bertahun-tahun
akan mengunci ribuan baris sekaligus dan memicu lonjakan lag replica. Jalankan berulang sampai
ROW_COUNT() nol.
Latihan: buat tabel sesi, fungsi buatSesi dan memberDariCookie, lalu
halaman login sederhana. Verifikasi empat hal di DevTools → Application → Cookies: nama berawalan
__Host-, HttpOnly tercentang, Secure tercentang, dan
SameSite bernilai Lax. Lalu coba baca document.cookie dari
konsol — sesi tidak boleh muncul di sana.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.