Pengguna berbelanja lima belas menit: membuka katalog, menaruh barang ke keranjang, membuka halaman lain, lalu membayar. Padahal setiap klik adalah permintaan HTTP yang saling asing — Chapter HTTP Fundamentals sudah menegaskan protokol ini tidak menyimpan apa pun tentang permintaan sebelumnya. Agar toko tetap mengenal pembelanjanya, aplikasi harus membangun memori sendiri di atas protokol stateless lewat dua mekanisme yang saling melengkapi: cookie sebagai kartu nama kecil yang dibawa klien, dan session sebagai lemari penyimpanan di sisi server. Chapter ini membahas keduanya lalu mensimulasikan percakapan lengkapnya dalam Java murni.
Cookie sebagai Kartu Nama #
Cookie adalah potongan data yang server titipkan ke klien lewat header Set-Cookie, lalu klien kirim ulang otomatis pada setiap permintaan berikutnya melalui header Cookie. Spesifikasinya (RFC 6265) menetapkan sejumlah atribut yang mengatur perilaku cookie:
| Atribut | Peran |
|---|---|
Path=/ | cookie dikirim hanya untuk path di bawah / |
Max-Age=3600 | cookie kedaluwarsa 3600 detik setelah diterima |
Secure | cookie hanya dikirim lewat HTTPS |
HttpOnly | JavaScript di halaman tidak boleh membaca cookie |
SameSite=Lax | cookie tidak dikirim pada permintaan lintas situs dari situs lain |
Dua atribut terakhir adalah baris pertahanan keamanan: HttpOnly melindungi cookie dari pencurian lewat script yang tertanam di halaman (XSS), dan SameSite memutus sebagian besar pola CSRF — keduanya akan muncul lagi pada Chapter Keamanan Web Application.
Session sebagai Lemari di Server #
Cookie hanya layak membawa identifier kecil, bukan data belanja utuh — data di klien bisa dibaca dan dipalsukan. Pola yang benar adalah session: server menyimpan seluruh data di memorinya, lalu hanya menitipkan kunci berupa identifier acak pada cookie. Pada servlet, kunci tersebut bernama JSESSIONID dan datanya hidup dalam objek HttpSession:
HttpSession sesi = req.getSession(); // membuat sesi baru bila belum ada
sesi.setAttribute("keranjang", daftarBelanja);
List<Barang> isi = (List<Barang>) sesi.getAttribute("keranjang");
sesi.setMaxInactiveInterval(1800); // kedaluwarsa setelah 30 menit tidak aktif
sesi.invalidate(); // logout: hapus sesi sekarangContainer mengurus pembuatan identifier, pencocokan cookie JSESSIONID yang masuk dengan peta session di server, serta pembersihan session yang kedaluwarsa (default umumnya 30 menit tanpa aktivitas). Perbedaan peran keduanya dirangkum tabel berikut:
| Aspek | Cookie | Session |
|---|---|---|
| Tempat penyimpanan | peramban pengguna | memori/disk server |
| Isi yang wajar | preferensi, identifier | data sensitif, keranjang, status login |
| Kepercayaan | rendah — bisa dibaca dan dipalsukan | tinggi — klien hanya pegang kunci |
| Umur | ditentukan Max-Age | ditentukan timeout server atau invalidate() |
Simulasi Percakapan Cookie dan Session #
Program SesiMini memerankan dua pihak: PerambanMini yang menyimpan cookie pada toples kecil, dan TokoSesi yang menyimpan kunjungan per session di server. Identifier session dibangkitkan dari penghitung agar output simulasi tetap sama pada setiap eksekusi — pada container sungguhan identifier ini nilai acak yang tidak mudah ditebak:
import java.util.LinkedHashMap;
import java.util.Map;
public class SesiMini {
static class PerambanMini {
private final Map<String, String> toples = new LinkedHashMap<>();
void simpanDariSetCookie(String headerSetCookie) {
String pasangan = headerSetCookie.split(";")[0].trim();
int samaDengan = pasangan.indexOf('=');
toples.put(pasangan.substring(0, samaDengan),
pasangan.substring(samaDengan + 1));
}
String headerCookie() {
if (toples.isEmpty()) {
return "(tanpa cookie)";
}
StringBuilder b = new StringBuilder();
int i = 0;
for (Map.Entry<String, String> e : toples.entrySet()) {
if (i++ > 0) {
b.append("; ");
}
b.append(e.getKey()).append('=').append(e.getValue());
}
return b.toString();
}
void hapusCookie(String nama) {
toples.remove(nama);
}
}
static class TokoSesi {
private final Map<String, Integer> kunjunganSesi = new LinkedHashMap<>();
private int nomorBerikut = 0;
String buatSetCookie(String idSesi) {
return "JSESSIONID=" + idSesi + "; Path=/; HttpOnly; SameSite=Lax";
}
String tangani(String label, PerambanMini peramban) {
System.out.println(label + ": Cookie: " + peramban.headerCookie());
String idSesi = peramban.toples.get("JSESSIONID");
if (idSesi == null || !kunjunganSesi.containsKey(idSesi)) {
idSesi = String.format("SESI-%03d", ++nomorBerikut);
kunjunganSesi.put(idSesi, 1);
peramban.simpanDariSetCookie(buatSetCookie(idSesi));
System.out.println(label + ": Set-Cookie: " + buatSetCookie(idSesi));
return "halo! sesi baru " + idSesi + " dibuat";
}
int kunjungan = kunjunganSesi.merge(idSesi, 1, Integer::sum);
return "selamat datang kembali, kunjungan ke-" + kunjungan;
}
void keluar(PerambanMini peramban) {
String idSesi = peramban.toples.get("JSESSIONID");
if (idSesi != null) {
kunjunganSesi.remove(idSesi);
}
peramban.hapusCookie("JSESSIONID");
System.out.println("logout : sesi " + idSesi + " dihapus, cookie dibatalkan");
}
}
public static void main(String[] args) {
PerambanMini peramban = new PerambanMini();
TokoSesi toko = new TokoSesi();
System.out.println("Balasan : " + toko.tangani("permintaan1", peramban));
System.out.println();
System.out.println("Balasan : " + toko.tangani("permintaan2", peramban));
System.out.println();
System.out.println("Balasan : " + toko.tangani("permintaan3", peramban));
System.out.println();
toko.keluar(peramban);
System.out.println();
System.out.println("Balasan : " + toko.tangani("permintaan4", peramban));
}
}Output yang Dihasilkan:
permintaan1: Cookie: (tanpa cookie)
permintaan1: Set-Cookie: JSESSIONID=SESI-001; Path=/; HttpOnly; SameSite=Lax
Balasan : halo! sesi baru SESI-001 dibuat
permintaan2: Cookie: JSESSIONID=SESI-001
Balasan : selamat datang kembali, kunjungan ke-2
permintaan3: Cookie: JSESSIONID=SESI-001
Balasan : selamat datang kembali, kunjungan ke-3
logout : sesi SESI-001 dihapus, cookie dibatalkan
permintaan4: Cookie: (tanpa cookie)
permintaan4: Set-Cookie: JSESSIONID=SESI-002; Path=/; HttpOnly; SameSite=Lax
Balasan : halo! sesi baru SESI-002 dibuatMari kita baca percakapan pada output bersama bedah kodenya:
- Baris 9-14 (
simpanDariSetCookie): peramban mem-parse headerSet-Cookiedengan membuang seluruh atribut setelah titik koma — hanya pasangan nama-nilai pertama yang disimpan. AtributHttpOnlydanSameSiteadalah instruksi bagi peramban, bukan data yang disimpan. - Baris 16-29 (
headerCookie): format headerCookieyang dikirim kembali adalahnama=nilai; nama2=nilai2— perhatikan pemisah titik koma-spasi, berbeda dengan formatSet-Cookieyang memakai titik koma untuk atribut. - Baris 44-53 (
tanganiversi sesi baru): permintaan pertama tiba tanpa cookie. Server membuat identifierSESI-001, mendaftarkannya, lalu mengirimSet-Cookielengkap dengan tiga atribut perlindungan. Inilah tepatnya pekerjaanreq.getSession()tanpa argumen pada servlet. - Baris 46-47 (pemeriksaan dua kondisi): identifier bisa tidak ada atau ada tetapi sudah tidak dikenal server (misalnya session sudah kedaluwarsa atau sudah di-invalidate). Dua kondisi ini menghasilkan perlakuan yang sama: buat sesi baru — pengguna tidak akan mendapat galat, hanya kehilangan data sesi lama.
- Baris 54 (
merge(idSesi, 1, Integer::sum)): kunjungan dihitung di server, bukan di cookie. Klien tidak pernah bisa memanipulasi angka kunjungan karena yang dibawa hanyalah kunci. - Baris 58-65 (
keluar): logout menghapus dua sisi sekaligus — session di server dan cookie di peramban. Menghapus salah satu saja menyisakan kebocoran kecil: session tanpa pemilik menunggu timeout di server, atau cookie tanpa pasangan yang membuat server terus membuat session baru.
Data session pada simulasi kita berupa angka kunjungan; pada aplikasi nyata isinya lebih kaya — nama pengguna, isi keranjang, hasil pencarian. Menampilkan data tersebut sebagai halaman HTML yang rapi adalah pekerjaan template engine, topik Chapter Template Rendering.
Referensi #
- RFC 6265: HTTP State Management Mechanism
- Jakarta Servlet 6.1 Specification
- Jakarta Servlet 6.1 API Documentation
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
SesiMini.java
Unduh berkasimport java.util.LinkedHashMap;
import java.util.Map;
public class SesiMini {
static class PerambanMini {
private final Map<String, String> toples = new LinkedHashMap<>();
void simpanDariSetCookie(String headerSetCookie) {
String pasangan = headerSetCookie.split(";")[0].trim();
int samaDengan = pasangan.indexOf('=');
toples.put(pasangan.substring(0, samaDengan),
pasangan.substring(samaDengan + 1));
}
String headerCookie() {
if (toples.isEmpty()) {
return "(tanpa cookie)";
}
StringBuilder b = new StringBuilder();
int i = 0;
for (Map.Entry<String, String> e : toples.entrySet()) {
if (i++ > 0) {
b.append("; ");
}
b.append(e.getKey()).append('=').append(e.getValue());
}
return b.toString();
}
void hapusCookie(String nama) {
toples.remove(nama);
}
}
static class TokoSesi {
private final Map<String, Integer> kunjunganSesi = new LinkedHashMap<>();
private int nomorBerikut = 0;
String buatSetCookie(String idSesi) {
return "JSESSIONID=" + idSesi + "; Path=/; HttpOnly; SameSite=Lax";
}
String tangani(String label, PerambanMini peramban) {
System.out.println(label + ": Cookie: " + peramban.headerCookie());
String idSesi = peramban.toples.get("JSESSIONID");
if (idSesi == null || !kunjunganSesi.containsKey(idSesi)) {
idSesi = String.format("SESI-%03d", ++nomorBerikut);
kunjunganSesi.put(idSesi, 1);
peramban.simpanDariSetCookie(buatSetCookie(idSesi));
System.out.println(label + ": Set-Cookie: " + buatSetCookie(idSesi));
return "halo! sesi baru " + idSesi + " dibuat";
}
int kunjungan = kunjunganSesi.merge(idSesi, 1, Integer::sum);
return "selamat datang kembali, kunjungan ke-" + kunjungan;
}
void keluar(PerambanMini peramban) {
String idSesi = peramban.toples.get("JSESSIONID");
if (idSesi != null) {
kunjunganSesi.remove(idSesi);
}
peramban.hapusCookie("JSESSIONID");
System.out.println("logout : sesi " + idSesi + " dihapus, cookie dibatalkan");
}
}
public static void main(String[] args) {
PerambanMini peramban = new PerambanMini();
TokoSesi toko = new TokoSesi();
System.out.println("Balasan : " + toko.tangani("permintaan1", peramban));
System.out.println();
System.out.println("Balasan : " + toko.tangani("permintaan2", peramban));
System.out.println();
System.out.println("Balasan : " + toko.tangani("permintaan3", peramban));
System.out.println();
toko.keluar(peramban);
System.out.println();
System.out.println("Balasan : " + toko.tangani("permintaan4", peramban));
}
}