Session + CSRF vs Stateless JWT
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:
| Aspek | Session | Stateless JWT |
|---|---|---|
| Di mana "siapa saya" disimpan | Di server | Di dalam token klien |
| Server menyimpan state | Ya (serverful) | Tidak (stateless) |
| Request membawa apa | Hanya session id | Seluruh token (payload ikut) |
| Verifikasi di server | Cari session di store | Periksa 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:
- Login — server memvalidasi kredensial, lalu membuat session yang berisi data pengguna (misal
userId,role,iat). - Cookie hanya berisi id — browser menerima cookie bernama
JSESSIONIDyang berisi session id acak, bukan data pengguna. Isi session tetap di server. - 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:
| Policy | Arti |
|---|---|
IF_REQUIRED | Buat session kalau dibutuhkan saja (default) |
ALWAYS | Selalu buat session, bahkan tanpa login |
NEVER | Spring Security tidak membuat session, tapi tetap membaca cookie session yang dikirim klien |
STATELESS | Tidak pernah membuat atau membaca session — untuk JWT / API |
Catatan penting:
STATELESStidak 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:
| Pencegahan | Cara kerja | Kekuatan / Catatan |
|---|---|---|
| Token CSRF | Server mengirim token rahasia (via template/form); browser harus menyertakannya di setiap request berbahaya | Terkuat untuk request yang mengubah state; dasar dari dokumentasi CSRF Spring Security |
Cookie SameSite | Atribut 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 header | Wajibkan header khusus seperti X-Requested-With | Request 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 (menyimpanjtiyang 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 dilocalStorage, 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
| Aspek | Session (server-side) | Stateless JWT |
|---|---|---|
| Lokasi state | Server (memory/DB/Redis) | Token klien |
| Server menyimpan state | Ya | Tidak |
| Logout instan | Ya — hapus session | Tidak — butuh blacklist / tunggu exp |
| Revoke (cabut akses) | Mudah, langsung | Sulit sebelum exp |
| Ancaman utama | CSRF (cookie otomatis) | XSS (token di localStorage) |
| Skala horizontal | Butuh shared store / sticky session | Cukup kunci verifikasi bersama |
| Multi-klien (mobile, IoT) | Cookie merepotkan | Header universal, mudah |
| Muatan request | Kecil (hanya id) | Besar (seluruh klaim) |
| Kapan cocok | Browser-first, satu origin | SPA, 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.
| Skenario | Rekomendasi |
|---|---|
| Aplikasi web satu origin, form login, butuh logout instan | Session |
| API dipakai SPA + mobile + pihak ketiga | JWT |
| Butuh admin mencabut akses pengguna seketika | Session (atau JWT + blacklist) |
| Trafik naik turun, server ditambah dikurangi cepat | JWT |
| SSO lintas banyak aplikasi | OAuth2 / OIDC |
Pola Hybrid: Refresh + Access Token
Di produksi, Anda jarang memakai salah satu "murni". Pola yang paling umum justru hibrida:
- Saat login, server menerbitkan access token (berumur pendek, misal 15–30 menit) dan refresh token (berumur panjang, misal 7–30 hari).
- Access token dipakai untuk memanggil API — tetap stateless dan cepat.
- Ketika access token kedaluwarsa, klien menukar refresh token ke endpoint khusus untuk mendapat access token baru.
- 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), danSameSite=LaxatauStrict. Jangan pernah matikan CSRF. - JWT di
localStorage→ hanya jika aplikasi benar-benar bebas XSS (sulit dijamin 100%), dan selalu denganexppendek. - 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
- Session Management (Spring Security) — dokumentasi resmi konfigurasi session.
- CSRF Protection (Spring Security) — mekanisme CSRF default dan cara kerjanya.
- CSRF — OWASP — deskripsi resmi serangan CSRF dari OWASP.
- Auth0: JSON Web Tokens — panduan konsep dan praktik JWT dari vendor identitas.
- JWT (JSON Web Token) untuk Autentikasi — struktur JWT, HS256 vs RS256, dan verifikasi.
- OAuth2 & OIDC — standar autentikasi enterprise untuk SSO.
- Autentikasi vs Otorisasi — perbedaan identitas dan hak akses.
- Password Hashing dengan BCrypt — cara menyimpan kredensial dengan benar.