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

Cấu Hình SecurityFilterChain Bằng Lambda DSL Hiện Đại

Làm chủ cú pháp Lambda DSL trong Spring Security 6: cấu hình requestMatchers, xử lý CSRF, CORS, session stateless và xử lý lỗi chuẩn REST API.

TL;DR: Spring Security 6 loại bỏ hoàn toàn WebSecurityConfigurerAdapter và chuyển toàn bộ thiết lập sang @Bean SecurityFilterChain sử dụng lambda DSL. Cú pháp requestMatchers() thay thế antMatchers() với khả năng phân tích đường dẫn thông minh hơn. Thứ tự khai báo các quy tắc phân quyền mang tính quyết định: quy tắc chi tiết luôn đặt trước, còn chốt chặn .anyRequest() phải đặt ở cuối cùng. Mỗi khối chức năng (CSRF, CORS, Session Management, OAuth2) được cấu hình độc lập, an toàn kiểu dữ liệu và dễ dàng tái sử dụng.

Tại sao Spring Security 6 loại bỏ WebSecurityConfigurerAdapter?

Trước phiên bản Spring Security 6, các dự án bắt buộc phải kế thừa WebSecurityConfigurerAdapter và ghi đè phương thức configure(HttpSecurity):

Java
// Spring Security 5 pattern (deprecated and removed)
@Configuration
public class LegacySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .and()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

Kiến trúc cũ bộc lộ hai hạn chế lớn:

  1. Thiếu tính lắp ghép (Non-composable): Toàn bộ cấu hình bị bó chặt trong một lớp kế thừa duy nhất. Không thể tách riêng phần cấu hình CORS hay Session thành các component độc lập để kiểm thử hoặc tái sử dụng.
  2. Cú pháp .and() mập mờ ngữ cảnh: Các phương thức con trả về các đối tượng builder khác nhau. Khi gọi .and(), trình biên dịch không thể đảm bảo bạn đã thoát ra đúng cấp độ cấu hình cha, dẫn đến lỗi cú pháp ngầm chỉ phát hiện lúc chạy.

Lambda DSL trong Spring Security 6 giải quyết triệt để vấn đề này: mỗi khối nhận một functional interface Customizer<T> với kiểu dữ liệu tường minh và scope rõ ràng.

Cấu trúc chuẩn của một @Bean SecurityFilterChain gồm những gì?

Khung xương mẫu cho cấu hình Spring Security 6:

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .csrf(csrf -> csrf.disable());

        return http.build(); // Always required to construct the chain
    }
}

Ba nguyên tắc cấu trúc bất biến:

  • Phương thức nhận tham số HttpSecurity do Spring IoC tiêm vào.
  • Mỗi module chức năng là một khối lambda độc lập với gợi ý cú pháp chuẩn xác từ IDE.
  • Lệnh gọi kết thúc bắt buộc phải là http.build().
Lỗi quên return http.build()

Nếu phương thức trả về null hoặc không đăng ký đúng bean SecurityFilterChain, Spring Security sẽ bỏ qua bộ lọc và để toàn bộ request đi thẳng vào ứng dụng mà không được bảo vệ.

Quy tắc sắp xếp requestMatchers và phân quyền hoạt động ra sao?

Phương thức requestMatchers() hỗ trợ đa dạng kiểu so khớp đường dẫn, hỗ trợ chỉ định rõ HTTP method:

Java
http.authorizeHttpRequests(auth -> auth
    // Public paths
    .requestMatchers("/api/public/**", "/actuator/health").permitAll()

    // Method-specific rules
    .requestMatchers(HttpMethod.POST, "/api/projects").hasRole("ADMIN")
    .requestMatchers(HttpMethod.GET, "/api/projects/**").authenticated()

    // Authority-based rules (fine-grained permission without prefix)
    .requestMatchers("/api/reports/**").hasAuthority("reports:read")

    // Catch-all rule MUST be placed at the very end
    .anyRequest().authenticated()
);

Nguyên tắc First match wins: Spring duyệt danh sách quy tắc từ trên xuống dưới. Quy tắc đầu tiên khớp với URL sẽ được áp dụng ngay lập tức, toàn bộ các quy tắc phía sau bị bỏ qua.

Cùng một request GET /api/admin/users từ user đã đăng nhập nhưng không có ROLE_ADMIN, chạy qua hai thứ tự rule: khi anyRequest authenticated đứng trước thì nó khớp ngay dòng 1 nên trả 200 OK và user thường đọc được dữ liệu admin, dòng hasRole ADMIN không bao giờ tới lượt; khi rule cụ thể đứng trước thì dòng 1 khớp và trả 403 vì thiếu ROLE_ADMIN

Nếu đặt .anyRequest().authenticated() lên trên cùng, nó sẽ nuốt chửng mọi request và khiến các thiết lập kiểm tra quyền hasRole("ADMIN") ở bên dưới mất hoàn toàn tác dụng.

Các khối DSL phổ biến (CSRF, CORS, Session, OAuth2) thiết lập như thế nào?

1. CSRF Protection

Khi xây dựng REST API nhận Bearer token qua header, trình duyệt không tự động gửi kèm token khi cross-site, do đó bề mặt tấn công CSRF không tồn tại:

Java
http.csrf(csrf -> csrf.disable());

2. Session Creation Policy

Đối với kiến trúc microservice hoặc REST API stateless, yêu cầu Spring không tạo và không lưu trữ HTTP Session:

Java
http.sessionManagement(session -> session
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);

3. OAuth2 Resource Server (JWT)

Tự động kích hoạt bộ lọc xác thực Bearer token và ánh xạ quyền hạn từ Claims của JWT:

Java
http.oauth2ResourceServer(oauth2 -> oauth2
    .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedConverter = new JwtGrantedAuthoritiesConverter();
    grantedConverter.setAuthoritiesClaimName("roles");
    grantedConverter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(grantedConverter);
    return converter;
}

4. Custom Exception Handling

Phản hồi chuẩn JSON RFC 7807 thay vì redirect mã 302 về trang HTML đăng nhập:

Java
http.exceptionHandling(ex -> ex
    .authenticationEntryPoint((req, res, err) -> {
        res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        res.setContentType("application/json");
        res.getWriter().write("{\"error\": \"Unauthorized\", \"status\": 401}");
    })
    .accessDeniedHandler((req, res, err) -> {
        res.setStatus(HttpServletResponse.SC_FORBIDDEN);
        res.setContentType("application/json");
        res.getWriter().write("{\"error\": \"Forbidden\", \"status\": 403}");
    })
);

Cơ chế Lambda DSL đem lại lợi ích kiến trúc và kiểm thử gì?

Nhờ thiết kế Customizer<T>, ta có thể trích xuất từng khối cấu hình thành các Bean độc lập để tái sử dụng xuyên suốt nhiều chuỗi Filter Chain khác nhau:

Java
@Component
public class StatelessCustomizer implements Customizer<SessionManagementConfigurer<HttpSecurity>> {
    @Override
    public void customize(SessionManagementConfigurer<HttpSecurity> configurer) {
        configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

// Reuse in configuration
@Bean
public SecurityFilterChain apiChain(HttpSecurity http, StatelessCustomizer sessionCustomizer) throws Exception {
    http.sessionManagement(sessionCustomizer);
    return http.build();
}

Ngoài ra, với các tính năng muốn áp dụng cấu hình mặc định an toàn của Spring, ta chỉ cần gọi Customizer.withDefaults():

Java
http.httpBasic(Customizer.withDefaults());

Những cạm bẫy phổ biến khi viết SecurityFilterChain DSL là gì?

  • Nhầm lẫn giữa hasRole và hasAuthority: hasRole("ADMIN") tự động tìm quyền ROLE_ADMIN. Nếu truyền hasRole("ROLE_ADMIN"), Spring sẽ tìm chuỗi ROLE_ROLE_ADMIN và luôn trả về lỗi 403.
  • Đặt .anyRequest() sai vị trí: Luôn đặt .anyRequest() ở dòng cuối cùng trong khối authorizeHttpRequests.
  • Chuỗi nhiều FilterChain thiếu securityMatcher: Nếu định nghĩa nhiều bean SecurityFilterChain mà chain đầu tiên không dùng .securityMatcher("/api/admin/**"), nó sẽ bắt trọn mọi request và các chain phía sau không bao giờ được chạm tới.

Những điểm then chốt cần ghi nhớ là gì?

  • Spring Security 6 chuẩn hóa toàn bộ cấu hình theo phong cách @Bean SecurityFilterChain và lambda DSL.
  • Thứ tự phân quyền tuân theo nguyên tắc "First match wins", yêu cầu sắp đặt từ chi tiết đến tổng quát.
  • Các API REST không dùng cookie cần tắt CSRF và đưa session về chế độ STATELESS.
  • Xử lý ngoại lệ 401 và 403 cần được override để phản hồi định dạng JSON đồng nhất cho client.

Bạn đã làm chủ SecurityFilterChain Lambda DSL chưa?

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao Spring Security 6 loại bỏ WebSecurityConfigurerAdapter để chuyển sang lambda DSL?
  2. Q2
    Nếu trong cấu hình bạn đặt .anyRequest().authenticated() trước .requestMatchers('/api/admin/**').hasRole('ADMIN'), điều gì sẽ xảy ra?
  3. Q3
    Sự khác biệt căn bản giữa hasRole('MANAGER') và hasAuthority('MANAGER') là gì?
  4. Q4
    Tại sao các ứng dụng REST API dùng JWT trong Authorization header thường tắt CSRF (csrf.disable())?
  5. Q5
    Khi có nhiều bean SecurityFilterChain, phương thức nào trên HttpSecurity dùng để giới hạn phạm vi URL mà một chain đảm nhận?

Bài tiếp theo: Multiple chains & stateless config

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 4/19 của chặng

Thảo luận & góp ý

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

Bài tiếp theo

Đa Chuỗi SecurityFilterChain và Cấu Hình Stateless JWT