Spring Security & Testing
16/20
Bài 16 / 20~12 phútAuthorization & Web SecurityLộ trình · chặng 18/21Miễn phí… lượt xem

CORS — Same-Origin Policy, Preflight và Spring Security

Hiểu rõ Same-Origin Policy, cơ chế preflight OPTIONS, cấu hình CorsConfigurationSource an toàn trong Spring Security 6 và cấm kỵ wildcard credentials.

TL;DR: CORS (Cross-Origin Resource Sharing) không phải là cơ chế phòng thủ ở phía máy chủ mà là chính sách bảo mật do trình duyệt thực thi nhằm ngăn chặn JavaScript tại một Origin độc hại đọc lén dữ liệu từ Origin khác. Khi JavaScript gửi các yêu cầu phức tạp (như có Content-Type: application/json hoặc header Authorization), trình duyệt sẽ tự động phát một yêu cầu thăm dò preflight OPTIONS để hỏi máy chủ xem có chấp nhận không. Trong Spring Security 6, cấu hình CORS chuẩn mực phải thông qua http.cors(...) kết hợp CorsConfigurationSource để chèn CorsFilter lên trước AuthorizationFilter. Tiêu chuẩn W3C nghiêm cấm việc kết hợp allowedOrigins("*") với allowCredentials(true) để triệt tiêu nguy cơ chiếm đoạt phiên làm việc diện rộng.

Same-Origin Policy là gì và tại sao trình duyệt áp đặt cơ chế này?

Theo định nghĩa chuẩn mực RFC 6454, một Origin được xác định bởi bộ ba thành phần: (scheme, host, port). Hai URL chỉ được coi là cùng nguồn (Same-Origin) khi và chỉ khi cả ba thành phần đều trùng khớp hoàn toàn:

URL Yêu CầuSo Với https://olhub.org:443Kết Luận
https://olhub.org/coursesTrùng scheme, host, port (443 mặc định)Same-Origin
http://olhub.org/coursesKhác scheme (http vs https)Cross-Origin
https://api.olhub.org/coursesKhác host (api.olhub.org vs olhub.org)Cross-Origin
https://olhub.org:8080/coursesKhác port (8080 vs 443)Cross-Origin

Same-Origin Policy (SOP) được thiết kế nhằm bảo vệ người dùng: nếu bạn đang đăng nhập vào ngân hàng và vô tình mở một trang web chứa mã độc, JavaScript của trang độc hại không thể âm thầm gọi API của ngân hàng để đánh cắp số dư và dữ liệu cá nhân.

Ranh giới cốt lõi: SOP chặn đọc kết quả, không chặn gửi yêu cầu

Nhiều lập trình viên lầm tưởng CORS ngăn chặn các cuộc gọi gửi đến server. Trên thực tế, trình duyệt vẫn gửi request đi trọn vẹn, server vẫn tiếp nhận và xử lý thay đổi dữ liệu, nhưng trình duyệt sẽ từ chối bàn giao dữ liệu phản hồi (Response Body) cho JavaScript:

Vòng bốn góc: JS ở olhub.org gọi fetch tới api.olhub.org, mũi tên gửi đi trọn vẹn nên server xử lý request và side-effect đã xảy ra, response 200 kèm body đi ngược về browser, nhưng browser chặn ở bước cuối vì thiếu Access-Control-Allow-Origin khớp origin nên JS nhận TypeError thay vì đọc được body

Vì SOP không chặn chiều gửi yêu cầu, các cuộc tấn công thay đổi trạng thái (như chuyển tiền qua form POST) vẫn có thể thành công nếu không có biện pháp phòng thủ CSRF độc lập.

Yêu cầu preflight OPTIONS được kích hoạt trong những trường hợp nào?

Trình duyệt phân loại các yêu cầu HTTP thành hai nhóm:

  1. Simple Request (Yêu cầu đơn giản): Sử dụng các phương thức GET, HEAD, POST với các header tiêu chuẩn cổ điển (Accept, Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain). Nhóm này không cần gửi preflight vì tương thích ngược với hành vi form HTML từ thập niên 1990.
  2. Non-simple Request (Yêu cầu phức tạp): Bất kỳ yêu cầu nào sử dụng Content-Type: application/json, các phương thức PUT, PATCH, DELETE, hoặc chứa Custom Header như Authorization: Bearer <token>.

Hầu hết các REST API hiện đại đều thuộc nhóm Non-simple Request. Do đó, trình duyệt luôn bắt buộc phát một yêu cầu OPTIONS thăm dò trước:

Trục thời gian năm bước: SPA gọi một lời fetch POST với body JSON và header Authorization, browser tự chèn một request OPTIONS mang Access-Control-Request-Method POST, server trả 200 kèm Allow-Origin, Allow-Methods, Allow-Headers, Max-Age 3600 và Vary Origin, preflight pass được cache một giờ, rồi POST thật mới chạy và trả 201 Created

Thông số Access-Control-Max-Age chỉ thị thời gian (tính bằng giây) trình duyệt được phép lưu kết quả thăm dò vào bộ nhớ đệm (Cache), giúp tránh việc phải gửi preflight lặp lại cho các lời gọi API tiếp theo trên cùng một endpoint.

Cấu hình CORS chuẩn mực trong Spring Security 6 được thực hiện ra sao?

Cấu hình CORS chuẩn xác nhất trong môi trường Spring Boot 3 là đăng ký bean CorsConfigurationSource và tích hợp trực tiếp vào chuỗi lọc của HttpSecurity:

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Kich hoat CorsFilter trong chuoi Spring Security
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable()) // Tat CSRF cho REST API dung Bearer token
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            );

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        
        // Danh sach Origin cu the duoc phep truy cap
        config.setAllowedOrigins(List.of("https://olhub.org", "https://app.olhub.org"));
        
        // Cac phuong thuc HTTP duoc phep
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        
        // Cac Header ma client duoc phep gui len
        config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-Id"));
        
        // Cac Header ma JavaScript tren trinh duyet duoc phep doc
        config.setExposedHeaders(List.of("Location", "X-Total-Count"));
        
        // Cho phep mang theo Cookie hoac thong tin xac thuc trinh duyet
        config.setAllowCredentials(true);
        
        // Luu cache preflight trong 1 gio (3600 giay)
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

Vì sao WebMvcConfigurer thường gây lỗi 401 khi tích hợp Spring Security?

Nhiều lập trình viên có thói quen cấu hình CORS thông qua việc cài đặt interface WebMvcConfigurer:

Java
// CAU HINH NAY CO THE GAY LOI KHI DUNG VOI SPRING SECURITY
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://olhub.org")
            .allowedMethods("*");
    }
}

Nguyên nhân sâu xa: Thứ tự thực thi của Filter Chain

Cơ chế WebMvcConfigurer chỉ có hiệu lực ở tầng DispatcherServlet của Spring MVC. Khi một request gửi đến ứng dụng:

  1. Request bắt buộc phải đi qua toàn bộ chuỗi lọc SecurityFilterChain trước tiên.
  2. Tại đây, AuthorizationFilter thấy một request OPTIONS (preflight) gửi đến endpoint yêu cầu xác thực nhưng không có header Authorization.
  3. Spring Security lập tức từ chối và trả về mã lỗi 401 Unauthorized hoặc 403 Forbidden.
  4. Request bị chặn đứng hoàn toàn và không bao giờ chạm tới được tầng Spring MVC để áp dụng cấu hình WebMvcConfigurer.

Khi dùng http.cors(...), Spring Security tự động chèn một CorsFilter chuyên dụng vào vị trí sớm nhất trong chuỗi lọc (đứng trước AuthorizationFilter). Nhờ đó, các request preflight OPTIONS được nhận diện và xử lý trả về mã 200 OK ngay lập tức mà không bị kiểm tra quyền đăng nhập.

Các thuộc tính cấu hình CORS cốt lõi hoạt động như thế nào?

1. setAllowedOrigins so với setAllowedOriginPatterns

Khi ứng dụng phục vụ mô hình đa người thuê (Multi-tenant) với các tên miền phụ động (https://tenant-a.olhub.org, https://tenant-b.olhub.org), sử dụng danh sách tĩnh là bất khả thi:

Java
// Khuyen nghi cho he thong SaaS su dung subdomain dong
config.setAllowedOriginPatterns(List.of("https://*.olhub.org"));

Khi sử dụng setAllowedOriginPatterns, Spring Security đối soát header Origin gửi lên với pattern. Nếu khớp, Spring sẽ phản hồi đúng giá trị origin đó (Echo) thay vì trả về ký tự đại diện *, tuân thủ hoàn hảo tiêu chuẩn bảo mật.

2. Sự cần thiết của setExposedHeaders

Mặc định, trình duyệt chỉ cho phép mã JavaScript đọc được một số header an toàn (Cache-Control, Content-Type, Content-Length). Nếu API của bạn trả về thông tin phân trang qua X-Total-Count hoặc đường dẫn bản ghi mới qua Location, JavaScript sẽ luôn nhận giá trị null trừ khi bạn công khai chúng qua exposedHeaders:

Java
config.setExposedHeaders(List.of("X-Total-Count", "Location"));

3. Hiểu đúng về setAllowCredentials

Thuộc tính này chỉ định việc trình duyệt có được phép tự động đính kèm Cookie hoặc thông tin xác thực HTTP Basic khi gửi yêu cầu cross-origin hay không. Đối với kiến trúc Single Page Application sử dụng Bearer Token thuần túy lưu trữ trong bộ nhớ và tự truyền qua header Authorization, bạn không cần bật allowCredentials(true).

Tại sao tiêu chuẩn Web cấm tuyệt đối tổ hợp wildcard và credentials?

Kịch bản tấn công chiếm đoạt tài khoản diện rộng sẽ xảy ra nếu tiêu chuẩn cho phép kết hợp Access-Control-Allow-Origin: * cùng với Access-Control-Allow-Credentials: true:

Chuỗi năm bước: user đã login olhub.org nên cookie session nằm trong browser, user mở evil.com và JS ở đó gọi fetch tới api.olhub.org/user/profile với credentials include, browser tự đính kèm cookie olhub.org nên request tới nơi và server xử lý, server trả Access-Control-Allow-Origin sao kèm Access-Control-Allow-Credentials true, và browser từ chối bằng network error TypeError Failed to fetch — nếu browser không chặn ở bước này thì mọi website đều đọc được profile của mọi user đang đăng nhập

Nếu trình duyệt chấp nhận cấu hình này:

  1. Bạn đã đăng nhập vào dịch vụ email tại https://mail.olhub.org.
  2. Bạn truy cập vào một trang web giải trí độc hại https://evil.com.
  3. Script trên evil.com âm thầm gửi fetch("https://mail.olhub.org/inbox", { credentials: "include" }).
  4. Trình duyệt tự đính kèm Cookie của mail.olhub.org. Nếu máy chủ phản hồi Allow-Origin: * và trình duyệt cho phép, evil.com có thể đọc toàn bộ hòm thư cá nhân của bạn.

Do đó, tiêu chuẩn W3C/Fetch Specification quy định: Nếu cờ credentials bật (true), trình duyệt bắt buộc phải từ chối nếu Origin phản hồi là dấu hoa thị *.

Java
// HOP LE: Chi dinh ro rang ten mien tin cay
config.setAllowedOrigins(List.of("https://olhub.org"));
config.setAllowCredentials(true);

// HOP LE: Cho phep tat ca cho public API khong can session
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(false);

Các bước chẩn đoán và khắc phục sự cố CORS thường gặp là gì?

Triệu chứng lỗiNguyên nhân gốc rễGiải pháp xử lý
No 'Access-Control-Allow-Origin' headerOrigin yêu cầu chưa có trong danh sách cho phépBổ sung Origin vào allowedOrigins
Wildcard with credentials errorMáy chủ trả về * khi credentials bậtDùng Origin tường minh hoặc allowedOriginPatterns
Preflight OPTIONS trả mã 401 UnauthorizedAuthorizationFilter chặn preflightSử dụng http.cors(...) trong SecurityFilterChain
Header 'X-Custom-Header' is not allowedThiếu khai báo header gửi lên từ clientThêm header vào allowedHeaders
JavaScript đọc header trả về nhận nullHeader phản hồi chưa được công khaiĐăng ký header vào exposedHeaders

Bạn có thể kiểm tra trực tiếp phản hồi preflight từ dòng lệnh mà không bị ảnh hưởng bởi cache của trình duyệt bằng công cụ curl:

Bash
curl -i -X OPTIONS https://api.olhub.org/api/projects \
    -H "Origin: https://olhub.org" \
    -H "Access-Control-Request-Method: POST" \
    -H "Access-Control-Request-Headers: Authorization,Content-Type"

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

  • CORS là cơ chế phòng thủ phía client do trình duyệt thực thi dựa trên Same-Origin Policy (SOP).
  • SOP ngăn cản JavaScript đọc kết quả phản hồi nhưng không ngăn chặn việc gửi yêu cầu HTTP.
  • Yêu cầu phức tạp (như có JSON payload hoặc Bearer token) luôn kích hoạt preflight OPTIONS.
  • Luôn cấu hình CORS thông qua CorsConfigurationSource trong Spring Security để CorsFilter được đặt trước các bộ lọc xác thực.
  • Tuyệt đối không bao giờ kết hợp allowedOrigins("*") với allowCredentials(true).

Bạn đã làm chủ cơ chế CORS và cấu hình Spring Security chưa?

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao Same-Origin Policy (SOP) trên trình duyệt không thể tự mình ngăn chặn triệt để các cuộc tấn công CSRF?
  2. Q2
    Nguyên nhân phổ biến nhất khiến yêu cầu preflight OPTIONS luôn bị từ chối với mã lỗi 401 Unauthorized là gì?
  3. Q3
    Tại sao tiêu chuẩn Fetch Specification nghiêm cấm việc cấu hình đồng thời Access-Control-Allow-Origin: * và Access-Control-Allow-Credentials: true?
  4. Q4
    Để cho phép ứng dụng Single Page (SPA) đọc được header phân trang 'X-Total-Count' trả về từ REST API, lập trình viên cần cấu hình thuộc tính nào?
  5. Q5
    Trong kiến trúc SaaS hỗ trợ subdomain động (ví dụ: https://tenant1.olhub.org, https://tenant2.olhub.org), phương thức cấu hình nào được khuyến nghị trong Spring Security 6?

Bài tiếp theo: CSRF — Bản Chất Tấn Công và Chiến Lược Bảo Vệ

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

Thảo luận & góp ý

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

Bài tiếp theo

CSRF — Cross-Site Request Forgery và Chiến Lược Bảo Vệ