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 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:

AtributPeran
Path=/cookie dikirim hanya untuk path di bawah /
Max-Age=3600cookie kedaluwarsa 3600 detik setelah diterima
Securecookie hanya dikirim lewat HTTPS
HttpOnlyJavaScript di halaman tidak boleh membaca cookie
SameSite=Laxcookie 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:

java Session pada servlet nyata
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 sekarang

Container 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:

AspekCookieSession
Tempat penyimpananperamban penggunamemori/disk server
Isi yang wajarpreferensi, identifierdata sensitif, keranjang, status login
Kepercayaanrendah — bisa dibaca dan dipalsukantinggi — klien hanya pegang kunci
Umurditentukan Max-Ageditentukan timeout server atau invalidate()

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:

java SesiMini.java
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:

text
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 dibuat

Mari kita baca percakapan pada output bersama bedah kodenya:

  1. Baris 9-14 (simpanDariSetCookie): peramban mem-parse header Set-Cookie dengan membuang seluruh atribut setelah titik koma — hanya pasangan nama-nilai pertama yang disimpan. Atribut HttpOnly dan SameSite adalah instruksi bagi peramban, bukan data yang disimpan.
  2. Baris 16-29 (headerCookie): format header Cookie yang dikirim kembali adalah nama=nilai; nama2=nilai2 — perhatikan pemisah titik koma-spasi, berbeda dengan format Set-Cookie yang memakai titik koma untuk atribut.
  3. Baris 44-53 (tangani versi sesi baru): permintaan pertama tiba tanpa cookie. Server membuat identifier SESI-001, mendaftarkannya, lalu mengirim Set-Cookie lengkap dengan tiga atribut perlindungan. Inilah tepatnya pekerjaan req.getSession() tanpa argumen pada servlet.
  4. 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.
  5. 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.
  6. 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

SesiMini.java

Unduh berkas
java
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));
    }
}