目录
- 什么是CORS预检请求
- 为什么OPTIONS请求经常失败
- Nginx处理OPTIONS的两种方案
- 方案一:if指令处理OPTIONS
- 方案二:map指令动态处理
- 常见坑与排查方法
- 完整配置模板
一、什么是CORS预检请求
当浏览器发起跨域请求时,如果满足以下任一条件,浏览器会先发一个OPTIONS请求(也叫预检请求,preflight request):
- 请求方法不是GET、HEAD、POST
- POST的Content-Type不是application/x-www-form-urlencoded、multipart/form-data、text/plain
- 请求携带了自定义Header(如Authorization、X-Token等)
预检请求的本质是浏览器替你问服务器:\"我能不能发这个跨域请求?\"服务器必须正确响应,浏览器才会发出真正的请求。
一个典型的预检请求长这样:
OPTIONS /api/user HTTP/1.1
Host: api.example.com
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type
服务器需要返回类似这样的响应:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
注意响应码是204,不是200。204表示\"成功但没有内容返回\",这正是预检响应的标准做法。
二、为什么OPTIONS请求经常失败
很多开发者配置了CORS之后发现GET请求正常,但PUT、DELETE或者带自定义Header的POST还是报跨域错误。原因通常有这几种:
1. OPTIONS请求被业务逻辑拦截
Nginx把OPTIONS请求转发到了后端应用,后端没有处理OPTIONS方法的逻辑,返回405或401,浏览器认为预检失败。
2. 响应头缺失
只配了Access-Control-Allow-Origin,没配Access-Control-Allow-Methods或Access-Control-Allow-Headers,浏览器检查不通过。
3. 多次add_header被覆盖
Nginx的add_header在if块和location块中是不同的层级,内层会覆盖外层,导致某些CORS头丢失。
4. 重复的CORS头
同时配了多层add_header,响应中出现两个Access-Control-Allow-Origin,浏览器直接拒绝。
三、Nginx处理OPTIONS的两种方案
核心思路只有一个:在Nginx层直接响应OPTIONS请求,不转发到后端。这样做的好处:
- 减轻后端压力,OPTIONS请求根本不到达应用服务器
- 配置集中,不容易出现前后端CORS配置冲突
- 响应更快,Nginx直接返回204,延迟极低
下面介绍两种实现方式。
四、方案一:if指令处理OPTIONS
这是最常见的写法,简单直接:
server {
listen 80;
server_name api.example.com;
location /api/ {
# 处理预检请求
if ($request_method = 'OPTIONS') {
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, X-Requested-With' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain; charset=utf-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
# 正常请求的CORS头
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, X-Requested-With' always;
proxy_pass http://backend;
}
}
要点说明:
$http_origin动态获取请求来源,比写死*更安全always参数确保即使响应码是4xx/5xx也加上CORS头Access-Control-Max-Age 86400让浏览器缓存预检结果24小时,减少OPTIONS请求- if块内的add_header和if块外的add_header是独立的,两边都要写全
if方案的局限
Nginx的if指令有个著名的问题:\"if is evil\"。在if块中使用add_header时,if块内的add_header会覆盖外层的所有add_header。所以你必须确保if块内的CORS头是完整的。
五、方案二:map指令动态处理
如果你的API需要支持多个特定域名(不允许所有来源),map方案更优雅:
# 在http块中定义白名单映射
map $http_origin $cors_origin {
default "";
"https://www.example.com" "https://www.example.com";
"https://admin.example.com" "https://admin.example.com";
"https://app.example.com" "https://app.example.com";
}
map $cors_origin $cors_enabled {
default 0;
"" 0;
"~^https?://" 1;
}
server {
listen 80;
server_name api.example.com;
location /api/ {
# 预检请求处理
if ($request_method = 'OPTIONS' && $cors_enabled = 1) {
add_header 'Access-Control-Allow-Origin' '$cors_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
add_header 'Access-Control-Max-Age' 86400 always;
return 204;
}
# 正常跨域请求
if ($cors_enabled = 1) {
add_header 'Access-Control-Allow-Origin' '$cors_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
}
proxy_pass http://backend;
}
}
map方案的优势:
- 域名白名单集中管理,增删域名只改map块
- 不在白名单的来源直接不返回CORS头,安全性更高
- 避免Access-Control-Allow-Origin出现*或错误域名
简化版:允许所有来源
如果是内部API或者不需要严格限制来源,可以用更简单的写法:
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Max-Age' 86400 always;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' '*' always;
proxy_pass http://backend;
}
六、常见坑与排查方法
坑1:OPTIONS返回301/302重定向
如果location末尾没加斜杠,而proxy_pass末尾加了,Nginx会对OPTIONS请求做重定向,浏览器就报跨域失败了。
解决:确保location和proxy_pass的路径一致,或者显式处理OPTIONS。
坑2:后端框架也返回了CORS头
后端(如Spring、Django)自己配了CORS,Nginx也配了CORS,响应头里出现重复的Access-Control-Allow-Origin。
解决:只在一个地方配CORS,推荐全部交给Nginx,后端不要管CORS。
坑3:携带Cookie时的CORS配置
前端设置了withCredentials: true时,Access-Control-Allow-Origin不能写*,必须写具体域名,还要加上:
add_header 'Access-Control-Allow-Credentials' 'true' always;
坑4:Nginx配置语法检查
改完配置后先检查语法:
nginx -t
再重新加载:
nginx -s reload
排查步骤
- 浏览器F12看Network:找到OPTIONS请求,看Response Headers有没有CORS头
- curl模拟预检:
curl -X OPTIONS -H "Origin: https://www.example.com" -H "Access-Control-Request-Method: PUT" -i https://api.example.com/api/user - 检查Nginx错误日志:
tail -f /var/log/nginx/error.log - 检查后端日志:OPTIONS是否真的没到后端
七、完整配置模板
以下是一个生产环境可用的完整配置,直接改域名就能用:
server {
listen 80;
server_name api.example.com;
# 允许的域名白名单(按需修改)
map $http_origin $cors_origin {
default "";
"https://www.example.com" "https://www.example.com";
"https://admin.example.com" "https://admin.example.com";
}
# API接口
location /api/ {
# 预检请求:Nginx直接响应,不到后端
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$cors_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, Accept' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
return 204;
}
# 正常请求:携带CORS头转发到后端
add_header 'Access-Control-Allow-Origin' '$cors_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, Accept' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
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;
proxy_pass http://127.0.0.1:8080;
}
}
相关文章推荐
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论