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ầu | So Với https://olhub.org:443 | Kết Luận |
|---|---|---|
https://olhub.org/courses | Trùng scheme, host, port (443 mặc định) | Same-Origin |
http://olhub.org/courses | Khác scheme (http vs https) | Cross-Origin |
https://api.olhub.org/courses | Khác host (api.olhub.org vs olhub.org) | Cross-Origin |
https://olhub.org:8080/courses | Khá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ì 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:
- Simple Request (Yêu cầu đơn giản): Sử dụng các phương thức
GET,HEAD,POSTvớ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. - 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ứcPUT,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:

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:
@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:
// 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:
- Request bắt buộc phải đi qua toàn bộ chuỗi lọc
SecurityFilterChaintrước tiên. - Tại đây,
AuthorizationFilterthấy một requestOPTIONS(preflight) gửi đến endpoint yêu cầu xác thực nhưng không có headerAuthorization. - Spring Security lập tức từ chối và trả về mã lỗi
401 Unauthorizedhoặc403 Forbidden. - 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:
// 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:
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:

Nếu trình duyệt chấp nhận cấu hình này:
- Bạn đã đăng nhập vào dịch vụ email tại
https://mail.olhub.org. - Bạn truy cập vào một trang web giải trí độc hại
https://evil.com. - Script trên
evil.comâm thầm gửifetch("https://mail.olhub.org/inbox", { credentials: "include" }). - Trình duyệt tự đính kèm Cookie của
mail.olhub.org. Nếu máy chủ phản hồiAllow-Origin: *và trình duyệt cho phép,evil.comcó 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ị *.
// 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ỗi | Nguyên nhân gốc rễ | Giải pháp xử lý |
|---|---|---|
No 'Access-Control-Allow-Origin' header | Origin yêu cầu chưa có trong danh sách cho phép | Bổ sung Origin vào allowedOrigins |
Wildcard with credentials error | Máy chủ trả về * khi credentials bật | Dùng Origin tường minh hoặc allowedOriginPatterns |
Preflight OPTIONS trả mã 401 Unauthorized | AuthorizationFilter chặn preflight | Sử dụng http.cors(...) trong SecurityFilterChain |
Header 'X-Custom-Header' is not allowed | Thiếu khai báo header gửi lên từ client | Thêm header vào allowedHeaders |
JavaScript đọc header trả về nhận null | Header 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:
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
CorsConfigurationSourcetrong 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ớiallowCredentials(true).
Bạn đã làm chủ cơ chế CORS và cấu hình Spring Security chưa?
- Q1Tạ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?
- Q2Nguyê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ì?
- Q3Tạ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?
- 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?
- Q5Trong 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ọc Dữ Liệu Method Security và PermissionEvaluator
Bài tiếpCSRF — Cross-Site Request Forgery và Chiến Lược Bảo Vệ
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