Filter chain architecture — Spring Security đứng trước DispatcherServlet ra sao
Spring Security không hook vào Spring MVC — nó là một chuỗi Servlet Filter chạy trước DispatcherServlet. Bài này giải thích tại sao thiết kế đó đúng, FilterChainProxy là gateway duy nhất, 15+ filter mỗi cái một trách nhiệm, và Security 6 lambda DSL thay thế WebSecurityConfigurerAdapter như thế nào.
TL;DR: Spring Security triển khai bảo mật như một chuỗi Servlet Filter chèn trước DispatcherServlet — request bị reject ngay ở tầng Servlet, controller không bao giờ nhận được. DelegatingFilterProxy giải quyết vấn đề thứ tự khởi động: Tomcat tạo filter trước khi Spring context sẵn sàng, nên proxy lazy-fetch FilterChainProxy bean vào lần request đầu. Bên trong, SecurityFilterChain là danh sách ~15 filter chuyên dụng — mỗi filter đúng 1 nhiệm vụ: load context, CORS, CSRF, xác thực JWT/form, phân quyền. Spring Security 6 (Boot 3+) bỏ WebSecurityConfigurerAdapter, thay bằng SecurityFilterChain bean với lambda DSL bắt buộc. Hiểu kiến trúc này là nền để đọc Authentication flow và SecurityFilterChain DSL.
Bạn vừa thêm spring-boot-starter-security vào project — mọi endpoint lập tức trả 401. Điều gì ngăn request trước khi controller nhận được? Câu trả lời là kiến trúc filter chain. Bài này bóc đúng một thứ: Spring Security nằm ở đâu trong request lifecycle, và tại sao nó được thiết kế như vậy.
1. Tại sao filter-based — security là lớp ngoài business
Nguyên tắc thiết kế cốt lõi: security không phải business logic — nó là gating condition. Reject sớm, reject trước khi tốn tài nguyên.
Nếu Spring Security hook vào DispatcherServlet (như một HandlerInterceptor), request đã đi qua routing, deserialize body, khởi tạo controller bean — rồi mới bị chặn. Với Servlet Filter, request bị chặn ở tầng thấp nhất: trước khi Spring MVC thậm chí được gọi.
flowchart LR
Req["HTTP Request"]
Tomcat["Tomcat<br/>(Servlet Container)"]
SS["Spring Security<br/>Filter Chain<br/>(15+ filters)"]
DS["DispatcherServlet<br/>(Spring MVC)"]
Ctrl["@RestController"]
Rej["401 / 403<br/>Response"]
Req --> Tomcat --> SS
SS -->|"reject"| Rej
SS -->|"pass"| DS --> CtrlHệ quả: khi filter reject, controller không bao giờ được gọi — security trở thành lớp độc lập bên ngoài business code. Đây là lý do Spring Security được implement bằng Servlet Filter API thay vì Spring MVC Interceptor.
2. Servlet Filter — nền tảng
Servlet API (chuẩn jakarta.servlet) định nghĩa Filter interface. Mỗi filter có thể inspect/modify request + response, và quyết định có chuyển tiếp xuống chuỗi hay không:
public interface Filter {
void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException;
}
Filter chạy theo chuỗi tuyến tính. Mỗi filter gọi chain.doFilter() để chuyển request sang filter kế tiếp; nếu không gọi, request dừng tại đó:
public class LoggingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// Before: intercept incoming request
long start = System.currentTimeMillis();
chain.doFilter(req, res); // pass to next filter
// After: request returned from downstream
log.info("Took {}ms", System.currentTimeMillis() - start);
}
}
Spring Security xây trên cơ sở này — cung cấp sẵn một chuỗi ~15 filter chuyên dụng cho authentication và authorization.
3. Ba tầng: DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain
Thay vì đăng ký 15 filter riêng lẻ vào Tomcat, Spring Security dùng kiến trúc 3 tầng:
flowchart TB
Tomcat["Tomcat<br/>(Servlet Container)"]
DFP["DelegatingFilterProxy<br/>registered at Tomcat level<br/>lazy-fetch Spring bean"]
FCP["FilterChainProxy<br/>Spring bean -- match URL<br/>chon SecurityFilterChain"]
SFC1["SecurityFilterChain #1<br/>/api/admin/** -- admin rules"]
SFC2["SecurityFilterChain #2<br/>/** -- default rules"]
Filters["CorsFilter -> CsrfFilter<br/>BearerTokenAuthFilter<br/>AuthorizationFilter<br/>..."]
Tomcat --> DFP --> FCP
FCP --> SFC1
FCP --> SFC2
SFC2 --> Filters| Tầng | Class | Trách nhiệm |
|---|---|---|
| Servlet container | DelegatingFilterProxy | Bridge từ Tomcat sang Spring bean |
| Spring infrastructure | FilterChainProxy | Match URL → chọn SecurityFilterChain |
| Application config | SecurityFilterChain | Danh sách filter cho URL pattern |
Tại sao cần DelegatingFilterProxy
Tomcat khởi tạo servlet filter trong quá trình ServletContext initialization — xảy ra trước khi Spring ApplicationContext được tạo. Nếu đăng ký FilterChainProxy (một Spring bean) trực tiếp như Tomcat filter, Tomcat sẽ cố lấy bean khi context chưa sẵn sàng — lỗi.
DelegatingFilterProxy giải quyết bằng cách lazy-fetch: khi Tomcat hỏi về filter trong init(), proxy chỉ lưu tên bean. Khi request đầu tiên đến, Spring context đã sẵn sàng, proxy mới tìm bean springSecurityFilterChain qua WebApplicationContext và uỷ quyền từ đó về sau.
FilterChainProxy là gateway duy nhất
FilterChainProxy nhận request, match URL, và chọn SecurityFilterChain phù hợp. Một app có thể có nhiều SecurityFilterChain bean với URL pattern khác nhau — ví dụ chain riêng cho admin endpoints với rule chặt hơn. FilterChainProxy dùng thứ tự @Order để ưu tiên.
4. Cơ chế bên dưới — 15+ filter, mỗi filter một nhiệm vụ
SecurityFilterChain bên trong là một danh sách filter chạy theo thứ tự cố định. Spring Security 6 default chain (đơn giản hoá):
flowchart TB
F1["DisableEncodeUrlFilter"]
F2["WebAsyncManagerIntegrationFilter"]
F3["SecurityContextHolderFilter"]
F4["HeaderWriterFilter"]
F5["CorsFilter"]
F6["CsrfFilter"]
F7["LogoutFilter"]
F8["UsernamePasswordAuthenticationFilter"]
F9["BearerTokenAuthenticationFilter"]
F10["BasicAuthenticationFilter"]
F11["RequestCacheAwareFilter"]
F12["AnonymousAuthenticationFilter"]
F13["ExceptionTranslationFilter"]
F14["AuthorizationFilter"]
F1 --> F2 --> F3 --> F4 --> F5 --> F6 --> F7 --> F8 --> F9 --> F10 --> F11 --> F12 --> F13 --> F14Các filter quan trọng nhất bạn sẽ chạm:
| Filter | Vị trí | Nhiệm vụ |
|---|---|---|
SecurityContextHolderFilter | Đầu chain | Load SecurityContext từ session/repo vào ThreadLocal per-thread |
CorsFilter | Trước auth | Xử lý CORS preflight + đính headers |
CsrfFilter | Trước auth | Validate CSRF token (form-based request) |
UsernamePasswordAuthenticationFilter | Auth zone | Xử lý form login (POST /login) |
BearerTokenAuthenticationFilter | Auth zone | Validate JWT từ Authorization: Bearer ... |
AnonymousAuthenticationFilter | Sau auth filters | Nếu chưa có auth, set AnonymousAuthenticationToken |
ExceptionTranslationFilter | Cuối trước authz | Catch exception — map sang 401/403 response |
AuthorizationFilter | Cuối chain | Check GrantedAuthority match URL rule |
Thứ tự có ý nghĩa: SecurityContextHolderFilter phải chạy đầu tiên để tạo SecurityContext cho thread hiện tại; mọi filter sau mới có context để đọc. ExceptionTranslationFilter chạy trước AuthorizationFilter để bắt AccessDeniedException từ filter ngay sau.
Mỗi filter một trách nhiệm — tại sao không gộp
Tách filter theo trách nhiệm (Single Responsibility) cho phép bật/tắt từng tính năng độc lập. Ví dụ: REST API stateless không cần CSRF protection — tắt CsrfFilter trong DSL mà không ảnh hưởng filter khác. CORS config tách biệt với auth logic. Không cần viết lại toàn bộ pipeline khi thay đổi một tính năng.
5. SecurityFilterChain DSL — Spring Security 6
Spring Security 6 (Spring Boot 3+) bỏ hoàn toàn WebSecurityConfigurerAdapter. Config được khai báo qua SecurityFilterChain bean với lambda DSL bắt buộc:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // REST API -- no CSRF
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.cors(cors -> cors.configurationSource(corsSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(Customizer.withDefaults()) // validate Bearer token
);
return http.build();
}
}
Các thay đổi cốt lõi so với Spring Security 5:
| Aspect | Spring Security 5 | Spring Security 6 |
|---|---|---|
| Configuration | Extend WebSecurityConfigurerAdapter | @Bean SecurityFilterChain |
| DSL style | Method chain với .and() | Lambda DSL bắt buộc |
| URL matchers | antMatchers() | requestMatchers() (smarter) |
| Authorization | authorizeRequests() | authorizeHttpRequests() |
| Java baseline | 8 | 17 |
Lambda DSL không chỉ là thay đổi cú pháp — mỗi http.csrf(...) trả về chính HttpSecurity (không phải sub-configurer), nên không cần .and() để quay lại context. Code gọn hơn và dễ hơn khi extract một đoạn config vào method riêng.
Cơ chế bên dưới — request chạy qua chain như thế nào
Nhìn vào FilterChainProxy.doFilter() (source Spring Security), luồng thực tế khi request đến:
sequenceDiagram
participant Req as HTTP Request
participant DFP as DelegatingFilterProxy
participant FCP as FilterChainProxy
participant SFC as SecurityFilterChain
participant F as Filter[i]
Req->>DFP: doFilter()
DFP->>FCP: doFilter() (lazy bean lookup first time)
FCP->>FCP: getFilters(request) -- match URL -> find chain
FCP->>SFC: getFilters()
loop each filter in chain
FCP->>F: doFilter(req, res, virtualChain)
F->>F: execute security logic
F->>FCP: chain.doFilter() -- proceed to next
end
FCP-->>Req: responseFilterChainProxy tạo VirtualFilterChain — wrapper FilterChain nội bộ iterate qua danh sách filter của SecurityFilterChain. Từng filter nhận chain.doFilter() trỏ tới VirtualFilterChain, không phải Tomcat chain gốc. Đây là lý do Spring Security hoàn toàn kiểm soát thứ tự và thành phần filter — Tomcat chỉ thấy 1 filter duy nhất.
Boot autoconfig — khởi động tự động
Chỉ cần thêm dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Boot autoconfig kích hoạt 3 thứ:
SecurityAutoConfiguration— đăng kýFilterChainProxy, tạo defaultSecurityFilterChainprotect tất cả endpoint.UserDetailsServiceAutoConfiguration— tạo in-memoryUserDetailsServicevới useruser+ random password log ra console.WebSecurityEnablerConfiguration— apply@EnableWebSecurityimplicit.
Default behavior: tất cả endpoint yêu cầu auth, form login tại /login, HTTP Basic auth bật, CSRF bật. Khi bạn khai báo SecurityFilterChain bean của riêng mình, UserDetailsServiceAutoConfiguration và default chain lùi lại — config của bạn thay thế.
Pitfall phổ biến
❌ Nhầm 1 — Extend WebSecurityConfigurerAdapter trong Spring Security 6:
// SAI -- class bi remove, compile error Spring Security 6
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception { ... }
}
// DUNG -- SecurityFilterChain bean
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// ...
return http.build();
}
}
❌ Nhầm 2 — Dùng antMatchers() trong Spring Security 6:
// SAI -- method bi remove
http.authorizeHttpRequests(auth -> auth
.antMatchers("/admin/**").hasRole("ADMIN")
);
// DUNG
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
);
requestMatchers() thông minh hơn: tự detect AntPathMatcher vs MvcRequestMatcher dựa vào DispatcherServlet có trong context không.
❌ Nhầm 3 — Tưởng HandlerInterceptor và Security Filter tương đương:
HandlerInterceptor chạy trong DispatcherServlet — sau khi Spring MVC đã routing. Security filter chạy trước DispatcherServlet. Nếu implement access control bằng interceptor, request đã deserialize rồi mới bị chặn — tốn tài nguyên không cần thiết. Dùng SecurityFilterChain cho authorization.
Liên hệ các bài khác
- Bài 02 — Authentication flow: khi filter xác thực chạy,
AuthenticationManager→AuthenticationProvider→SecurityContextHolderhoạt động ra sao — đây là phần bên trong "auth zone" của chain. - Bài 03 — SecurityFilterChain DSL: đào sâu lambda DSL —
authorizeHttpRequests,oauth2ResourceServer,sessionManagement,exceptionHandling— và cách multipleSecurityFilterChainchia URL pattern. - Tổng quan module: bản đồ toàn module kiến trúc filter chain —
Authenticationobject,SecurityContextHolderThreadLocal,AuthorizationFilter, exception mapping 401/403.
Tóm tắt
- Spring Security là chuỗi Servlet Filter chạy trước
DispatcherServlet— security là lớp ngoài business, reject trước khi controller nhận. - 3 tầng:
DelegatingFilterProxy(bridge Tomcat-Spring) →FilterChainProxy(match URL, chọn chain) →SecurityFilterChain(danh sách filter cho app). DelegatingFilterProxytồn tại vì Tomcat khởi tạo filter trước Spring context — lazy-fetch bean lần request đầu.- ~15 filter, mỗi filter 1 nhiệm vụ:
SecurityContextHolderFilterload context,CorsFilter/CsrfFilterbảo vệ web, auth filters xác thực,AuthorizationFilterphân quyền,ExceptionTranslationFiltermap exception sang 401/403. - Spring Security 6:
WebSecurityConfigurerAdapterbị xoá — dùngSecurityFilterChainbean + lambda DSL +requestMatchers(). - Boot autoconfig kích hoạt khi có
spring-boot-starter-security— override bằngSecurityFilterChainbean của riêng.
Tự kiểm tra
Q1Tại sao Spring Security implement bảo mật bằng Servlet Filter thay vì Spring MVC HandlerInterceptor? Đâu là hệ quả nếu chọn ngược lại?▸
HandlerInterceptor? Đâu là hệ quả nếu chọn ngược lại?Servlet Filter chạy trước DispatcherServlet — request bị reject trước khi Spring MVC thực hiện routing, deserialize body, hay khởi tạo controller bean. Đây là điểm quan trọng: bảo mật không phải business logic, nên xử lý sớm nhất có thể, tiết kiệm tài nguyên.
Analogy toà nhà: Servlet Filter là vệ sĩ đứng ở cửa toà nhà — người không có thẻ bị chặn ngay sảnh, chưa tốn điện thang máy, chưa làm phiền ai. HandlerInterceptor là vệ sĩ đứng trước từng cửa phòng — kẻ lạ đã đi thang máy lên tầng, đã đứng giữa hành lang (routing + deserialize xong) rồi mới bị hỏi thẻ. Cùng chặn được, nhưng chi phí và phạm vi bảo vệ khác hẳn: vệ sĩ cửa phòng không bảo vệ được kho, phòng kỹ thuật — những nơi không có cửa phòng (static resource, WebSocket).
Nếu dùng HandlerInterceptor: request đã đi qua routing, DispatcherServlet đã chọn handler, Spring MVC đã chuẩn bị context — rồi mới bị chặn. Tài nguyên đã bị tiêu tốn cho request không hợp lệ. Hơn nữa, interceptor không bảo vệ được tài nguyên nằm ngoài Spring MVC (như static resource server, WebSocket, actuator endpoint dùng servlet riêng).
Servlet Filter chạy độc lập với framework phía sau — Tomcat chỉ biết đến filter, không biết Spring MVC hay không. Security trở thành lớp bên ngoài hoàn toàn tách với business code.
Q2Tại sao cần DelegatingFilterProxy thay vì đăng ký FilterChainProxy trực tiếp vào Tomcat?▸
DelegatingFilterProxy thay vì đăng ký FilterChainProxy trực tiếp vào Tomcat?Tomcat khởi tạo servlet filter trong quá trình ServletContext initialization — xảy ra trước khi Spring ApplicationContext được tạo và refresh xong. Nếu đăng ký FilterChainProxy (một Spring bean) trực tiếp như Tomcat filter, Tomcat cố tìm bean khi context chưa tồn tại — lỗi khởi động.
DelegatingFilterProxy giải quyết bằng cách tách hai pha: trong init() chỉ lưu tên bean (springSecurityFilterChain); đến khi request đầu tiên đến, Spring context đã sẵn sàng, proxy mới tìm bean qua WebApplicationContext và uỷ quyền từ đó về sau.
Đây là pattern "lazy-fetch từ Spring context" — không phải vấn đề đặc thù Spring Security mà là giải pháp cho mọi Spring bean cần tồn tại như Servlet component.
Q3Trong default filter chain, SecurityContextHolderFilter phải chạy đầu tiên và AuthorizationFilter cuối cùng. Tại sao thứ tự này không thể đảo?▸
SecurityContextHolderFilter phải chạy đầu tiên và AuthorizationFilter cuối cùng. Tại sao thứ tự này không thể đảo?SecurityContextHolderFilter load SecurityContext (chứa thông tin user đã xác thực) từ session hoặc repository vào ThreadLocal của thread hiện tại. Tất cả filter sau đó đều phụ thuộc vào SecurityContext này — auth filters ghi vào nó, AuthorizationFilter đọc từ nó để kiểm tra quyền.
Nếu AuthorizationFilter chạy trước SecurityContextHolderFilter: SecurityContextHolder.getContext().getAuthentication() trả về null — mọi request đều bị coi là anonymous và bị reject (hoặc được permit tùy config). Cả pipeline sẽ sai.
Tương tự: authentication filters (BearerTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter) phải chạy trước AuthorizationFilter để kịp đặt Authentication vào context trước khi phân quyền được kiểm tra.
Q4Spring Security 6 bắt buộc lambda DSL. So sánh cú pháp cũ (Spring Security 5 chain với .and()) và cú pháp mới. Tại sao lambda DSL là cải tiến?▸
.and()) và cú pháp mới. Tại sao lambda DSL là cải tiến?Spring Security 5 (chain với .and()):
http.csrf().disable()
.and()
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();Spring Security 6 (lambda DSL):
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s ->
s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated());Lambda DSL cải tiến ở 3 điểm: (1) mỗi http.csrf(...) trả về HttpSecurity (không phải sub-configurer) nên không cần .and() để quay lại context; (2) mỗi khối config độc lập — dễ extract thành method riêng hoặc test từng phần; (3) khi một block lambda gặp lỗi, stack trace rõ hơn vì scope rõ ràng hơn chain dài.
Q5App có 2 SecurityFilterChain bean: một cho /api/admin/** (@Order(1)), một cho /** (@Order(2)). Request tới /api/admin/users chạy qua chain nào? Nếu chain 1 không match, điều gì xảy ra?▸
SecurityFilterChain bean: một cho /api/admin/** (@Order(1)), một cho /** (@Order(2)). Request tới /api/admin/users chạy qua chain nào? Nếu chain 1 không match, điều gì xảy ra?FilterChainProxy iterate qua danh sách SecurityFilterChain theo thứ tự @Order tăng dần, kiểm tra matches(request) trên từng chain. Chain đầu tiên match sẽ được dùng — các chain sau bị bỏ qua.
Request tới /api/admin/users: chain 1 (@Order(1), pattern /api/admin/**) match → toàn bộ 15+ filter của chain 1 xử lý request. Chain 2 không được gọi.
Nếu chain 1 không match (ví dụ request tới /api/users): FilterChainProxy kiểm tra chain 2 (/**) — match → dùng chain 2. Nếu không có chain nào match, request bị để đi qua không qua security filter nào (không bị bảo vệ — đây là pitfall cần chú ý khi khai báo URL pattern thiếu).
Pattern phổ biến: chain chuyên dụng cho admin đặt @Order thấp (ưu tiên cao) với rule chặt hơn; catch-all chain /** đặt cuối. Quan trọng là chain catch-all phải tồn tại để bảo vệ endpoint không được liệt kê rõ ràng.
Bài tiếp theo: Authentication flow & SecurityContext
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên