Session + CSRF vs Stateless JWT

12 min readIntermediate
SessionJWTCSRFSecurity

Di artikel JWT untuk Autentikasi kita sudah melihat kenapa token stateless populer. Sekarang saatnya membahas pertanyaan yang selalu muncul di forum dan wawancara kerja: session server-side vs JWT stateless, mana yang lebih baik? Jawaban jujurnya: tidak ada yang lebih baik secara mutlak. Keduanya adalah cara berbeda menyimpan "bukti login" (bukti bahwa klien sudah terautentikasi), masing-masing dengan kelebihan, kekurangan, dan ancaman keamanan yang khas. Artikel ini membedah keduanya dari nol — termasuk mekanisme CSRF yang jarang benar-benar dipahami — supaya Anda bisa memilih dengan alasan, bukan sekadar tren.

Dua Pola Menyimpan Bukti Login

Setelah berhasil login, server harus memastikan bahwa request berikutnya benar-benar datang dari pengguna yang sama. Pertanyaannya cuma satu: di mana bukti login itu disimpan?

  • Session server-side — bukti login disimpan di server (memory, database, atau Redis), dan klien hanya memegang sebuah session id acak. Setiap request, server mencocokkan id itu dengan data session-nya.
  • Stateless JWT — bukti login disimpan di dalam token itu sendiri (identitas ditandatangani dan dibawa klien). Server tidak menyimpan apa pun; cukup memverifikasi tanda tangannya.

Perbedaan inilah akar dari semua kelebihan dan kekurangan keduanya. Diagram berikut merangkumnya:

Session vs Stateless JWT

AspekSessionStateless JWT
Di mana "siapa saya" disimpanDi serverDi dalam token klien
Server menyimpan stateYa (serverful)Tidak (stateless)
Request membawa apaHanya session idSeluruh token (payload ikut)
Verifikasi di serverCari session di storePeriksa signature kriptografi

Ingat kuncinya: session bertanya ke server "siapa pemilik id ini?" — JWT berkata pada dirinya sendiri "saya adalah user id 42, dan ini tanda tangannya." Yang satu butuh storage, yang lain butuh kunci rahasia.

Session Server-Side

Ini adalah pendekatan klasik yang sudah ada sejak awal web. Alurnya seperti ini:

  Browser                          Server
    |                                |
    | 1. POST /login {user, pass}    |
    |------------------------------->|
    |                      2. Cek kredensial
    |                      3. Simpan session di memory/Redis
    | 4. Set-Cookie: JSESSIONID=abc   |
    |<-------------------------------|
    |                                |
    | 5. GET /profile                |
    |    Cookie: JSESSIONID=abc      |
    |------------------------------->|
    |                      6. Cocokkan id ke session store
    |                      7. Proses request
    | 8. 200 OK {data}               |
    |<-------------------------------|

Beberapa hal penting dari alur di atas:

  1. Login — server memvalidasi kredensial, lalu membuat session yang berisi data pengguna (misal userId, role, iat).
  2. Cookie hanya berisi id — browser menerima cookie bernama JSESSIONID yang berisi session id acak, bukan data pengguna. Isi session tetap di server.
  3. Setiap request — browser otomatis mengirim cookie, server mencocokkan id-nya ke session store, lalu menandai request sebagai terautentikasi.

Kelebihan Session

  • Logout instan — cukup hapus session dari store, token/session id tidak berlaku seketika. Ini sangat penting untuk aplikasi dengan kebijakan keamanan ketat.
  • Mudah dicabut — admin bisa memutus sesi pengguna tertentu (misalnya karena akun disusupi) tanpa menunggu token kedaluwarsa.
  • Kontrol penuh — server bisa melacak jumlah sesi aktif, memaksa login ulang, atau menerapkan kebijakan concurrent session limit.

Kekurangan Session

  • Butuh shared storage — kalau aplikasi berjalan di banyak server, session harus disimpan di penyimpanan bersama (misal Redis) atau memakai sticky session (request pengguna selalu diarahkan ke server yang sama). Keduanya menambah kompleksitas infrastruktur.
  • Kurang cocok untuk API multi-klien — cookie adalah mekanisme browser. Aplikasi mobile, desktop, atau IoT harus meniru perilaku cookie secara manual, yang menyulitkan.
  • Stateful menyulitkan scaling — setiap request "menempel" pada data session, jadi server harus selalu bisa menjangkau session store tersebut.

Konfigurasi Session di Spring Security

Di Spring Security, manajemen session dikonfigurasi lewat sessionManagement. Ini adalah konfigurasi session yang khas:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.enable())
            .formLogin(Customizer.withDefaults())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }
}

SessionCreationPolicy.IF_REQUIRED berarti: buat session hanya jika memang dibutuhkan (misalnya setelah login), bukan untuk setiap request. Opsi lainnya:

PolicyArti
IF_REQUIREDBuat session kalau dibutuhkan saja (default)
ALWAYSSelalu buat session, bahkan tanpa login
NEVERSpring Security tidak membuat session, tapi tetap membaca cookie session yang dikirim klien
STATELESSTidak pernah membuat atau membaca session — untuk JWT / API

Catatan penting: STATELESS tidak berarti "tidak aman" atau "tanpa autentikasi". Ia hanya bilang ke Spring Security: jangan simpan session di server; autentikasi akan ditangani lewat mekanisme lain (misalnya JWT).

CSRF: Ancaman yang Ikut Menempel pada Session

Kalau Anda memakai cookie session, Anda otomatis mewarisi satu masalah keamanan yang tidak dimiliki pendekatan token di header: CSRF (Cross-Site Request Forgery). Mari kita pahami dari akarnya.

Apa itu CSRF

Cross-Site Request Forgery adalah serangan yang memaksa pengguna yang sudah login untuk mengeksekusi aksi yang tidak diinginkannya. Serangan ini berhasil karena satu perilaku browser yang mendasar: cookie session dikirim otomatis ke situs asalnya, tanpa perlu diminta.

Bayangkan Anda login di internet-banking dengan session cookie. Lalu Anda membuka situs lain yang berisi:

<img src="https://bank.example.com/transfer?to=ATTACKER&amount=10000000">

Browser mengirim request GET ke alamat itu beserta cookie session Anda secara otomatis. Server bank melihat cookie yang valid, jadi menganggap request itu sah dari Anda — padahal Anda tidak pernah memintanya. Itulah CSRF.

  User login di bank (cookie session tersimpan di browser)
        |
        |  Buka situs jahat: <img src="bank.example.com/transfer?...">
        v
  Browser otomatis mengirim cookie + request transfer
        |
        v
  Server bank: cookie valid -> proses transfer! (padahal bukan niat user)

Kenapa same-origin policy tidak mencegah ini? Karena kebijakan itu mengatur membaca respons lintas origin, bukan mengirim request. Untuk request sederhana (misal POST dengan application/x-www-form-urlencoded), browser tetap mengizinkan pengirimannya lengkap dengan cookie — Anda hanya tidak bisa membaca hasilnya dari situs jahat. Dan untuk efek serangan CSRF, hasilnya tidak perlu dibaca: aksi sudah terlanjur dieksekusi.

Pencegahan CSRF

Karena akar masalahnya adalah cookie yang dikirim otomatis, semua solusi pada dasarnya memaksa request "asli" membawa bukti tambahan yang tidak otomatis dikirim oleh browser:

PencegahanCara kerjaKekuatan / Catatan
Token CSRFServer mengirim token rahasia (via template/form); browser harus menyertakannya di setiap request berbahayaTerkuat untuk request yang mengubah state; dasar dari dokumentasi CSRF Spring Security
Cookie SameSiteAtribut cookie: Strict (tidak pernah lintas situs) atau Lax (hanya untuk navigasi GET)Perlindungan bagus di browser modern, tapi Lax masih mengizinkan GET lintas situs
Custom headerWajibkan header khusus seperti X-Requested-WithRequest lintas situs tidak bisa menambahkan header kustom tanpa izin CORS

Prinsipnya: browser mengirim cookie dan form body secara otomatis, tapi tidak mengirim token CSRF atau header kustom secara otomatis. Jadi bukti tambahan itulah yang membedakan request asli dari request palsu.

Implementasi di Spring Security

Untuk aplikasi berbasis session, CSRF protection di Spring Security aktif secara default — Anda tidak perlu menulis apa-apa untuk mendapatkannya. Di aplikasi yang merender template (Thymeleaf, JSP), token otomatis disuntikkan ke dalam form:

<form th:action="@{/api/transfer}" method="post">
    <input type="hidden"
           th:name="${_csrf.parameterName}"
           th:value="${_csrf.token}" />
    ...
</form>

Untuk API berbasis JavaScript, Anda membaca token dari cookie XSRF-TOKEN (dikirim Spring Security secara default) dan mengirimnya kembali lewat header X-XSRF-TOKEN. Di aplikasi browser-first dengan session, jangan matikan CSRF — itu salah satu kesalahan keamanan paling umum.

JWT Stateless

Sekarang sisi lain: JWT menyimpan bukti login di dalam token, dan token dikirim lewat header Authorization — bukan cookie. Alurnya:

  Klien (SPA/Mobile)                     Server
        |                                   |
        | 1. POST /api/auth/login           |
        |---------------------------------->|
        |                         2. Validasi kredensial
        |                         3. Tanda tangani JWT (exp pendek)
        | 4. 200 OK {accessToken}           |
        |<----------------------------------|
        |                                   |
        | 5. Simpan token                   |
        |                                   |
        | 6. GET /api/profile               |
        |    Authorization: Bearer <token>  |
        |---------------------------------->|
        |                         7. Verifikasi signature + exp
        |                         8. Proses request
        | 9. 200 OK {data}                  |
        |<----------------------------------|

Kelebihan JWT Stateless

  • Skalabilitas horizontal tanpa shared store — server mana pun yang memegang kunci verifikasi yang sama bisa melayani request. Tidak perlu Redis bersama, tidak perlu sticky session. Cara kerja detail verifikasi JWT sudah kita bahas sebelumnya.
  • Ideal untuk SPA, mobile, dan lintas layanan — token dikirim manual lewat header, jadi mekanismenya identik untuk semua jenis klien. Ini menjadikannya pilihan natural untuk API publik.
  • Ringan di sisi server — tidak ada lookup ke session store di setiap request; verifikasi murni kriptografi.

Kekurangan JWT Stateless

  • Tidak bisa dicabut sebelum exp — selama signature valid dan belum kedaluwarsa, token tetap diterima. Logout butuh blacklist (menyimpan jti yang dicabut) atau menunggu token kedaluwarsa.
  • Token besar — semua klaim ikut dalam setiap request, membebani bandwidth sedikit demi sedikit.
  • Rawan disalah-simpan di localStorage — kalau token ditaruh di localStorage, script XSS yang berhasil mengeksekusi bisa mencurinya. Ini risiko yang kita bahasa saat membahas penyimpanan token.

Implementasi di Spring Security

Konfigurasi JWT stateless di Spring Security — perhatikan STATELESS dan CSRF dimatikan:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}

Mengapa csrf.disable() di sini? Karena CSRF adalah ancaman yang muncul karena browser mengirim cookie secara otomatis. JWT di header Authorization tidak dikirim otomatis oleh browser — script yang menaruhnya di header harus punya akses ke token itu. Jadi masalah CSRF klasik memang tidak berlaku untuk bearer token di header.

Tapi hati-hati: JWT yang disimpan di cookie httpOnly (sebagai pengganti localStorage) kembali membawa masalah CSRF, karena cookie itu ikut terkirim otomatis. Kalau Anda menyimpan JWT di cookie, Anda tetap butuh proteksi CSRF — jalan ceritanya jadi mirip session.

Perbandingan Akhir

AspekSession (server-side)Stateless JWT
Lokasi stateServer (memory/DB/Redis)Token klien
Server menyimpan stateYaTidak
Logout instanYa — hapus sessionTidak — butuh blacklist / tunggu exp
Revoke (cabut akses)Mudah, langsungSulit sebelum exp
Ancaman utamaCSRF (cookie otomatis)XSS (token di localStorage)
Skala horizontalButuh shared store / sticky sessionCukup kunci verifikasi bersama
Multi-klien (mobile, IoT)Cookie merepotkanHeader universal, mudah
Muatan requestKecil (hanya id)Besar (seluruh klaim)
Kapan cocokBrowser-first, satu originSPA, mobile, API publik

Kapan Memakai yang Mana

Aturan praktisnya bergantung pada siapa klien Anda dan di mana state harus hidup:

  • Browser-first application dengan form login dan satu origin (misalnya aplikasi admin internal, e-commerce monolith) → Session. Alurnya natural (form → cookie), logout instan, dan CSRF protection sudah menjadi bagian default Spring Security.
  • SPA, aplikasi mobile, atau API publik yang melayani banyak jenis klien → Stateless JWT. Tidak ada cookie, tidak ada CSRF klasik, dan server bisa ber-skala horizontal dengan mudah.
  • Enterprise SSO (login sekali, masuk ke banyak aplikasi) → jangan membangun sendiri. Pakai standar OAuth2 / OIDC dengan penyedia identitas (Keycloak, Okta, Auth0) — pembahasannya ada di artikel OAuth2 & OIDC.
SkenarioRekomendasi
Aplikasi web satu origin, form login, butuh logout instanSession
API dipakai SPA + mobile + pihak ketigaJWT
Butuh admin mencabut akses pengguna seketikaSession (atau JWT + blacklist)
Trafik naik turun, server ditambah dikurangi cepatJWT
SSO lintas banyak aplikasiOAuth2 / OIDC

Pola Hybrid: Refresh + Access Token

Di produksi, Anda jarang memakai salah satu "murni". Pola yang paling umum justru hibrida:

  1. Saat login, server menerbitkan access token (berumur pendek, misal 15–30 menit) dan refresh token (berumur panjang, misal 7–30 hari).
  2. Access token dipakai untuk memanggil API — tetap stateless dan cepat.
  3. Ketika access token kedaluwarsa, klien menukar refresh token ke endpoint khusus untuk mendapat access token baru.
  4. Refresh token disimpan di server / database sehingga bisa dicabut — inilah yang mengembalikan kemampuan logout instan yang hilang dari JWT murni.

Pola ini populer karena menggabungkan kelebihan keduanya: API tetap stateless dan mudah di-scale, sementara sisi "revoke" ditangani lewat refresh token yang stateful.

// Ringkasan pola hybrid di sisi service autentikasi
@Service
public class AuthService {

    public AuthTokens login(String username, String password) {
        // 1. validasi kredensial...
        String accessToken = jwtService.generateToken(username, 30 * 60 * 1000);   // 30 menit
        String refreshToken = jwtService.generateToken(username, 7L * 24 * 60 * 60 * 1000); // 7 hari
        refreshTokenStore.save(username, refreshToken); // tersimpan -> bisa dicabut
        return new AuthTokens(accessToken, refreshToken);
    }
}

Rekomendasi Keamanan

Beberapa praktik yang sebaiknya Anda jadikan kebiasaan, apa pun pendekatannya:

  • Session → kirim cookie dengan atribut httpOnly (tidak bisa dibaca JavaScript), Secure (hanya lewat HTTPS), dan SameSite=Lax atau Strict. Jangan pernah matikan CSRF.
  • JWT di localStorage → hanya jika aplikasi benar-benar bebas XSS (sulit dijamin 100%), dan selalu dengan exp pendek.
  • JWT di httpOnly cookie → bisa jadi — meredam XSS, tapi kembali butuh proteksi CSRF seperti session.
  • Apa pun pendekatannya → selalu HTTPS, hash password dengan BCrypt (lihat Password Hashing), dan perpendek masa hidup access token agar jendela bahaya kecil.

Ringkasan

Session dan stateless JWT adalah dua cara berbeda menyimpan bukti login: session menyimpannya di server dengan cookie sebagai pengantar session id; JWT menyimpannya di dalam token yang ditandatangani dan dibawa klien. Karena cookie dikirim browser secara otomatis, session wajib dilengkapi proteksi CSRF — serangan yang membuat request tidak diinginkan seolah sah dari pengguna. JWT di header tidak kena CSRF klasik, tapi tidak bisa dicabut sebelum kedaluwarsa dan rawan XSS kalau disimpan di localStorage. Pilih session untuk aplikasi browser-first yang butuh logout instan; pilih JWT untuk SPA, mobile, dan API publik yang butuh skala horizontal; dan untuk SSO enterprise gunakan OAuth2/OIDC. Di produksi, pola hybrid access + refresh token yang bisa dicabut sering jadi jawaban terbaik.

Lanjut membaca

← Back to technical articles