CORS trong lập trình web: Nguyên lý hoạt động và cách cấu hình trên mọi backend

CORS (Cross-Origin Resource Sharing) là cơ chế bảo mật của trình duyệt web cho phép máy chủ kiểm soát nguồn nào được truy cập tài nguyên của mình. Đây là một trong những khái niệm quan trọng nhất mà lập trình viên web cần nắm khi xây dựng ứng dụng tách frontend và backend thành hai domain khác nhau.

Vì sao trình duyệt chặn request chéo nguồn?

Trình duyệt áp dụng Same-Origin Policy (SOP) — chính sách cùng nguồn gốc — từ những năm 1990: script chạy trên trang domain-a.com không được đọc dữ liệu từ domain-b.com. Mục đích là chặn các cuộc tấn công XSS và CSRF, ngăn website độc hại đánh cắp cookie hoặc token của người dùng từ domain khác.

Hai nguồn được coi là khác nhau khi khác về giao thức (http vs https), tên miền hoặc cổng (port). Ví dụ: frontend chạy tại frontend.com gọi API api.backend.com/users là một request chéo nguồn. Ngay cả localhost:3000 gọi localhost:5000 cũng bị coi là chéo nguồn vì khác port.

Trước đây SOP hoạt động tốt với website truyền thống, nhưng khi các ứng dụng SPA (React, Vue, Angular) tách frontend khỏi backend phát triển bùng nổ, SOP trở nên quá chặt. CORS ra đời để nới lỏng SOP một cách có kiểm soát.

Bảng DevTools Network hiển thị request OPTIONS preflight với header CORS
Bảng DevTools Network hiển thị request OPTIONS preflight với header CORS

CORS hoạt động thế nào?

Request đơn giản (Simple request)

Với request đơn giản — GET, POST với nội dung đơn giản — trình duyệt gửi thẳng request kèm header Origin:

GET /api/data HTTP/1.1
Host: api.backend.com
Origin: https://frontend.com

Nếu máy chủ trả về header Access-Control-Allow-Origin: frontend.com khớp với Origin gửi lên, trình duyệt cho phép frontend đọc response. Nếu không có header này, request bị chặn và hiện lỗi nổi tiếng trên console:

Access to fetch at 'https://api.backend.com/data' from origin
'https://frontend.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Sơ đồ luồng CORS preflight giữa trình duyệt và máy chủ
Sơ đồ luồng CORS preflight giữa trình duyệt và máy chủ

Request preflight

Với request phức tạp — method PUT/DELETE, header tùy chỉnh như Authorization, hoặc Content-Type: application/json — trình duyệt gửi một request OPTIONS trước để hỏi máy chủ cho phép gì:

OPTIONS /api/users/1 HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type

Máy chủ trả lời bằng các header cho phép:

Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

Header Access-Control-Max-Age cho trình duyệt biết cache kết quả preflight trong bao lâu, giúp giảm số request OPTIONS lặp lại.

Credentials và cookie

Khi dùng fetch(url, { credentials: 'include' }) hoặc withCredentials = true, máy chủ bắt buộc phải trả về cả hai header:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://frontend.com

Lưu ý quan trọng: khi có credentials, Access-Control-Allow-Origin không được dùng ký tự đại diện * — phải chỉ định origin cụ thể.

Cách cấu hình CORS trên các backend phổ biến

Node.js / Express

const cors = require('cors');
// Phát triển: cho phép tất cả
app.use(cors());
// Production: whitelist cụ thể
app.use(cors({
  origin: ['https://frontend.com', 'https://admin.frontend.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  credentials: true,
  maxAge: 86400
}));

Python / Flask

from flask_cors import CORS
CORS(app, resources={
    r"/api/*": {
        "origins": ["https://frontend.com"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Authorization", "Content-Type"],
        "supports_credentials": True
    }
})

Django

# settings.py
CORS_ALLOWED_ORIGINS = ["https://frontend.com"]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = ["authorization", "content-type"]

Nginx

location /api/ {
    add_header Access-Control-Allow-Origin $http_origin always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
    if ($request_method = OPTIONS) { return 204; }
}

Ví dụ thực tế: frontend gọi API với token

Hãy xem tình huống phổ biến nhất: ứng dụng React tại app.example.com gọi API tại api.example.com với header Authorization: Bearer .... Vì request chứa header tùy chỉnh, trình duyệt sẽ gửi preflight OPTIONS trước. Nếu backend trả về thiếu Access-Control-Allow-Headers: Authorization, trình duyệt chặn request ngay lập tức dù backend xử lý đúng — lỗi này chiếm phần lớn các câu hỏi “CORS hoạt động ở localhost nhưng fail khi deploy” trên diễn đàn lập trình.

Một lỗi tinh vi khác: cấu hình Access-Control-Allow-Origin cứng theo một domain cụ thể nhưng quên rằng URL có hoặc không có www là hai origin khác nhau. Tương tự, khi deploy frontend lên Vercel hoặc Netlify, domain preview mỗi lần build lại đổi — nên dùng danh sách origin động (đọc từ biến môi trường) thay vì hardcode để tránh phải sửa config liên tục.

Mẹo thực tế tránh lỗi CORS

  • Kiểm tra header trả về bằng DevTools → Network → Response Headers khi gặp lỗi.
  • Không bao giờ dùng Access-Control-Allow-Origin: * trong production khi có credentials.
  • Dùng proxy server (Vite proxy, Next.js rewrites, Nginx reverse proxy) để request cùng domain, tránh CORS hoàn toàn.
  • Thiếu Access-Control-Allow-Headers: Authorization là lỗi phổ biến nhất khi gọi API có token.
  • Nhớ rằng CORS là cơ chế của trình duyệt — request từ Postman, curl hoặc server-to-server không bị chặn.

Tham khảo thêm

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Elixir là gì? Hướng dẫn ngôn ngữ lập trình chạy trên Erlang VM

Elixir là ngôn ngữ lập trình hàm, chạy trên máy ảo BEAM của Erlang, được thiết kế để xây dựng hệ thống phân tán có khả năng chịu lỗi và…

Xem thêm

GitHub Actions là gì? Tự động hóa CI/CD ngay trong GitHub

GitHub Actions là nền tảng tự động hóa CI/CD được tích hợp sẵn trong GitHub, cho phép lập trình viên xây dựng, kiểm thử và triển khai mã nguồn ngay…

Xem thêm

WebAssembly là gì? Tìm hiểu cách chạy mã gốc trên trình duyệt

WebAssembly (thường viết tắt là Wasm) là định dạng mã nhị phân được thiết kế để chạy gần bằng tốc độ gốc trên trình duyệt web. WebAssembly là gì và…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất