Spring Security & Testing
2/20
Bài 2 / 20~12 phútSecurity Architecture & Filter ChainLộ trình · chặng 18/21Miễn phí… lượt xem

Kiến Trúc Filter Chain: Cơ Chế Bảo Vệ Trước Servlet

Tìm hiểu kiến trúc Servlet Filter, vai trò DelegatingFilterProxy, FilterChainProxy và cách cấu hình SecurityFilterChain chuẩn Spring Security 6.

TL;DR: Spring Security triển khai bảo mật dưới dạng một chuỗi Servlet Filter chèn trước DispatcherServlet — request bị từ chối ngay ở tầng web container mà không bao giờ chạm tới controller. DelegatingFilterProxy giải quyết bài toán lệch pha khởi động giữa Tomcat và Spring IoC bằng cơ chế lazy-lookup. Bên trong Spring, FilterChainProxy quản lý các bean SecurityFilterChain chứa khoảng 15 filter chuyên biệt cho từng nhiệm vụ: context, CORS, CSRF, authentication và authorization. Spring Security 6 loại bỏ WebSecurityConfigurerAdapter và bắt buộc dùng lambda DSL.

Tại sao bảo mật lại dựa trên Servlet Filter thay vì Controller?

Nguyên tắc cốt lõi: bảo mật không phải là nghiệp vụ kinh doanh (business logic), mà là điều kiện kiểm soát cổng vào. Mọi truy vấn không hợp lệ cần bị từ chối càng sớm càng tốt để bảo vệ tài nguyên hệ thống.

Nếu triển khai bảo mật qua HandlerInterceptor của Spring MVC, request buộc phải đi qua các bước: so khớp routing, deserialize JSON body và khởi tạo handler method rồi mới bị chặn. Ngược lại, với Servlet Filter, request bị chặn ngay tại tầng Servlet container.

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ả là khi một filter quyết định từ chối (401 Unauthorized hoặc 403 Forbidden), DispatcherServlet và các Controller không bao giờ được gọi.

Servlet Filter hoạt động theo cơ chế chuỗi tuyến tính ra sao?

Tiêu chuẩn Servlet (jakarta.servlet) định nghĩa giao diện Filter với phương thức duy nhất:

Java
public interface Filter {
    void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException;
}

Các filter vận hành theo chuỗi mắt xích. Mỗi filter xử lý tiền kỳ, gọi chain.doFilter() để chuyển giao request cho filter tiếp theo, rồi đón nhận kết quả ở hậu kỳ:

Java
public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        long start = System.currentTimeMillis();
        // Forward request downstream
        chain.doFilter(req, res);
        // Process response on return path
        log.info("Request duration: {}ms", System.currentTimeMillis() - start);
    }
}

Nếu một filter không gọi chain.doFilter(), chuỗi xử lý lập tức ngắt quãng tại điểm đó.

Ba tầng kiến trúc: DelegatingFilterProxy, FilterChainProxy và SecurityFilterChain là gì?

Thay vì đăng ký hàng chục filter riêng lẻ vào container Tomcat, Spring Security trừu tượng hóa hệ thống thành 3 tầng rõ rệt:

TầngLớp phụ tráchVai trò chính
Servlet ContainerDelegatingFilterProxyCầu nối từ Tomcat sang Spring IoC Container
Spring InfrastructureFilterChainProxySo khớp URL và chọn SecurityFilterChain phù hợp
Application ConfigSecurityFilterChainDanh sách các filter cụ thể cho từng URL pattern

Bài toán khởi động và DelegatingFilterProxy

Tomcat khởi tạo các Servlet Filter trong giai đoạn ServletContextListener, diễn ra trước khi Spring ApplicationContext được dựng hoàn tất. Nếu đăng ký một Spring bean trực tiếp với Tomcat, container sẽ thất bại vì bean chưa tồn tại.

DelegatingFilterProxy giải quyết vấn đề này bằng kỹ thuật nạp lười (lazy-lookup): tại thời điểm khởi động, nó chỉ ghi nhớ tên bean mục tiêu (springSecurityFilterChain). Đến khi request đầu tiên thực sự truyền tới, Spring context đã sẵn sàng, proxy mới lấy bean từ context và ủy quyền xử lý.

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 điều phối các chain

FilterChainProxy đóng vai trò cổng vào duy nhất. Nó duyệt qua các cấu hình SecurityFilterChain theo thứ tự ưu tiên @Order để tìm ra chain đầu tiên khớp với URL request.

Danh sách 15+ filter chuyên dụng thực hiện nhiệm vụ gì?

Bên trong mỗi SecurityFilterChain là một chuỗi các filter được sắp xếp nghiêm ngặt theo thứ tự trách nhiệm đơn nhất (Single Responsibility Principle):

Thứ tự tiêu biểuFilterTrách nhiệm chính
1SecurityContextHolderFilterNạp SecurityContext từ session/kho lưu trữ vào ThreadLocal
2CorsFilterXử lý yêu cầu CORS preflight và gắn HTTP headers
3CsrfFilterXác thực CSRF token cho các request thay đổi trạng thái
4UsernamePasswordAuthenticationFilterXử lý đăng nhập form login truyền thống
5BearerTokenAuthenticationFilterBóc tách và xác thực JWT Bearer token
6AnonymousAuthenticationFilterGán danh tính khách vô danh nếu chưa có xác thực
7ExceptionTranslationFilterĐón bắt ngoại lệ bảo mật và ánh xạ thành mã HTTP 401/403
8AuthorizationFilterKiểm tra quyền truy cập (GrantedAuthority) trên tài nguyên

Thứ tự sắp xếp mang tính quyết định: SecurityContextHolderFilter buộc phải chạy đầu tiên để thiết lập ngữ cảnh cho toàn bộ thread, trong khi ExceptionTranslationFilter phải bọc trước AuthorizationFilter để bắt AccessDeniedException.

Spring Security 6 cấu hình SecurityFilterChain qua Lambda DSL như thế nào?

Spring Security 6 (tích hợp từ Spring Boot 3+) xóa bỏ hoàn toàn lớp kế thừa WebSecurityConfigurerAdapter. Toàn bộ thiết lập chuyển sang bean @Bean SecurityFilterChain với cú pháp lambda DSL:

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Disable for stateless REST APIs
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**", "/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 ->
                oauth2.jwt(Customizer.withDefaults())
            );

        return http.build();
    }
}

Sử dụng lambda DSL giúp mỗi phương thức cấu hình trả về chính thể hiện HttpSecurity, loại bỏ hoàn toàn các lời gọi .and() cồng kềnh của phiên bản cũ.

VirtualFilterChain điều phối request qua từng filter ra sao?

Khi request đi qua FilterChainProxy, nó không duyệt trực tiếp chuỗi của Tomcat mà được gói trong một VirtualFilterChain. Đối tượng nội bộ này giữ con trỏ chỉ mục và lần lượt kích hoạt từng filter đã đăng ký.

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ó

Nhờ kiến trúc này, Tomcat ở tầng Servlet chỉ nhận diện đúng một filter duy nhất, trong khi Spring toàn quyền sắp đặt và bổ sung các logic bảo mật phức tạp.

Cơ chế tự động cấu hình (Autoconfiguration) kích hoạt những gì?

Khi bạn khai báo dependency spring-boot-starter-security:

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Boot tự động kích hoạt:

  1. SecurityAutoConfiguration: Đăng ký FilterChainProxy và chuỗi mặc định yêu cầu đăng nhập toàn bộ endpoint.
  2. UserDetailsServiceAutoConfiguration: Tạo sẵn tài khoản in-memory user với mật khẩu ngẫu nhiên hiển thị tại log console.
  3. Khi bạn định nghĩa một @Bean SecurityFilterChain của riêng mình, cấu hình mặc định sẽ tự động nhường chỗ.

Những sai lầm phổ biến khi thiết lập Filter Chain là gì?

  • Kế thừa WebSecurityConfigurerAdapter: Lớp này đã bị loại bỏ hoàn toàn trong Spring Security 6. Cần chuyển sang khai báo @Bean SecurityFilterChain.
  • Sử dụng antMatchers(): Phương thức cũ đã bị thay thế bởi requestMatchers(), có khả năng nhận biết ngữ cảnh DispatcherServlet thông minh hơn.
  • Dùng sai HandlerInterceptor cho Auth: Interceptor chạy sau khi đã tiêu tốn tài nguyên parse HTTP request body, không thể thay thế Servlet Filter cho các tác vụ gating bảo mật.

Những điểm mấu chốt cần ghi nhớ về Filter Chain là gì?

  • Gating ở tầng Servlet: Ngăn chặn truy cập bất hợp pháp ngay trước khi DispatcherServlet khởi chạy.
  • Cơ chế 3 tầng: DelegatingFilterProxy kết nối Tomcat với Spring; FilterChainProxy định tuyến; SecurityFilterChain lưu danh sách filter.
  • Quy tắc thứ tự nghiêm ngặt: Khởi tạo context đầu chuỗi, xác thực danh tính ở giữa, và kiểm tra phân quyền ở cuối.
  • Lambda DSL hiện đại: Định hình cấu hình rõ ràng, module hóa cao và tương thích chuẩn Spring Boot 3+.

Bạn đã hiểu rõ kiến trúc Filter Chain của Spring Security chưa?

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao Spring Security lại được thiết kế dựa trên Servlet Filter thay vì HandlerInterceptor của Spring MVC?
  2. Q2
    Vai trò then chốt của DelegatingFilterProxy trong kiến trúc Spring Security là gì?
  3. Q3
    Trong chuỗi SecurityFilterChain mặc định, filter nào bắt buộc phải thực thi đầu tiên?
  4. Q4
    Cú pháp nào sau đây là chuẩn mực khi cấu hình phân quyền trong Spring Security 6 (Spring Boot 3+)?
  5. Q5
    Nếu một ứng dụng có 2 bean SecurityFilterChain được đánh số @Order(1) và @Order(2), quy trình xử lý của FilterChainProxy diễn ra như thế nào?

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?

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.

Lộ trình Junior Java Backend Developer

Chặng 18/21 · Authentication & Authorization — bài 2/19 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Luồng Xác Thực và SecurityContext Trong ThreadLocal