Spring Security & Testing/Filter chain architecture — Spring Security đứng trước DispatcherServlet ra sao
2/20
Bài 2 / 20~12 phútSecurity Architecture & Filter ChainMiễn phí lượt xem

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 flowSecurityFilterChain 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.

Cùng một request mang token hết hạn đi theo hai đường: bên Servlet Filter, Tomcat gọi doFilter rồi filter đọc token và trả 401 ngay khi chưa match URL, chưa chọn handler, chưa parse body; bên HandlerInterceptor, DispatcherServlet đã match URL, chọn handler và parse body xong thì interceptor mới đọc token và trả 401

Hệ 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:

TầngClassTrách nhiệm
Servlet containerDelegatingFilterProxyBridge từ Tomcat sang Spring bean
Spring infrastructureFilterChainProxyMatch URL → chọn SecurityFilterChain
Application configSecurityFilterChainDanh 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.

Hai pha thời gian đặt cạnh nhau: lúc Tomcat khởi động chưa có Spring context nên DelegatingFilterProxy.init chỉ ghi nhớ tên bean, chưa gọi getBean; tới request đầu tiên thì context đã sẵn sàng nên proxy tra WebApplicationContext tìm bean springSecurityFilterChain, uỷ quyền cho FilterChainProxy và từ đó về sau dùng lại bean vừa tìm được

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á) đi lần lượt qua: DisableEncodeUrlFilterWebAsyncManagerIntegrationFilterSecurityContextHolderFilterHeaderWriterFilterCorsFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterBearerTokenAuthenticationFilterBasicAuthenticationFilterRequestCacheAwareFilterAnonymousAuthenticationFilterExceptionTranslationFilterAuthorizationFilter.

Các filter quan trọng nhất bạn sẽ chạm:

FilterVị tríNhiệm vụ
SecurityContextHolderFilterĐầu chainLoad SecurityContext từ session/repo vào ThreadLocal per-thread
CorsFilterTrước authXử lý CORS preflight + đính headers
CsrfFilterTrước authValidate CSRF token (form-based request)
UsernamePasswordAuthenticationFilterAuth zoneXử lý form login (POST /login)
BearerTokenAuthenticationFilterAuth zoneValidate JWT từ Authorization: Bearer ...
AnonymousAuthenticationFilterSau auth filtersNếu chưa có auth, set AnonymousAuthenticationToken
ExceptionTranslationFilterCuối trước authzCatch exception — map sang 401/403 response
AuthorizationFilterCuối chainCheck 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:

AspectSpring Security 5Spring Security 6
ConfigurationExtend WebSecurityConfigurerAdapter@Bean SecurityFilterChain
DSL styleMethod chain với .and()Lambda DSL bắt buộc
URL matchersantMatchers()requestMatchers() (smarter)
AuthorizationauthorizeRequests()authorizeHttpRequests()
Java baseline817

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: FilterChainProxy 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.

Tomcat chỉ đăng ký một filter là DelegatingFilterProxy; bên trong nó VirtualFilterChain lặp qua khoảng 15 filter theo thứ tự cố định, kèm ba ràng buộc: SecurityContextHolderFilter chạy trước mọi filter có việc với danh tính vì nó dựng SecurityContext trong ThreadLocal, các filter xác thực phải xong trước phân quyền, và ExceptionTranslationFilter bọc ngoài AuthorizationFilter vì nó chỉ bắt được exception của filter chạy sau nó

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 default SecurityFilterChain protect tất cả endpoint.
  • UserDetailsServiceAutoConfiguration — tạo in-memory UserDetailsService với user user + random password log ra console.
  • WebSecurityEnablerConfiguration — apply @EnableWebSecurity implicit.

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, AuthenticationManagerAuthenticationProviderSecurityContextHolder hoạ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 multiple SecurityFilterChain chia URL pattern.
  • Tổng quan module: bản đồ toàn module kiến trúc filter chain — Authentication object, SecurityContextHolder ThreadLocal, 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).
  • DelegatingFilterProxy tồ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ụ: SecurityContextHolderFilter load context, CorsFilter / CsrfFilter bảo vệ web, auth filters xác thực, AuthorizationFilter phân quyền, ExceptionTranslationFilter map exception sang 401/403.
  • Spring Security 6: WebSecurityConfigurerAdapter bị xoá — dùng SecurityFilterChain bean + lambda DSL + requestMatchers().
  • Boot autoconfig kích hoạt khi có spring-boot-starter-security — override bằng SecurityFilterChain bean của riêng.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tạ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?
  2. Q2
    Tại sao cần DelegatingFilterProxy thay vì đăng ký FilterChainProxy trực tiếp vào Tomcat?
  3. Q3
    Trong 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?
  4. Q4
    Spring 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?
  5. Q5
    App 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?

Bài tiếp theo: Authentication flow & SecurityContext

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.

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

Đặt 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

Bài tiếp theo

Authentication flow & SecurityContext — từ request đến principal trong ThreadLocal