Verifikasi sandi pada Chapter sebelumnya hanya berlaku untuk satu permintaan. HTTP tidak menyimpan status antar permintaan, jadi begitu Budi meminta endpoint berikutnya, server sudah tidak ingat bahwa login barusan berhasil. Session menyelesaikan masalah ini dengan menyimpan status login di server, lalu memakai cookie sebagai tanda pengenal yang dibawa setiap permintaan klien. Pada chapter ini kita membedah anatomi keduanya dan membangun alur login lengkap: masuk, tercatat, dikenali, lalu keluar.
Cookie: Tanda Pengenal dari Server #
Cookie adalah potongan data kecil yang dikirim server lewat header Set-Cookie, disimpan browser, lalu dikirim balik otomatis pada setiap permintaan berikutnya ke domain yang sama. Untuk keperluan login, isinya sengaja dibuat tidak bermakna — hanya id sesi acak; seluruh data pengguna tetap berada di server.
Header Set-Cookie membawa pasangan nama-nilai plus sekumpulan atribut yang mengatur perilakunya:
| Atribut | Peran |
|---|---|
HttpOnly | Melarang JavaScript di halaman membaca cookie — pertahanan pertama terhadap pencurian lewat XSS |
Secure | Cookie hanya dikirim lewat koneksi HTTPS |
SameSite=Lax / Strict | Membatasi pengiriman cookie dari situs lain — pertahanan terhadap CSRF |
Path=/ | Cookie berlaku untuk seluruh path pada domain tersebut |
Max-Age=3600 | Umur cookie dalam detik; lewat dari itu browser menghapusnya sendiri |
Transkrip ringkas berikut memperlihatkan dialog aslinya antara browser dan server:
POST /login HTTP/1.1
Content-Type: application/json
{"nama": "Budi"}
HTTP/1.1 200 OK
Set-Cookie: sesi=1f9a7c...; HttpOnly; SameSite=Lax; Path=/
GET /profil HTTP/1.1
Cookie: sesi=1f9a7c...Browser mem-parsing header Set-Cookie untuk memutuskan bagaimana cookie disimpan dan dikirim. Kita bisa meniru logika parsing itu dalam function kecil:
function bacaSetCookie(header) {
const [pasangan, ...atribut] = header.split('; ');
const [nama, nilai] = pasangan.split('=');
const hasil = { nama, nilai };
for (const a of atribut) {
const [kunci, nilaiAtribut] = a.split('=');
hasil[kunci.toLowerCase()] = nilaiAtribut ?? true;
}
return hasil;
}
const cookie = bacaSetCookie('sesi=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600');
console.log(JSON.stringify(cookie, null, 2));Output yang Dihasilkan:
{
"nama": "sesi",
"nilai": "abc123",
"httponly": true,
"secure": true,
"samesite": "Lax",
"path": "/",
"max-age": "3600"
}Atribut tanpa nilai seperti HttpOnly tersimpan sebagai true berkat operator ?? — itulah tanda bahwa atribut tersebut berupa penanda, bukan pasangan kunci-nilai.
Session Store di Sisi Server #
Di sisi server, status login berupa Map (atau tabel database, atau Redis pada sistem besar) yang memetakan id sesi ke data pengguna. Id sesi dibuat dengan randomBytes(16).toString('hex') — 32 karakter heksadesimal yang tidak bisa ditebak. Perhatikan pembagian kerjanya: cookie hanya membawa kunci laci, sedangkan isi laci — identitas, peran, preferensi — tidak pernah meninggalkan server.
Alur lengkapnya berjalan lima langkah: klien mengirim kredensial ke POST /login; server memverifikasi lalu membuat id sesi baru dan menyimpannya di session store; server mengirim response dengan header Set-Cookie; browser menyimpan cookie dan melampirkannya pada setiap permintaan berikutnya; server mencari id itu di session store untuk mengenali pengirimnya.
Demo Login Sampai Logout #
Berkas berikut menjalankan seluruh siklus dalam satu proses: server dan klien hidup berdampingan memakai pola listen(0) yang sudah akrab dari Section HTTP dan Web Servers.
import http from 'node:http';
import { randomBytes } from 'node:crypto';
// Session store: idSesi -> data pengguna. Hanya hidup selama proses ini.
const sesi = new Map();
function ambilSesiDariCookie(headerCookie) {
if (headerCookie === undefined) return null;
for (const pasangan of headerCookie.split(';')) {
const [nama, nilai] = pasangan.trim().split('=');
if (nama === 'sesi') return nilai;
}
return null;
}
function bacaBodyJson(req) {
return new Promise((resolve, reject) => {
let teks = '';
req.on('data', (potongan) => (teks += potongan));
req.on('end', () => resolve(JSON.parse(teks)));
req.on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://127.0.0.1`);
const kirim = (kode, data) => {
res.writeHead(kode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
};
if (req.method === 'POST' && url.pathname === '/login') {
const body = await bacaBodyJson(req);
const idSesi = randomBytes(16).toString('hex');
sesi.set(idSesi, { nama: body.nama });
res.writeHead(200, {
'Content-Type': 'application/json',
'Set-Cookie': `sesi=${idSesi}; HttpOnly; SameSite=Lax; Path=/`,
});
res.end(JSON.stringify({ nama: body.nama }));
console.log(`server : Set-Cookie: sesi=<${idSesi.length} karakter acak>; HttpOnly; SameSite=Lax; Path=/`);
return;
}
if (req.method === 'GET' && url.pathname === '/profil') {
const idSesi = ambilSesiDariCookie(req.headers.cookie);
if (idSesi === null || !sesi.has(idSesi)) {
return kirim(401, { error: 'Belum masuk' });
}
return kirim(200, sesi.get(idSesi));
}
if (req.method === 'POST' && url.pathname === '/logout') {
const idSesi = ambilSesiDariCookie(req.headers.cookie);
if (idSesi !== null) sesi.delete(idSesi);
console.log('server : sesi dihapus dari session store');
res.writeHead(204);
return res.end();
}
return kirim(404, { error: 'Endpoint tidak ditemukan' });
});
server.listen(0, '127.0.0.1', async () => {
const basis = `http://127.0.0.1:${server.address().port}`;
console.log('klien : GET /profil (belum punya cookie)');
const r1 = await fetch(`${basis}/profil`);
console.log('server :', r1.status, await r1.text());
console.log('klien : POST /login {"nama":"Budi"}');
const login = await fetch(`${basis}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama: 'Budi' }),
});
console.log('server :', login.status, await login.text());
// Ambil pasangan "sesi=<id>" dari Set-Cookie lalu kirim balik sebagai
// header Cookie - persis pekerjaan yang dilakukan browser secara otomatis.
const cookie = login.headers.get('set-cookie').split(';')[0];
console.log('klien : GET /profil (Cookie: sesi=...)');
const r2 = await fetch(`${basis}/profil`, { headers: { Cookie: cookie } });
console.log('server :', r2.status, await r2.text());
console.log('klien : POST /logout (Cookie: sesi=...)');
await fetch(`${basis}/logout`, { method: 'POST', headers: { Cookie: cookie } });
console.log('klien : GET /profil (cookie lama, sesi sudah dihapus)');
const r3 = await fetch(`${basis}/profil`, { headers: { Cookie: cookie } });
console.log('server :', r3.status, await r3.text());
server.close();
});Output yang Dihasilkan:
klien : GET /profil (belum punya cookie)
server : 401 {"error":"Belum masuk"}
klien : POST /login {"nama":"Budi"}
server : Set-Cookie: sesi=<32 karakter acak>; HttpOnly; SameSite=Lax; Path=/
server : 200 {"nama":"Budi"}
klien : GET /profil (Cookie: sesi=...)
server : 200 {"nama":"Budi"}
klien : POST /logout (Cookie: sesi=...)
server : sesi dihapus dari session store
klien : GET /profil (cookie lama, sesi sudah dihapus)
server : 401 {"error":"Belum masuk"}Mari kita bedah titik-titik kuncinya:
- Baris 7-14 (
ambilSesiDariCookie): headerCookieberisi daftar pasangannama=nilaiyang dipisah titik koma, sehingga pemeriksaan dilakukan dengan memecah string lalu mencari namasesi. Browser mengirim semua cookie yang cocok untuk domain, bukan hanya milik aplikasi kita. - Baris 32-43 (handler
/login): id sesi baru dibuat untuk setiap login — bukan sekali untuk selamanya. Session store dan headerSet-Cookiesama-sama diisi pada baris 35 dan 38. - Baris 41: log server mencetak struktur cookie lengkap dengan nilai acak yang disamarkan panjangnya, agar transkrip contoh tetap sama antar pembaca meski id sesi selalu baru.
- Baris 45-51 (handler
/profil): inilah kekuatan arsitektur session — identitas tidak pernah diambil dari cookie, melainkan dari session store. Cookie palsu tanpa pasangan diMaplangsung ditolak401. - Baris 53-59 (handler
/logout): logout hanyalah penghapusan baris di session store. Cookie boleh tetap tersimpan di browser; setelah barisnya hilang, cookie tersebut jadi tanda pengenal tanpa isi.
Rotasi Id dan Session Fixation #
Id sesi yang tetap sama sejak sebelum login hingga sesudahnya membuka celah session fixation: penyerang menanamkan id sesi yang sudah diketahui di browser pengguna sasaran (misalnya lewat tautan berisi parameter sesi), lalu menunggu pengguna itu login dengan id tersebut sehingga penyerang ikut tercatat sebagai pemilik sesi. Pertahanannya adalah rotasi: server membuang id lama dan menerbitkan id segar setiap kali tingkat hak berubah — paling penting tepat setelah login berhasil dan setelah perubahan sandi.
Session menempatkan kepercayaan sepenuhnya di server. Keluarga solusi kedua membalik logikanya: seluruh informasi identitas dijelaskan di dalam token yang ditandatangani, sehingga server tidak perlu menyimpan apa pun. Chapter Token dan JWT membangun mekanisme itu dari nol dengan node:crypto.
Detail lengkap atribut cookie terdokumentasi pada referensi header Set-Cookie
dan panduan cookie HTTP
di MDN, termasuk atribut Domain dan Expires yang tidak dibahas contoh ini.