Nginx 反向代理 + 全局跨域:那些看着通了但其实拦了的坑

前后端分离项目上线,前端一直报跨域,可 Nginx 里明明写了 Access-Control-Allow-Origin *。折腾一圈才发现,跨域配置里有几个”看着通了但其实拦了”的隐蔽坑。

最小可用配置

先把最小骨架搭起来——反代到后端 + 全局放开 CORS:

server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://backend.example.com: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;

        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,Origin,User-Agent,DNT,Cache-Control,X-Requested-With,Token" always;

        if ($request_method = OPTIONS) {
            return 204;
        }
    }
}

这份能应付纯 API、无登录态的场景。要带 cookie 的话继续往下看,不然会直接被浏览器拦。

坑一:*Allow-Credentials: true 不能共存

很多人从两份教程各抄一段,凑成:

add_header Access-Control-Allow-Origin       * always;
add_header Access-Control-Allow-Credentials  true always;

浏览器规范明确禁止这种组合,直接跨域失败。

正确做法:把 * 换成 $http_origin 或白名单:

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

坑二:Host 被写死

见过这种写法:

proxy_set_header Host backend.example.com;

后端如果基于 Host 做路由(Spring、Django 之类)或者生成回调 URL,就全乱套。改回:

proxy_set_header Host $host;

坑三:OPTIONS 只返回 204,没带 CORS 头

写了:

if ($request_method = OPTIONS) {
    return 204;
}

有些浏览器要求预检响应本身也带跨域头,直接 return 204 就少一批头。解法是让 add_header 放在 if 之前——Nginx 的 add_header 会自动挂到最终响应上,只要不用 error_page 覆盖响应就行。

生产版本(推荐)

同时兼顾 cookie、白名单和预检:

map $http_origin $cors_origin {
    default "";
    "~^http://localhost:.*"          $http_origin;
    "~^https://your-frontend\.com$"  $http_origin;
}

server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://backend.example.com: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;

        add_header Access-Control-Allow-Origin      $cors_origin always;
        add_header Access-Control-Allow-Credentials true         always;
        add_header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, OPTIONS, PATCH" always;
        add_header Access-Control-Allow-Headers     "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Requested-With,Token" always;
        add_header Vary Origin always;

        if ($request_method = OPTIONS) {
            return 204;
        }
    }
}

map 是精髓:预定义的域名才回响原 Origin,其它的会拿到空字符串,浏览器就自然拒绝。

一张表决定怎么选

场景推荐写法
纯 API,无 cookieAllow-Origin *
带 cookie / tokenmap + $http_origin 白名单
本地开发临时放开Allow-Origin $http_origin

一句话总结

CORS 卡住 90% 的时候都是这三件事:*Credentials、Host 写死、OPTIONS 没配好。抄配置前先想清楚要不要带 cookie。