Nginx 反向代理开启 CORS:Access-Control 配置与 OPTIONS 预检处理

前后端分离时,在 Nginx 反向代理层统一添加 CORS 头,比在每个后端框架里单独配置更集中可控。

基础配置

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://backend-server:8002;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # CORS 响应头
        add_header Access-Control-Allow-Origin * always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, X-Requested-With" always;

        # OPTIONS 预检请求直接返回 204
        if ($request_method = OPTIONS) {
            return 204;
        }
    }
}

always 参数确保 CORS 头在 4xx/5xx 错误响应中也会附加。

带 Credentials 时不能用通配符

Access-Control-Allow-Origin: * 不能与 Access-Control-Allow-Credentials: true 同时使用,浏览器会拒绝:

# 错误:浏览器拒绝
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Credentials true always;

需要改为动态匹配请求来源:

add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;

Vary: Origin 告知缓存层不同 Origin 的响应不同,防止缓存污染。

多域名白名单:map 方案

允许特定域名列表访问(带 Credentials):

map $http_origin $cors_origin {
    default "";
    "~^https?://localhost(:\d+)?$" $http_origin;
    "~^https://app\.example\.com$" $http_origin;
    "~^https://admin\.example\.com$" $http_origin;
}

server {
    location /api/ {
        proxy_pass http://backend-server:8002;

        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Access-Control-Allow-Credentials true always;
        add_header Vary 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;
        }
    }
}

非白名单来源 $cors_origin 为空字符串,不会添加 CORS 头,浏览器拒绝跨域请求。

常见坑

proxy_pass 末尾斜杠影响路径

proxy_pass http://backend:8002;     # /api/users → /api/users
proxy_pass http://backend:8002/;    # /api/users → //users(多了斜杠)

后端已有 CORS 头导致重复

如果后端也返回了 CORS 头,Nginx 再叠加会导致响应中出现两个 Access-Control-Allow-Origin,浏览器拒绝。需要在后端关闭 CORS,统一由 Nginx 管理:

proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $cors_origin always;