前后端分离项目上线,前端一直报跨域,可 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,无 cookie | Allow-Origin * |
| 带 cookie / token | map + $http_origin 白名单 |
| 本地开发临时放开 | Allow-Origin $http_origin |
一句话总结
CORS 卡住 90% 的时候都是这三件事:* 撞 Credentials、Host 写死、OPTIONS 没配好。抄配置前先想清楚要不要带 cookie。
