0

Nginx CORS预检请求OPTIONS处理配置:解决跨域预检失败的完整实战

2026.05.23 | youres | 13次围观

目录

  1. 什么是CORS预检请求
  2. 为什么OPTIONS请求经常失败
  3. Nginx处理OPTIONS的两种方案
  4. 方案一:if指令处理OPTIONS
  5. 方案二:map指令动态处理
  6. 常见坑与排查方法
  7. 完整配置模板

一、什么是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

排查步骤

  1. 浏览器F12看Network:找到OPTIONS请求,看Response Headers有没有CORS头
  2. curl模拟预检curl -X OPTIONS -H "Origin: https://www.example.com" -H "Access-Control-Request-Method: PUT" -i https://api.example.com/api/user
  3. 检查Nginx错误日志tail -f /var/log/nginx/error.log
  4. 检查后端日志: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辅助作者原创,未经许可,转载请保留原文链接。

发表评论
883文章数 0评论数
作者其它文章