← Semua pembelajaran / Astro Nol → Portal Berita
Fase 5 · Membership, Auth & Paywall

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.

Sumber asli docs.astro.build Resmi Rangkuman ~9 menit baca

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() atau randomBytes — tidak pernah Math.random().
  • Sesi di memori proses tidak bekerja dengan lebih dari satu task ECS: pembaca akan ter-logout acak.

Dua pendekatan

Cookie bertanda tanganSesi tersimpan (DB/Redis)
IsiData ada di dalam cookieHanya ID; data di server
UkuranDibatasi ~4 KB, ikut tiap request~40 byte
Bisa dicabut seketikaTidakYa
Butuh kueri per pembacaanTidakYa
Cocok untukPreferensi, pembatas gratisSesi 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

TempatBoleh?Alasan
Halaman artikel (di-cache)Tidak pernahMembuatnya personal; menghancurkan cache edge
Server islandYaIni tempatnya
/akun/*YaSeluruh halamannya personal
Action & endpoint mutasiYaButuh tahu siapa yang bertindak
Middleware untuk semua ruteTidakLihat 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

AtributNilaiKalau salah
httpOnlytrueXSS bisa mencuri sesi lewat document.cookie
securetrueCookie terkirim lewat HTTP polos
sameSite"lax""none" membuka CSRF; "strict" memutus alur kembali dari Midtrans
path"/"Diwajibkan oleh __Host-
maxAgeEksplisitTanpa 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.