CORS Giải Thích: Hướng Dẫn Toàn Diện Cho Web Developer

Bạn đang build một frontend app trên localhost:3000. Gọi API trên localhost:8080. Browser quăng ra lỗi đỏ chói:
Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy.
Bạn vừa dính cái cơ chế security phổ biến nhất — và cũng bị hiểu lầm nhiều nhất — trên web. Mọi web developer đều gặp CORS, và đa số fix bằng cách bịt mắt thêm Access-Control-Allow-Origin: * mà chẳng hiểu mình đang làm gì.
Bài này giải thích CORS từ đầu: tại sao nó tồn tại, nó hoạt động thế nào bên dưới, và cách configure đúng trong ứng dụng thực tế.
Bạn Sẽ Học Được Gì
✅ Tại sao browser block cross-origin request (same-origin policy)
✅ CORS là gì và nó giải quyết problem gì
✅ Simple request vs. preflight request hoạt động ra sao
✅ Mọi CORS header và chúng control cái gì
✅ Cách configure CORS trong Express, FastAPI, Spring Boot, và Go
✅ Các lỗi CORS phổ biến và cách debug
✅ Security best practice (tại sao * nguy hiểm)
Yêu Cầu Trước Khi Bắt Đầu
Trước khi đi sâu, bạn nên có:
- Hiểu biết cơ bản về HTTP method và header
- Quen thuộc với REST API
- Experience cơ bản về frontend/backend development
Phần 1: Same-Origin Policy
"Origin" Là Gì?
Một origin được xác định bởi ba phần của URL:
| Phần | Ví dụ |
|---|---|
| Scheme (protocol) | https |
| Host (domain) | api.example.com |
| Port | 443 |
Hai URL có cùng origin chỉ khi cả ba phần khớp chính xác.
Cùng origin:
https://example.com/page1vàhttps://example.com/page2— cùng scheme, host, port
Khác origin (cross-origin):
http://example.comvshttps://example.com— khác schemehttps://example.comvshttps://api.example.com— khác host (subdomain cũng tính)https://example.comvshttps://example.com:8080— khác port
Tại Sao Same-Origin Policy Tồn Tại?
Same-origin policy (SOP) là một cơ chế security của browser, hạn chế cách script từ origin này tương tác với resource từ origin khác.
Nếu không có nó, bất kỳ website nào cũng có thể:
- Đánh cắp data của bạn: Một trang web độc hại có thể fetch inbox Gmail của bạn qua
fetch('https://mail.google.com/api/messages')— browser sẽ tự động gắn cookie Gmail của bạn - Thực hiện action thay bạn: Chuyển tiền từ ngân hàng, đăng bài trên social media, đổi password
- Đọc response nhạy cảm: Access token, data cá nhân, private API response
Same-origin policy đảm bảo rằng browser sẽ không cho phép website này đọc response từ website khác trừ khi được cho phép rõ ràng.
SOP Block Và Không Block Cái Gì
Bị block bởi SOP (đọc cross-origin response):
fetch()vàXMLHttpRequestđến origin khác- Đọc content từ cross-origin
<iframe> - Truy cập cross-origin
<canvas>data
KHÔNG bị block bởi SOP (embed thì OK):
<img src="https://other-site.com/image.png">— hình ảnh load cross-origin<script src="https://cdn.example.com/lib.js">— script chạy cross-origin<link href="https://fonts.googleapis.com/...">— stylesheet load cross-origin<form action="https://other-site.com/submit">— form submit cross-origin
Điểm mấu chốt: Embed resource cross-origin thì được phép, nhưng đọc cross-origin response bằng code thì bị block.
Phần 2: CORS Là Gì?
Vấn Đề
Same-origin policy tốt cho security, nhưng quá chặt cho web development hiện đại. Những use case hợp lệ bao gồm:
- Frontend trên
app.example.comgọi API trênapi.example.com - React app trên
localhost:3000gọi backend trênlocalhost:8080 - Web view của mobile app gọi API của bạn
- Third-party integration sử dụng public API của bạn
Giải Pháp: CORS
CORS (Cross-Origin Resource Sharing) là cơ chế cho phép server nói với browser: "Ổn rồi — cho phép cross-origin request này đi."
Nó hoạt động thông qua HTTP header. Server đính kèm các header cụ thể trong response để nói cho browser biết origin, method, và header nào được phép.
CORS không phải security feature bảo vệ server — server vẫn xử lý request bình thường. CORS nói cho browser biết liệu có cho frontend JavaScript đọc response hay không.
Quan trọng: CORS được enforce bởi browser, không phải server. Các tool như
curl, Postman, và server-to-server request hoàn toàn bypass CORS vì chúng không implement same-origin policy.
Phần 3: CORS Hoạt Động Thế Nào — Simple vs. Preflight Request
Simple Request
Simple request không trigger preflight check. Browser gửi request trực tiếp và kiểm tra response header.
Một request là "simple" nếu thỏa mãn tất cả điều kiện:
| Điều kiện | Giá trị được phép |
|---|---|
| Method | GET, HEAD, POST |
| Content-Type | text/plain, multipart/form-data, application/x-www-form-urlencoded |
| Headers | Chỉ CORS-safelisted header (Accept, Accept-Language, Content-Language, Content-Type) |
Ví dụ — simple GET request:
// Frontend: https://app.example.com
fetch('https://api.example.com/posts')
.then(response => response.json())
.then(data => console.log(data));Chuyện gì xảy ra bên dưới:
GET /posts HTTP/1.1
Host: api.example.com
Origin: https://app.example.comHTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
[{"id": 1, "title": "Hello World"}]Browser thấy Access-Control-Allow-Origin khớp với origin đang request → JavaScript đọc được response.
Preflight Request
Bất kỳ request nào không qualify là "simple" đều trigger preflight request — một OPTIONS request phụ hỏi server xin phép trước khi gửi request thật.
Những thứ trigger preflight:
- Dùng method như
PUT,DELETE,PATCH - Gửi
Content-Type: application/json - Đính kèm custom header như
Authorization,X-Request-ID
Ví dụ — POST với JSON (trigger preflight):
// Frontend: https://app.example.com
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
},
body: JSON.stringify({ name: 'Alice' })
});Bước 1: Browser gửi OPTIONS preflight:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, AuthorizationBước 2: Server phản hồi với permission:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600Bước 3: Browser gửi request thật (nếu preflight pass):
POST /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{"name": "Alice"}Toàn bộ flow minh họa:
Preflight Caching
Preflight request thêm latency vì mỗi cross-origin API call cần hai round trip. Header Access-Control-Max-Age nói browser cache kết quả preflight:
Access-Control-Max-Age: 3600 // Cache 1 giờSau khi cache, các request tiếp theo đến cùng endpoint sẽ bỏ qua preflight trong suốt thời gian cache.
| Browser | Access-Control-Max-Age tối đa |
|---|---|
| Chrome | 7200 (2 giờ) |
| Firefox | 86400 (24 giờ) |
| Safari | 604800 (7 ngày) |
Phần 4: Các CORS Header Giải Thích
Request Header (Browser → Server)
Các header này browser tự động set — bạn không bao giờ set thủ công:
| Header | Mục đích | Ví dụ |
|---|---|---|
Origin | Origin đang tạo request | https://app.example.com |
Access-Control-Request-Method | Method mà request thật sẽ dùng (chỉ preflight) | PUT |
Access-Control-Request-Headers | Custom header mà request thật sẽ gửi (chỉ preflight) | Content-Type, Authorization |
Response Header (Server → Browser)
Đây là các header server của bạn phải set:
| Header | Mục đích | Ví dụ |
|---|---|---|
Access-Control-Allow-Origin | Origin nào được phép | https://app.example.com hoặc * |
Access-Control-Allow-Methods | HTTP method nào được phép | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Request header nào được phép | Content-Type, Authorization |
Access-Control-Expose-Headers | Response header nào JavaScript đọc được | X-Total-Count, X-Request-ID |
Access-Control-Allow-Credentials | Cho phép cookie/auth header | true |
Access-Control-Max-Age | Thời gian cache preflight (giây) | 3600 |
Vấn Đề Credentials
Mặc định, cross-origin request không gửi kèm cookie hay Authorization header. Để gửi credential:
Frontend phải opt in:
fetch('https://api.example.com/profile', {
credentials: 'include' // Gửi cookie cross-origin
});Server phải opt in:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.example.com // PHẢI cụ thể, KHÔNG được dùng *Quy tắc quan trọng: Khi set
Access-Control-Allow-Credentials: true,Access-Control-Allow-Originkhông được là*. Bạn phải chỉ định origin cụ thể. Đây là security feature — wildcard + credential sẽ cho phép bất kỳ website nào tạo authenticated request đến API của bạn.
Phần 5: Configure CORS Trong Ứng Dụng Thực Tế
Node.js / Express
const express = require('express');
const cors = require('cors');
const app = express();
// Option 1: Cho phép mọi origin (chỉ dùng khi dev!)
app.use(cors());
// Option 2: Origin cụ thể
app.use(cors({
origin: 'https://app.example.com'
}));
// Option 3: Nhiều origin với credential
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 3600
}));
// Option 4: Validate origin động
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
// Cho phép request không có origin (server-to-server, curl, v.v.)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
app.listen(8080);Python / FastAPI
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Thêm CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com", "https://admin.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=3600,
)
@app.get("/api/users")
async def get_users():
return [{"id": 1, "name": "Alice"}]Java / Spring Boot
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("https://app.example.com");
config.addAllowedOrigin("https://admin.example.com");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}Hoặc dùng annotation cho từng controller:
@RestController
@CrossOrigin(origins = "https://app.example.com", maxAge = 3600)
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public List<User> getUsers() {
return userService.findAll();
}
}Go (net/http)
package main
import (
"encoding/json"
"net/http"
"slices"
)
func corsMiddleware(next http.Handler) http.Handler {
allowedOrigins := []string{
"https://app.example.com",
"https://admin.example.com",
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if slices.Contains(allowedOrigins, origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "3600")
}
// Xử lý preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"name": "Alice"})
})
http.ListenAndServe(":8080", corsMiddleware(mux))
}Nginx (Reverse Proxy)
Nếu backend không xử lý CORS, configure ở tầng Nginx:
server {
listen 80;
server_name api.example.com;
location /api/ {
# CORS headers
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' '3600' always;
# Xử lý preflight
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://localhost:8080;
}
}Cảnh báo: Đừng set CORS header ở cả Nginx lẫn application — bạn sẽ có duplicate header, và browser sẽ reject.
Phần 6: Các Lỗi CORS Phổ Biến Và Cách Debug
Lỗi 1: "No 'Access-Control-Allow-Origin' header is present"
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.Nguyên nhân: Server không trả về header Access-Control-Allow-Origin.
Fix: Thêm CORS header vào server response. Đảm bảo chúng được trả về cho mọi response, kể cả error response (4xx, 5xx).
Lỗi 2: "The 'Access-Control-Allow-Origin' header has a value that is not equal to the supplied origin"
Nguyên nhân: Server trả về Access-Control-Allow-Origin: https://other-site.com nhưng app của bạn ở https://app.example.com.
Fix: Cập nhật danh sách allowed origin trên server để bao gồm origin đang request.
Lỗi 3: "Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*'"
Nguyên nhân: Bạn gửi credentials: 'include' nhưng server phản hồi Access-Control-Allow-Origin: *.
Fix: Dùng origin cụ thể thay vì * khi cần credential.
Lỗi 4: "Method PUT is not allowed by Access-Control-Allow-Methods"
Nguyên nhân: Preflight check fail vì server không liệt kê PUT trong Access-Control-Allow-Methods.
Fix: Thêm method thiếu vào CORS configuration.
Lỗi 5: "Request header field Authorization is not allowed"
Nguyên nhân: Header Authorization không được liệt kê trong Access-Control-Allow-Headers.
Fix: Thêm Authorization vào allowed header configuration.
Checklist Debug
Khi gặp lỗi CORS, làm theo quy trình có hệ thống:
-
Mở DevTools → Network tab: Tìm request bị fail. Tìm preflight (
OPTIONS) request màu đỏ. -
Kiểm tra preflight response: Xem server có trả về đúng
Access-Control-*header không. -
Dùng curl để verify: Test mà không có browser CORS enforcement:
# Kiểm tra server có trả CORS header không curl -i -X OPTIONS https://api.example.com/data \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Content-Type, Authorization" -
Kiểm tra error response: CORS header phải có mặt trên mọi response, kể cả 401, 403, 404, và 500. Sai lầm phổ biến là chỉ thêm CORS header cho response thành công.
-
Kiểm tra duplicate header: Nếu bạn configure CORS ở cả reverse proxy (Nginx) lẫn application, browser có thể reject duplicate header.
Phần 7: Security Best Practice
1. Không Bao Giờ Dùng * Trong Production Với Credential
// ❌ NGUY HIỂM: cho phép bất kỳ website nào gọi API
app.use(cors({ origin: '*' }));
// ✅ AN TOÀN: chỉ frontend của bạn gọi được API
app.use(cors({ origin: 'https://app.example.com' }));Dùng Access-Control-Allow-Origin: * nghĩa là bất kỳ website nào cũng đọc được response từ API. Điều này ổn cho API thực sự public (ví dụ API thời tiết), nhưng nguy hiểm cho API phục vụ data theo user.
2. Validate Origin Động
Với ứng dụng multi-tenant hoặc nhiều frontend, validate dựa trên whitelist:
// ❌ TỆ: regex quá rộng
origin: /example\.com$/ // Match cả evil-example.com!
// ✅ TỐT: whitelist rõ ràng
const allowedOrigins = new Set([
'https://app.example.com',
'https://admin.example.com',
'https://staging.example.com'
]);
origin: (origin, callback) => {
if (!origin || allowedOrigins.has(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}3. Đừng Reflect Origin Header Một Cách Mù Quáng
// ❌ CỰC KỲ NGUY HIỂM: reflect bất kỳ origin nào
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});Cách này tương đương * với credential — bất kỳ site nào cũng tạo được authenticated request đến API của bạn.
4. Giới Hạn Method Và Header Được Expose
Chỉ expose những gì API thực sự cần:
// ❌ Quá rộng
methods: '*',
allowedHeaders: '*',
// ✅ Chỉ những gì cần
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],5. Cân Nhắc Alternative Thay Vì CORS
Đôi khi bạn không cần CORS:
| Scenario | Giải pháp |
|---|---|
| Frontend và API cùng domain | Không cần CORS — dùng path-based routing (/api/*) |
| Next.js app với API route | API route chạy trên cùng origin |
| Reverse proxy (Nginx) | Proxy /api request đến backend — browser thấy cùng origin |
| BFF (Backend for Frontend) | Server-side API call không có CORS restriction |
Phần 8: CORS Trong Các Scenario Khác Nhau
Single-Page Application (SPA)
Hầu hết SPA có frontend và backend tách biệt. CORS thường cần trong cả development lẫn production:
// Development: React trên :3000, API trên :8080
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? 'https://app.example.com'
: 'http://localhost:3000',
credentials: true
}));Microservices
Trong microservices architecture, CORS nên được configure ở API gateway level, không phải trong từng service riêng lẻ:
Public API
Với API thực sự public không có authentication:
app.use(cors({
origin: '*', // Ai cũng dùng được API
methods: ['GET'], // Chỉ đọc
// Không hỗ trợ credential
}));WebSocket Và Server-Sent Events
CORS không áp dụng cho WebSocket connection (ws:// hoặc wss://). WebSocket handshake dùng Origin header, nhưng server phải validate thủ công — browser không enforce CORS cho WebSocket.
Server-Sent Events (SSE) dùng HTTP thông thường, nên CORS áp dụng bình thường.
Tổng Kết
Same-origin policy block cross-origin read để ngăn đánh cắp data. CORS nới lỏng restriction này một cách có chọn lọc thông qua HTTP header.
Những điều cốt lõi:
- CORS là cơ chế của browser —
curlvà server-to-server call bỏ qua nó - Simple request (GET/POST với basic header) gửi trực tiếp; preflight request (
OPTIONS) kiểm tra permission trước Access-Control-Allow-Originlà header quan trọng nhất — set origin cụ thể, đừng dùng*- Khi gửi cookie hay auth header, cần
credentials: 'include'ở client vàAccess-Control-Allow-Credentials: trueở server (với origin cụ thể) - Cache preflight response bằng
Access-Control-Max-Ageđể giảm latency - Cân nhắc same-origin architecture (reverse proxy, BFF pattern) để tránh CORS hoàn toàn
Sai lầm phổ biến cần tránh:
- Dùng
*với credential - Reflect
Originheader mà không validate - Quên CORS header trên error response
- Configure CORS ở cả Nginx lẫn application (duplicate header)
- Dùng regex quá rộng để validate origin
Bài Viết Liên Quan
Nếu thấy bài này hữu ích, xem thêm các bài liên quan:
- HTTP Protocol Complete Guide — Deep dive vào protocol mà CORS xây dựng trên đó
- What is REST API? Complete Guide — API design principle và best practice
- Web Security Fundamentals — CIA triad, threat modeling, và defense in depth
- OWASP Top 10 — Các web application security risk nghiêm trọng nhất
- Express Middleware Deep Dive — Cách middleware như CORS hoạt động trong Express
- Authentication in FastAPI — JWT và OAuth2 pattern hoạt động cùng CORS
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.