Tampilan
Kenapa penting
API yang kamu bangun di M07–M08 masih terbuka: siapa pun yang tahu URL bisa membaca pesanan orang lain atau menghapus produk. Di tempat kerja, hampir setiap endpoint backend dilindungi, dan Spring Security adalah standar de facto di ekosistem Spring. Masalahnya, banyak developer memakai Spring Security dengan cara "salin konfigurasi dari internet sampai jalan" — lalu bingung kenapa dapat 403 di semua request POST, atau kenapa endpoint yang seharusnya publik malah minta login. Bab ini membangun model mental yang benar: apa itu authentication dan authorization, bagaimana request melewati rangkaian filter, dan bagaimana menulis konfigurasi yang kamu pahami baris per barisnya.
Konsep inti
Authentication vs authorization
Dua istilah ini sering tertukar, padahal menjawab pertanyaan berbeda.
| Istilah | Pertanyaan | Contoh | Gagal → status |
|---|---|---|---|
| Authentication (autentikasi) | Siapa kamu? | Cek username + password, cek token | 401 Unauthorized |
| Authorization (otorisasi) | Kamu boleh melakukan ini? | Hanya ADMIN yang boleh hapus produk | 403 Forbidden |
Authentication selalu terjadi lebih dulu: server tidak bisa memutuskan izin bila tidak tahu siapa pemanggilnya. Nama status 401 Unauthorized memang menyesatkan — artinya sebenarnya "unauthenticated". Respons 401 dari HTTP Basic juga membawa header WWW-Authenticate yang memberi tahu klien cara login. Sebaliknya, 403 berarti "saya tahu kamu siapa, dan jawabannya tidak" — login ulang tidak akan menolong.
Starter security dan perilaku default
Tambahkan starter ke pom.xml (versi diatur oleh parent Spring Boot):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>Tanpa menulis satu baris konfigurasi pun, Boot langsung menerapkan default yang aman secara bawaan:
- Semua endpoint wajib terautentikasi — termasuk yang dulu bebas diakses.
- Form login (
/login) dan HTTP Basic aktif. - Satu user in-memory bernama
userdengan password acak yang dicetak di log saat startup. - Proteksi CSRF, security header (mis.
X-Content-Type-Options,Cache-Control), dan penanganan401/403aktif.
text
Using generated security password: 8f1c2d3e-....
This generated password is for development use only. Your security configuration must be updated before running your application in production.Default ini cocok untuk membuktikan bahwa security aktif, bukan untuk dipakai sungguhan. Begitu kamu mendefinisikan bean SecurityFilterChain sendiri, konfigurasi default untuk web mundur dan aturanmu yang berlaku.
SecurityFilterChain dengan lambda DSL
Sejak Spring Security 6, konfigurasi ditulis sebagai bean — class lama WebSecurityConfigurerAdapter sudah dihapus. Di Spring Security 7, gaya lama berantai dengan .and() juga sudah dihapus; yang tersisa hanya lambda DSL: setiap bagian dikonfigurasi lewat lambda yang menerima "customizer".
java
// Potongan — konfigurasi untuk REST API stateless
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.formLogin(form -> form.disable())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}Hal penting dari konfigurasi ini:
- Urutan aturan bermakna.
authorizeHttpRequestsdievaluasi dari atas; aturan pertama yang cocok menang. Taruh aturan spesifik di atas dananyRequest()paling bawah — Spring Security menolak start bila ada aturan setelahanyRequest(). hasRole("ADMIN")mencocokkan authority bernamaROLE_ADMIN. PrefixROLE_ditambahkan otomatis.formLogindimatikan karena klien API (aplikasi mobile, frontend SPA, service lain) tidak membuka halaman HTML.- CSRF dimatikan hanya karena API ini stateless dan tidak memakai cookie sesi. Bila autentikasi memakai cookie, CSRF wajib tetap aktif — dibahas tuntas di bab keamanan API.
Filter chain: jalan yang dilalui setiap request
Spring Security bekerja sebagai servlet filter yang dijalankan sebelum request mencapai DispatcherServlet dan controller. Alurnya:
text
Request → [Filter servlet lain] → DelegatingFilterProxy → FilterChainProxy
→ SecurityFilterChain (pilih chain pertama yang cocok)
→ filter 1 → filter 2 → ... → AuthorizationFilter
→ DispatcherServlet → ControllerFilterChainProxy memilih satu SecurityFilterChain yang cocok dengan request, lalu menjalankan filter-filternya secara berurutan. Beberapa filter yang paling sering kamu temui:
| Urutan (disederhanakan) | Filter | Tugas |
|---|---|---|
| awal | SecurityContextHolderFilter | Menyiapkan SecurityContext untuk request ini |
HeaderWriterFilter | Menulis security header ke respons | |
CorsFilter | Menangani CORS (bila dikonfigurasi) | |
CsrfFilter | Memeriksa token CSRF (bila aktif) | |
| tengah | BasicAuthenticationFilter | Membaca header Authorization: Basic ... dan mengautentikasi |
AnonymousAuthenticationFilter | Memberi identitas "anonim" bila belum ada yang login | |
| akhir | ExceptionTranslationFilter | Mengubah exception security menjadi 401 atau 403 |
| akhir | AuthorizationFilter | Menerapkan aturan authorizeHttpRequests |
Kamu tidak perlu menghafal semuanya. Yang penting: filter autentikasi berjalan sebelum filter otorisasi, dan ExceptionTranslationFilter yang memutuskan apakah kegagalan menjadi 401 (belum login) atau 403 (sudah login, tapi ditolak). Untuk melihat filter mana yang aktif dan keputusan apa yang diambil, nyalakan log saat development:
yaml
logging:
level:
org.springframework.security: TRACEAplikasi bisa punya beberapa SecurityFilterChain — misalnya satu untuk /api/** dan satu untuk /actuator/**. Batasi tiap chain dengan http.securityMatcher(...) dan atur prioritasnya dengan @Order; chain pertama yang cocok dipakai dan chain lain tidak dijalankan.
SecurityContext: tempat identitas disimpan
Setelah filter autentikasi berhasil, hasilnya berupa objek Authentication yang disimpan di SecurityContext, yang dipegang oleh SecurityContextHolder. Secara default holder memakai ThreadLocal, jadi identitas berlaku untuk thread yang sedang melayani request itu saja dan dibersihkan setelah request selesai.
java
// Potongan — dua cara membaca identitas pemanggil
@GetMapping("/api/me")
MeResponse me(Authentication authentication) {
// authentication.getName() → username, getAuthorities() → daftar role/authority
return new MeResponse(authentication.getName(),
authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList());
}
void audit() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
log.info("aksi oleh {}", auth.getName());
}Parameter Authentication di handler adalah cara yang lebih mudah dites. SecurityContextHolder berguna di tempat yang tidak punya akses ke parameter request, misalnya komponen audit.
Stateless API dan HTTP Basic
Aplikasi web klasik menyimpan login di sesi server (cookie JSESSIONID). Untuk REST API, pola yang umum adalah stateless: setiap request membawa kredensialnya sendiri dan server tidak menyimpan sesi. Keuntungannya, instance aplikasi bisa ditambah tanpa berbagi sesi. SessionCreationPolicy.STATELESS membuat Spring Security tidak membuat sesi HTTP dan tidak memakainya untuk menyimpan SecurityContext.
HTTP Basic mengirim username:password yang di-encode Base64 di header setiap request. Base64 bukan enkripsi — siapa pun yang menyadap bisa men-decode-nya — sehingga Basic hanya layak lewat HTTPS. Di bab ini Basic dipakai karena sederhana untuk belajar alur security. Di production, API biasanya memakai token (JWT/OAuth2) yang dibahas dua bab lagi; konsep filter chain dan SecurityContext tetap sama.
Menguji dengan curl
Jalankan aplikasi dan uji ketiga skenario. Flag -i menampilkan status dan header respons.
bash
# 1. Tanpa kredensial → 401 + header WWW-Authenticate
curl -i http://localhost:8080/api/orders
# 2. Kredensial benar → 200
curl -i -u budi:rahasia-panjang-123 http://localhost:8080/api/orders
# 3. Login benar tapi tidak punya role ADMIN → 403
curl -i -u budi:rahasia-panjang-123 -X DELETE http://localhost:8080/api/admin/products/7
# Endpoint publik → 200 tanpa login
curl -i http://localhost:8080/api/productstext
HTTP/1.1 401
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
...Membiasakan diri menguji ketiga jalur — tanpa login, login tanpa izin, login dengan izin — mencegah bug klasik "endpoint admin ternyata bisa diakses semua user".
Contoh di dunia backend
Toko online punya katalog publik, endpoint pesanan untuk pelanggan yang login, dan endpoint admin untuk mengelola stok. Untuk sementara user disimpan in-memory (bab berikutnya memindahkannya ke database).
java
// Potongan — konfigurasi toko online, Spring Boot 4.1 / Spring Security 7
@Configuration
@EnableWebSecurity
public class ShopSecurityConfig {
@Bean
SecurityFilterChain shopApi(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/orders/**").hasAnyRole("CUSTOMER", "ADMIN")
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
// Hanya untuk belajar: user in-memory. {noop} = password tanpa hashing, JANGAN di production.
@Bean
UserDetailsService users() {
UserDetails budi = User.withUsername("budi")
.password("{noop}rahasia-panjang-123")
.roles("CUSTOMER")
.build();
UserDetails sari = User.withUsername("sari")
.password("{noop}admin-panjang-456")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(budi, sari);
}
}java
// Potongan — controller memakai identitas dari SecurityContext, bukan dari body
@RestController
@RequestMapping("/api/orders")
class OrderController {
private final OrderService orderService;
OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request,
Authentication authentication) {
OrderResponse created = orderService.create(authentication.getName(), request);
return ResponseEntity.created(URI.create("/api/orders/" + created.id())).body(created);
}
}Perhatikan bahwa pemilik pesanan diambil dari authentication.getName(). Bila klien boleh mengirim customerId di body, user mana pun bisa membuat pesanan atas nama orang lain. Menjamin bahwa user hanya bisa membaca pesanannya sendiri (cek kepemilikan data) dibahas di bab otorisasi.
Pertanyaan interview
1. Apa beda authentication dan authorization, dan bagaimana masing-masing tercermin di status HTTP?
Authentication memverifikasi identitas (siapa pemanggilnya), authorization memutuskan izin (bolehkah dia melakukan aksi ini). Gagal autentikasi menghasilkan 401 Unauthorized, biasanya dengan header WWW-Authenticate. Gagal otorisasi setelah identitas diketahui menghasilkan 403 Forbidden. Authentication selalu terjadi lebih dulu.
2. Bagaimana Spring Security mencegat request sebelum sampai ke controller?
Spring Security dipasang sebagai servlet filter. DelegatingFilterProxy meneruskan request ke FilterChainProxy, yang memilih SecurityFilterChain pertama yang cocok lalu menjalankan filternya berurutan: menyiapkan SecurityContext, mengautentikasi (mis. BasicAuthenticationFilter), lalu AuthorizationFilter menerapkan aturan. ExceptionTranslationFilter mengubah kegagalan menjadi 401 atau 403. Baru setelah lolos semua, request sampai ke DispatcherServlet.
3. Kenapa API stateless sering mematikan CSRF, dan kapan itu berbahaya?
Serangan CSRF memanfaatkan browser yang otomatis mengirim cookie sesi ke situs target. Bila API tidak memakai cookie sama sekali dan kredensial dikirim eksplisit di header (Authorization), browser tidak bisa "menitipkan" kredensial secara otomatis, sehingga CSRF tidak relevan. Mematikan CSRF menjadi berbahaya bila autentikasi tetap memakai cookie — mis. sesi atau token yang disimpan di cookie — karena serangan itu kembali mungkin.
Jebakan umum
- Menaruh
anyRequest()di atas aturan spesifik. Aturan dievaluasi berurutan; Spring Security menolak start bila ada aturan setelahanyRequest(). Aturan spesifik selalu di atas. - Mematikan CSRF tanpa memahami alasannya. Aman hanya untuk API stateless tanpa cookie autentikasi.
- Menulis
hasRole("ROLE_ADMIN").hasRolemenambahkan prefixROLE_sendiri; pakaihasRole("ADMIN")atauhasAuthority("ROLE_ADMIN"). - Mengira Base64 di HTTP Basic itu enkripsi. Itu hanya encoding; tanpa HTTPS password terbaca di jaringan.
- Menyalin konfigurasi lama dengan
WebSecurityConfigurerAdapteratau.and(). Keduanya sudah dihapus di Spring Security 7; pakai beanSecurityFilterChaindengan lambda DSL. - Mengambil identitas dari body atau query parameter. Selalu ambil dari
Authentication/SecurityContext.
Ringkasan
- Authentication menjawab "siapa kamu" (
401bila gagal); authorization menjawab "boleh apa" (403bila ditolak). - Starter security langsung mengunci semua endpoint dengan user
userdan password acak — hanya untuk development. - Konfigurasi ditulis sebagai bean
SecurityFilterChaindengan lambda DSL; aturan spesifik di atas,anyRequest()paling bawah. - Setiap request melewati filter chain: autentikasi dulu, lalu otorisasi;
ExceptionTranslationFiltermemetakan kegagalan ke401/403. - Identitas yang lolos autentikasi disimpan di
SecurityContextdan bisa disuntikkan sebagaiAuthentication. - API stateless tidak memakai sesi; HTTP Basic cukup untuk belajar, wajib HTTPS, dan diganti token di production.