
1. 为什么用 Nginx 解决 CORS 问题而不是在后端代码里加几行 header这个问题我被问过至少三十七次——从刚毕业的前端实习生到做了八年架构的老哥都在纠结既然Access-Control-Allow-Origin: *一行就能写完为啥还要折腾 Nginx甚至有团队在 FastAPI 里写了CORSMiddleware上线后还是报has been blocked by CORS policy: no access-control-allow-origin header is present最后发现是前端发的是带 credentials 的请求比如带 cookie 或Authorization头而*根本不兼容credentials: true。真相是CORS 不是一个“加 header 就完事”的单点配置而是一整套请求生命周期的协同机制。它横跨浏览器、客户端 JS、服务端响应、网络中间件四个层面。你只在应用层改 header就像给汽车装了空调却忘了配压缩机——表面凉快一启动就报警。举个真实案例某政务系统前端用 Vue 开发后端是 Spring Boot Shiro登录态靠 session cookie 维持。开发阶段用vue.config.js配了devServer.proxy一切正常但上线后Nginx 反向代理到后端集群用户登录后反复跳转登录页。排查三天才发现Vue 的proxy是开发时的本地 Webpack 中间件它把/api/xxx请求重写为http://localhost:8080/api/xxx并转发同时自动处理了 Origin 和 Cookie 转发而生产环境 Nginx 默认不透传Origin头也不设置Access-Control-Allow-Credentials: true更不会根据请求 Origin 动态反射Access-Control-Allow-Origin。浏览器一看响应里没Access-Control-Allow-Origin或者值不是当前页面的 exact origin立刻拦截响应体——连 HTTP 状态码都拿不到。所以Nginx 做反向代理解决 CORS本质是把跨域治理从“应用逻辑层”下沉到“基础设施层”。好处有三第一解耦与统一。10 个微服务每个都写一套 CORS 配置万一某个服务升级框架中间件漏配了Vary: Origin缓存就出问题。Nginx 一层配置所有后端服务回归“纯业务逻辑”不用关心跨域。第二精准控制请求上下文。Nginx 能拿到原始请求的Host、X-Forwarded-For、Origin甚至能读取请求体需开启proxy_buffering off这就意味着你可以做条件判断比如只对Origin匹配https://app.example.com的请求放行对http://localhost:3000的开发环境请求允许*对非法来源直接return 403。这种粒度应用层 middleware 很难干净实现——它得先解析请求再决策再构造响应链路长、开销大。第三规避 credential 场景下的经典陷阱。当credentials: true时Access-Control-Allow-Origin必须是具体域名不能是*同时必须显式声明Access-Control-Allow-Credentials: true且Access-Control-Allow-Headers必须包含客户端实际发送的自定义头如X-Request-ID最后Vary: Origin头必须存在否则 CDN 或浏览器缓存可能把 A 域名的响应错发给 B 域名。Nginx 的map指令和add_header指令组合能原子化完成这四件事零延迟、零额外进程。提示很多人以为add_header在location块里写一次就够了其实 Nginx 的add_header是“覆盖式添加”如果上游 proxy_pass 返回了Access-Control-Allow-Origin你的add_header会把它干掉。正确做法是用proxy_hide_header先隐藏上游 header再用add_header重写。这个细节90% 的线上配置都错了。我见过最典型的错误配置是这样写的location /api/ { proxy_pass http://backend; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; }看着没问题但只要后端服务自己也返回了Access-Control-Allow-OriginNginx 就会把两个 header 都塞进响应浏览器直接拒收——HTTP 规范不允许重复的Access-Control-Allow-Origin。这就是为什么必须先proxy_hide_header Access-Control-Allow-Origin;。所以别再问“能不能不配 Nginx”该问的是“你的跨域场景是否已经复杂到需要基础设施层介入”——如果你的系统有多个前端域名、需要精细化 Origin 白名单、要支持带凭证的请求、或已接入 CDN/负载均衡答案一定是必须配而且得配对。2. Nginx CORS 配置的核心指令链从 map 到 add_header 的完整闭环Nginx 的 CORS 配置不是堆砌几行add_header就能跑通的。它是一条精密的指令流水线每个环节都有明确职责缺一不可。我把这条链拆成五个关键环节按执行顺序讲清楚每一步“为什么必须这么写”。2.1 第一环用map指令动态映射 Origin解决*与credentials的根本矛盾这是整个 CORS 配置的基石。map指令在 Nginx 的http块中定义它能在请求进入server或location前根据Origin请求头的值预先计算出一个变量值。这个变量后续会被add_header引用。为什么非用map不可因为add_header不支持条件表达式你不能写if ($origin ~* example\.com) { add_header ... }。而map是唯一能在变量层面做正则匹配并赋值的机制。标准写法如下放在http块内map $http_origin $cors_origin { default ; ~^https?://(app\.example\.com|admin\.example\.com|localhost:3000)$ $http_origin; ~^https?://(staging\.example\.com)$ $http_origin; }这里的关键点有三个第一default 是安全兜底。当$http_origin为空比如 curl 直接请求或某些爬虫不带 Origin或不匹配任何规则时$cors_origin为空字符串。后续add_header Access-Control-Allow-Origin $cors_origin;就不会输出该 header浏览器自然拒绝——这比放行非法来源强一万倍。第二正则必须用~^开头表示大小写敏感的开始匹配。https?://覆盖 http 和 httpsapp\.example\.com中的点要转义否则.是正则通配符$表示严格结尾防止app.example.com.hacker.com这种绕过。第三值直接用$http_origin而不是硬编码域名。这是为了支持“Origin 反射”——即响应头里的Access-Control-Allow-Origin必须和请求头里的Origin完全一致。很多教程写死add_header Access-Control-Allow-Origin https://app.example.com;这在多前端域名场景下必挂。注意map指令只能在http块中使用不能放在server或location内。且$http_origin是 Nginx 自动提取的请求头变量命名规则是$http_ 小写 header 名Origin→origin。2.2 第二环用proxy_hide_header清除上游干扰避免 header 冲突上游服务如 FastAPI、Spring Boot很可能自己也注入了 CORS header。如果不清理Nginx 的add_header会和上游的 header 同时存在导致响应头重复浏览器直接判定为非法响应。必须在location块中显式隐藏location /api/ { proxy_pass http://backend; proxy_hide_header Access-Control-Allow-Origin; proxy_hide_header Access-Control-Allow-Credentials; proxy_hide_header Access-Control-Allow-Methods; proxy_hide_header Access-Control-Allow-Headers; proxy_hide_header Access-Control-Expose-Headers; proxy_hide_header Access-Control-Max-Age; }proxy_hide_header的作用是当 Nginx 从 upstream 收到响应后在转发给客户端前把指定 header 从响应中彻底删除。这样后续add_header才能干净地写入我们可控的值。提示proxy_hide_header对Set-Cookie也有效但慎用如果后端依赖 Set-Cookie 设置 session隐藏它会导致登录失败。CORS 相关 header 可以放心隐藏其他 header 需按需判断。2.3 第三环用add_header注入标准化 CORS 响应头核心输出这才是真正向浏览器宣告“我允许跨域”的环节。基于前面map计算出的$cors_origin我们注入全套 headerlocation /api/ { # ... proxy_hide_header 已上文写出 ... # 允许的源动态反射 add_header Access-Control-Allow-Origin $cors_origin always; # 允许携带凭证cookie、Authorization等 add_header Access-Control-Allow-Credentials true always; # 允许的 HTTP 方法 add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, PATCH, OPTIONS always; # 允许客户端发送的自定义请求头 add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Request-ID always; # 暴露给前端 JS 的响应头如 X-Total-Count add_header Access-Control-Expose-Headers Content-Length,Content-Range,X-Request-ID always; # 预检请求缓存时间秒 add_header Access-Control-Max-Age 17280000 always; # 关键告诉缓存系统响应依赖 Origin 头不能乱缓存 add_header Vary Origin always; }这里每个always参数至关重要。默认情况下add_header只在 HTTP 状态码为 200、201、204、206、301、302、303、304、307、308 时生效。但 OPTIONS 预检请求返回的是 204如果没有alwaysadd_header就不会执行浏览器收不到 CORS header预检失败后续真实请求直接被拦。always表示“无论状态码是什么都添加这个 header”。这是处理 OPTIONS 请求的强制要求。2.4 第四环专门处理 OPTIONS 预检请求短路响应不转发浏览器在发送复杂请求如POST带Content-Type: application/json或带自定义 header前会先发一个OPTIONS请求探路。这个请求不该打到后端因为后端通常不处理 OPTIONS或者处理了也慢要走完整 MVC 链路。最佳实践是Nginx 拦截OPTIONS直接返回 204不走proxy_pass。location /api/ { # ... 上述所有 add_header ... # 拦截 OPTIONS 请求立即返回 204 if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin $cors_origin; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, PATCH, OPTIONS; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Request-ID; add_header Access-Control-Expose-Headers Content-Length,Content-Range,X-Request-ID; add_header Access-Control-Max-Age 17280000; add_header Vary Origin; add_header Content-Length 0; add_header Content-Type text/plain; charsetutf-8; return 204; } proxy_pass http://backend; }注意if在location内是安全的但if在server块顶层是危险的Nginx 官方明确反对。这里放在location内仅用于方法判断无性能风险。return 204后Nginx 立即终止处理不执行后续proxy_pass毫秒级响应。实测比转发到后端再返回快 5~10 倍。2.5 第五环全局Vary: Origin与缓存策略协同防止 CDN 缓存污染Vary: Origin是 CORS 的“缓存保险丝”。它的意思是“这个响应的内容取决于请求头里的 Origin 字段。所以CDN 或浏览器缓存时必须把 Origin 作为缓存 key 的一部分。”没有它会发生什么假设用户 A 从https://app.example.com发起请求Nginx 返回Access-Control-Allow-Origin: https://app.example.comCDN 把这个响应缓存下来。紧接着用户 B 从https://hacker.com发起同样路径的请求CDN 直接把缓存的响应含app.example.com的 Origin返回给他——浏览器一看Origin不匹配立刻拦截。更糟的是这个错误响应还被缓存了影响所有后续请求。所以Vary: Origin必须加而且要加在所有可能返回 CORS header 的响应上。上面add_header Vary Origin always;已覆盖。但光有Vary不够。如果你的 API 本身也支持缓存比如GET /products返回静态商品列表就得确保Vary和Cache-Control协同location /api/products { # ... CORS 配置 ... add_header Cache-Control public, max-age3600; add_header Vary Origin, Accept-Encoding; }这里Vary同时包含Origin和Accept-Encoding因为 gzip 压缩后的响应体不同也得单独缓存。实操心得我在某电商项目上线后发现iOS Safari 对Vary头异常敏感漏配一个Vary: Origin整个首页商品列表的跨域请求就 50% 失败。后来加了always和Vary故障率归零。这不是玄学是 HTTP 缓存规范的硬性要求。这五环指令链环环相扣。少一环轻则开发环境能跑、生产环境报错重则引发安全漏洞如 Origin 反射未校验导致任意域名劫持。我建议把这五环做成 checklist每次部署新 API 前逐项核对。3. 从 Ubuntu 到银河麒麟Nginx 安装与配置落地的全平台实操指南Nginx 的跨域配置再完美如果连安装都卡在第一步一切归零。我经历过从 Ubuntu 22.04 桌面版、CentOS 7 最小化安装、银河麒麟 V10 SP1信创环境、到树莓派 ARM64 的全平台部署。不同系统差异极大尤其信创环境稍不注意就踩坑。下面按实战优先级给出可直接复制粘贴的命令和避坑要点。3.1 Ubuntu/Debian 系统apt 安装最快但版本可能过旧Ubuntu 官方源的 Nginx 版本通常滞后。比如 Ubuntu 22.04 默认是 1.18.x而 1.18.0 有已知的proxy_buffering与add_header always的兼容问题。推荐用官方 NGINX 仓库安装最新稳定版# 1. 安装依赖 sudo apt update sudo apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring # 2. 添加 NGINX 官方签名密钥 curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg /dev/null # 3. 添加稳定版仓库注意mainline 是开发版不稳定 echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/nginx-archive-keyring.gpg] \ http://nginx.org/packages/ubuntu lsb_release -cs nginx | sudo tee /etc/apt/sources.list.d/nginx.list # 4. 更新并安装 sudo apt update sudo apt install -y nginx # 5. 验证 sudo nginx -v # 应输出 1.24.x 或更高 sudo systemctl status nginx # 确保 active (running)注意lsb_release -cs输出的是jammy22.04或focal20.04自动适配。千万别手敲jammy否则在其他 Ubuntu 版本上会失败。常见问题sudo nginx -t报错unknown directive add_header。这是因为你装了nginx-core或nginx-light这些精简包它们阉割了部分模块。apt install nginx默认安装的是完整版nginx-full务必确认。3.2 CentOS/RHEL 系统yum/dnf 安装重点解决 SELinux 干扰CentOS 7/8 默认启用 SELinux它会阻止 Nginx 访问非标准端口如后端服务监听的 8000、8080或读取自定义配置文件。这是502 Bad Gateway的头号元凶。# 1. 安装 EPEL 仓库提供额外软件包 sudo yum install -y epel-release # 或 CentOS 8 用 dnf sudo dnf install -y epel-release # 2. 安装 Nginx sudo yum install -y nginx # 或 dnf sudo dnf install -y nginx # 3. 关键允许 Nginx 连接网络SELinux sudo setsebool -P httpd_can_network_connect 1 sudo setsebool -P httpd_can_network_connect_db 1 # 如果后端是数据库 # 4. 启动并开机自启 sudo systemctl start nginx sudo systemctl enable nginx验证 SELinux 是否生效# 查看布尔值状态 getsebool httpd_can_network_connect # 应输出httpd_can_network_connect -- on # 查看 Nginx 进程的 SELinux 上下文 ps -eZ | grep nginx # 正常应包含 system_u:system_r:httpd_t:s0踩坑实录某金融客户在 CentOS 7 上部署Nginx 配置完全正确curl -I http://localhost/api/test却返回502。/var/log/nginx/error.log里只有connect() failed (13: Permission denied)。折腾两天才发现是 SELinux 拦截。setsebool一行命令解决。信创环境务必牢记此步。3.3 银河麒麟 V10 SP1信创环境离线安装与 aarch64 移植银河麒麟基于 Ubuntu但内核和包管理有定制。官方不提供在线源必须离线安装。且多数服务器是鲲鹏aarch64架构x86_64 的二进制包无法运行。步骤一获取正确架构的安装包去 NGINX 官网下载页https://nginx.org/en/download.html不要下载.tar.gz源码编译太慢找Linux Packages下的.deb文件。注意区分nginx_1.24.0-1~jammy_amd64.deb→ x86_64nginx_1.24.0-1~jammy_arm64.deb→ aarch64鲲鹏银河麒麟 V10 SP1 对应jammy所以下载arm64.deb。步骤二离线安装依赖银河麒麟的apt依赖libpcre3,libssl1.1,zlib1g等。用一台联网的同版本麒麟机器导出依赖列表# 在联网麒麟上 apt download nginx libpcre3 libssl1.1 zlib1g # 打包所有 .deb 文件 tar -czf nginx-deps.tar.gz *.deb将nginx-deps.tar.gz拷贝到目标机器解压后批量安装tar -xzf nginx-deps.tar.gz sudo apt install -y ./*.deb步骤三安装 Nginx 主包sudo dpkg -i nginx_1.24.0-1~jammy_arm64.deb # 如果报依赖错误用 apt 修复 sudo apt --fix-broken install步骤四关闭麒麟自带防火墙firewalld 替代方案银河麒麟默认用ufw但有时是firewalldsudo ufw status # 查看状态 sudo ufw allow 80 sudo ufw allow 443 # 或如果是 firewalld sudo firewall-cmd --permanent --add-port80/tcp sudo firewall-cmd --reload信创经验银河麒麟的dpkg对包依赖检查极严--force-depends强制安装会破坏系统稳定性绝对禁止。宁可花时间找全依赖也不要跳过。3.4 Windows 10 WSL2 环境开发调试的黄金组合很多前端开发者用 Windows WSL2Ubuntu做本地开发。这是最接近生产环境的调试方式。# 在 WSL2 的 Ubuntu 中 sudo apt update sudo apt install -y nginx # 修改默认配置监听 8080避免 Windows 占用 80 sudo nano /etc/nginx/sites-available/default # 找到 server { listen 80; ... }改为 listen 8080; # 启动 sudo systemctl start nginx # 在 Windows 浏览器访问 http://localhost:8080应看到 Welcome to nginx!关键技巧WSL2 的 IP 是动态的但localhost从 Windows 侧访问 WSL2 是通的。所以你的 Vue 开发服务器http://localhost:3000通过http://localhost:8080/api/代理到后端完全模拟生产 Nginx 行为。我的开发流VS Code 远程连接 WSL2Nginx 配置文件实时编辑sudo nginx -t sudo nginx -s reload一键重载比在 Windows 原生跑 Nginx 稳定十倍。Windows 原生 Nginx 常因权限或路径问题崩溃。3.5 配置文件结构与热重载让修改秒生效Nginx 配置不是写完nginx.conf就完事。标准结构是/etc/nginx/ ├── nginx.conf # 主配置include sites-enabled/* ├── sites-available/ # 所有站点配置不生效 │ └── myapp # 你的 CORS 配置在此 ├── sites-enabled/ # 生效的配置软链接到 sites-available │ └── myapp - ../sites-available/myapp └── conf.d/ # 其他模块配置创建你的 CORS 配置sudo nano /etc/nginx/sites-available/myapp内容就是前面讲的五环配置完整示例# /etc/nginx/sites-available/myapp map $http_origin $cors_origin { default ; ~^https?://(app\.example\.com|admin\.example\.com|localhost:3000)$ $http_origin; } server { listen 80; server_name _; location /api/ { # 隐藏上游 CORS header proxy_hide_header Access-Control-Allow-Origin; proxy_hide_header Access-Control-Allow-Credentials; proxy_hide_header Access-Control-Allow-Methods; proxy_hide_header Access-Control-Allow-Headers; proxy_hide_header Access-Control-Expose-Headers; proxy_hide_header Access-Control-Max-Age; # 注入标准 CORS header 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, PATCH, OPTIONS always; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Request-ID always; add_header Access-Control-Expose-Headers Content-Length,Content-Range,X-Request-ID always; add_header Access-Control-Max-Age 17280000 always; add_header Vary Origin always; # 处理 OPTIONS 预检 if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin $cors_origin; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, PATCH, OPTIONS; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Request-ID; add_header Access-Control-Expose-Headers Content-Length,Content-Range,X-Request-ID; add_header Access-Control-Max-Age 17280000; add_header Vary Origin; add_header Content-Length 0; add_header Content-Type text/plain; charsetutf-8; return 204; } # 代理到后端 proxy_pass http://127.0.0.1:8000; # 假设后端 FastAPI 在 8000 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; } }启用配置# 创建软链接 sudo ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp # 测试配置语法 sudo nginx -t # 重载不中断服务 sudo nginx -s reload实操心得nginx -s reload是热重载毫秒级生效比systemctl restart nginx安全百倍。后者会短暂断连。我所有线上变更都用reload十年零事故。4. 真实排障链路从浏览器控制台报错到 Nginx 日志的完整定位再完美的配置上线后也可能出问题。我整理了一套标准化排障流程从浏览器最外层的报错信息一层层剥洋葱直到定位到 Nginx 配置的哪一行。这套流程帮我们团队在 15 分钟内解决过 90% 的 CORS 故障。4.1 第一层解读浏览器控制台报错精准锁定问题类型浏览器报错是第一线索。不同报错对应不同根因绝不能一概而论。报错 1has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource这是最常见报错但原因分三种情况 ANginx 根本没返回任何 CORS header检查打开浏览器 Network 面板找到那个失败的请求点开 → Response Headers。如果里面完全没有Access-Control-Allow-Origin说明 Nginx 的add_header没生效。排查方向map变量为空$cors_origin是或add_header写在了错误的location块或漏了always导致 OPTIONS 请求没 header。情况 BNginx 返回了Access-Control-Allow-Origin: *但请求带 credentials检查Response Headers 里有Access-Control-Allow-Origin: *同时请求头里有Cookie或Authorization。根因map没匹配到合法 Originfallback 到default 但你又在add_header里硬写了*。正确做法是map里不写default *, 而是default 然后靠if或add_header的条件逻辑控制。情况 CNginx 返回了Access-Control-Allow-Origin: https://hacker.com检查Response Headers 里的 Origin 值是非法域名。根因map的正则没写^和$导致app.example.com.hacker.com被误匹配。立即检查map正则。报错 2The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include这明确告诉你请求是credentials: truefetch 里写了{ credentials: include }但响应 header 是*。解决方案删掉所有硬编码的add_header Access-Control-Allow-Origin *;全部改用$cors_origin变量并确保map能匹配到当前请求的Origin。报错 3Preflight response is not successful或CORS preflight channel did not succeed这表示 OPTIONS 预检请求失败。检查Network 面板里找OPTIONS请求看它的 Status 是不是204或200。如果不是比如502、404说明 Nginx 没拦截住 OPTIONS转发给了后端而后端没处理。根因if ($request_method OPTIONS) { ... return 204; }这段代码没生效可能是因为if写在了server块顶层Nginx 不允许或location路径没匹配上。4.2 第二层抓包分析请求与响应确认 Nginx 是否介入浏览器控制台只能看到最终结果。要确认 Nginx 是否真的处理了请求必须抓包。工具选择WindowsWireshark 或 FiddlermacOS/Linuxtcpdump或 Charles Proxy关键命令Linux# 抓取 80 端口所有流量保存为 pcap sudo tcpdump -i any port 80 -w nginx-debug.pcap # 或只抓特定 host sudo tcpdump -i any host your-domain.com and port 80 -w nginx-debug.pcap用 Wireshark 打开nginx-debug.pcap过滤http找你的请求。重点看请求的Origin头是什么响应的状态码是204OPTIONS还是200真实请求响应头里有没有Access-Control-Allow-Origin值是什么如果抓包里看不到任何 CORS header说明请求根本没到 Nginx可能是 DNS 解析错误、端口没开、或前端 URL 写错了比如该用http://your-domain.com/api/却写了http://localhost:8000/api/。4.3 第三层检查 Nginx 错误日志定位配置执行路径Nginx 的error.log是真相之源。默认位置/var/log/nginx/error.log。开启详细日志临时# 在 http 块里 error_log /var/log/nginx/error.log debug;然后sudo nginx -s reload。debug 日志会记录每个请求匹配了哪个server、哪个location以及map变量的计算值。例如你会看到2024/05/20 10:23:45 [debug] 12345#12345: *1 http map started 2024/05/20 10:23:45 [debug] 12345#12345: *1 http map: https://app.example.com https://app.example.com 2024/05/20 10:23:45 [debug] 12345#12345: *1 http map done: https://app.example.com这证明map成功匹配。如果看到2024/05/20 10:23:45 [debug] 12345#12345: *1 http map: https://hacker.com 说明map的default 生效了$cors_origin是空add_header就不会输出。快速定位# 实时跟踪 error.log sudo tail -f /var/log/nginx/error.log | grep -E (map|cors|OPTIONS) # 查看最近 100 行 sudo tail -100 /var/log/nginx/error.log4.4 第四层手动模拟请求绕过浏览器直击 Nginx用curl模拟浏览器请求是最高效的验证方式。模拟简单 GET 请求curl -H Origin: https://app.example.com \ -H Accept: application/json \ -I http://localhost/api/test # -I 只看响应头 # 应看到 Access-Control-Allow-Origin: https://app.example.com