Đa Chuỗi SecurityFilterChain và Cấu Hình Stateless JWT
Cấu hình nhiều SecurityFilterChain song song bằng @Order và securityMatcher, thiết lập cơ chế stateless JWT và tối ưu tài nguyên tĩnh với ignoring.
TL;DR: FilterChainProxy quản lý một danh sách các SecurityFilterChain được sắp xếp theo độ ưu tiên @Order. Khi request đến, hệ thống duyệt từ trên xuống và dừng lại ở chuỗi đầu tiên có securityMatcher khớp với URL. Nhờ cơ chế này, một ứng dụng duy nhất có thể áp dụng các chính sách bảo mật khác nhau: /api/** dùng JWT không trạng thái (SessionCreationPolicy.STATELESS kèm tắt CSRF), /admin/** áp dụng kiểm tra quyền nghiêm ngặt, trong khi giao diện web dùng Session Cookie truyền thống. Để tối ưu hóa hiệu năng, WebSecurityCustomizer.ignoring() cho phép tài nguyên tĩnh bỏ qua hoàn toàn chuỗi filter.
Tại sao một ứng dụng thực tế lại cần nhiều SecurityFilterChain?
Một hệ thống doanh nghiệp tiêu chuẩn thường phải đồng thời phục vụ nhiều nhóm đối tượng với yêu cầu bảo mật hoàn toàn khác nhau:
| Phân vùng URL | Chính sách áp dụng | Phương thức nhận diện |
|---|---|---|
/api/** | Stateless REST, tắt CSRF | JWT Bearer token |
/admin/** | Stateless, yêu cầu quyền ROLE_ADMIN | JWT Bearer + Audit log |
/static/** | Bỏ qua toàn bộ bảo mật | Không kiểm tra |
| Trang web quản trị | Có trạng thái (Stateful), bật CSRF | Cookie Form login |
Nếu cố nhồi nhét tất cả vào một bean SecurityFilterChain duy nhất, mã nguồn sẽ trở nên phân mảnh với hàng loạt điều kiện rẽ nhánh và xung đột chính sách vòng đời session. Tách thành nhiều bean SecurityFilterChain độc lập giúp cô lập phạm vi trách nhiệm.
FilterChainProxy điều phối và lựa chọn chuỗi bảo mật ra sao?
FilterChainProxy duyệt qua danh sách các chain được sắp xếp theo thứ tự @Order tăng dần:

Nguyên tắc vàng: Mỗi HTTP request chỉ được thực thi bởi đúng MỘT SecurityFilterChain duy nhất. Chain nào khớp trước sẽ nhận việc, các chain còn lại bị bỏ qua hoàn toàn.
- securityMatcher (Cấp chuỗi filter): Đóng vai trò cánh cổng của chain. Quyết định request có được đưa vào chuỗi này hay không.
- requestMatchers (Cấp phân quyền): Nằm bên trong
authorizeHttpRequests, chỉ chạy khi chain đã được chọn để quyết định cho qua hay chặn quyền.
Cấu hình đa chuỗi với @Order và securityMatcher như thế nào?
Mẫu cấu hình chuẩn với ba chuỗi độc lập:
@Configuration
@EnableWebSecurity
public class MultiChainSecurityConfig {
// Chain 1: Admin API - Highest priority
@Bean
@Order(1)
public SecurityFilterChain adminChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/admin/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.anyRequest().hasRole("ADMIN")
)
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
// Chain 2: Public & Business API
@Bean
@Order(2)
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
// Chain 3: Catch-all for Web Form Login
@Bean
@Order(3)
public SecurityFilterChain defaultChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/error").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form.loginPage("/login").permitAll());
return http.build();
}
}
Cơ chế Stateless và việc tắt CSRF cho JWT API có ý nghĩa gì?
1. Bản chất của SessionCreationPolicy.STATELESS
Khi ứng dụng dùng form login truyền thống, SecurityContext được lưu trong HttpSession. Để nhận diện người dùng, máy chủ phụ thuộc vào cookie JSESSIONID và phải lưu trạng thái trong RAM của từng Pod (Stateful).

Ngược lại, với kiến trúc REST API JWT:
- Mỗi request tự mang theo đầy đủ bằng chứng nhận diện trong header
Authorization: Bearer <jwt>. SessionCreationPolicy.STATELESSra lệnh cho Spring Security không tạo mới và không tìm kiếmHttpSession.- Sau khi gửi xong phản hồi, đối tượng context trong
ThreadLocalđược giải phóng ngay lập tức. Đây là nền tảng để ứng dụng mở rộng ngang (horizontal scale) không giới hạn trên Kubernetes.
2. Tại sao tắt CSRF lại an toàn cho Stateless API?
Lỗ hổng CSRF chỉ phát sinh khi trình duyệt web tự động đính kèm cookie xác thực khi người dùng bị lừa truy cập trang web độc hại.
Với REST API stateless, mã định danh JWT được lưu ở client (memory/storage) và chỉ được đính kèm vào header khi mã JavaScript chủ động gửi đi. Trình duyệt không tự động gửi header tùy ý cross-origin, vì vậy bề mặt tấn công CSRF hoàn toàn bị triệt tiêu:
http.csrf(csrf -> csrf.disable());
Khi nào nên dùng WebSecurityCustomizer để bỏ qua filter?
Các tài nguyên tĩnh (HTML, CSS, ảnh đại diện, font chữ) hoàn toàn công khai và không chứa dữ liệu nhạy cảm. Nếu dùng permitAll() trong SecurityFilterChain, request vẫn phải đi qua toàn bộ 15+ filter, tiêu tốn CPU và sinh log dư thừa.
Sử dụng WebSecurityCustomizer.ignoring() giúp request rẽ nhánh ngay từ cổng vào, không chạm tới FilterChainProxy:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/static/**", "/css/**", "/js/**", "/images/**", "/favicon.ico");
}

Không bao giờ áp dụng ignoring() cho các endpoint nhạy cảm như /actuator/health hay các API công khai. Các endpoint này vẫn cần chạy qua chuỗi filter với permitAll() để được bảo vệ bởi các HTTP Security Header (như CSP, HSTS) và hỗ trợ ghi nhận log truy cập.
Những cạm bẫy phổ biến khi cấu hình đa chuỗi là gì?
- Quên securityMatcher ở chain ưu tiên cao: Nếu
@Order(1)không cósecurityMatcher(), nó sẽ coi là khớp với mọi URL và nuốt trọn mọi request, khiến các chain phía sau không bao giờ được kích hoạt. - Trùng lặp giá trị @Order: Khai báo hai bean
SecurityFilterChaincó cùng chỉ số@Ordersẽ khiến Spring ném ngoại lệIllegalStateExceptionkhi khởi động. - Bật CSRF cùng với STATELESS: Nếu session ở chế độ
STATELESSmà không tắt CSRF, token CSRF không có chỗ lưu trữ trong session và sẽ khiến mọi request thay đổi trạng thái (POST/PUT/DELETE) bị từ chối với mã 403.
Những điểm mấu chốt cần ghi nhớ là gì?
- Sử dụng
@Orderkết hợpsecurityMatcherđể tách biệt cấu hình bảo mật cho từng phân vùng URL. - Mỗi request chỉ chạy qua một chain duy nhất: chain đầu tiên khớp sẽ xử lý toàn bộ vòng đời.
- REST API stateless với JWT bắt buộc phải kết hợp
SessionCreationPolicy.STATELESSvàcsrf.disable(). - Tách riêng tài nguyên tĩnh ra khỏi chuỗi filter bằng
WebSecurityCustomizer.ignoring()để tối ưu hóa tài nguyên.
Bạn đã làm chủ cơ chế đa chuỗi và Stateless JWT chưa?
- Q1Điều gì sẽ xảy ra nếu bean SecurityFilterChain có @Order(1) quên không khai báo phương thức securityMatcher()?
- Q2Tại sao thiết lập SessionCreationPolicy.STATELESS lại hỗ trợ mở rộng ngang (horizontal scale) cho hệ thống backend?
- Q3Sự khác biệt cốt lõi giữa permitAll() trong authorizeHttpRequests và WebSecurityCustomizer.ignoring() là gì?
- Q4Tại sao việc tắt CSRF (csrf.disable()) lại được xem là an toàn đối với các REST API sử dụng JWT Bearer token?
- Q5Trong cấu hình đa chuỗi, chain cuối cùng đóng vai trò catch-all nên có đặc điểm gì?
Bài tiếp theo: Tổng kết module
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Cấu Hình SecurityFilterChain Bằng Lambda DSL Hiện Đại
Bài tiếpTổng Kết Module: Security Architecture & Filter Chain
Lộ trình Junior Java Backend Developer
Chặng 18/21 · Authentication & Authorization — bài 5/19 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này