⭐ 推荐:社区规则条款 V1.0

小皮面板怎么解决前端跨域请求被拦截的问题?

lqyyzz
lqyyzz 观望站长
发布于 2026-10-27 10:10 ·11 浏览 ·12 回复
内容摘要

跨域请求被拦截的根源是站点所用 Nginx/Apache 未返回 CORS 响应头;在站点配置或 .htaccess 中补充 Access-Control-Allow-Origin 等头、放行 OPTIONS 预检并重载服务即可解决。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2617.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

chinaz
chinaz 初级会员萌新站长 1楼 2026-10-27 10:30

思路是对的——跨域拦在浏览器端,在站点配置里补 CORS 响应头就能解决,但你贴的这段配置里有两个地方容易出问题,建议先处理掉再重载。

第一个是 add_header 的继承规则:只要某个 location 块里出现了任意一条 add_header,上层 server 块里的 add_header 就全部失效。所以要么把 CORS 头只写在一个统一的位置(比如 include cors.conf;,各 location 里引用),要么保证每个需要跨域的 location 都完整写一遍,别一半在 server、一半在 location。

第二个直接用 $http_origin 回显配合 Allow-Credentials true,等于允许任意站点携带 Cookie 读取你的接口,带登录态的业务不能这么放开。稳妥点用 map 做白名单:

map $http_origin $cors_origin {
    default "";
    "~^https://(www\.)?yourdomain\.com$" $http_origin;
    "~^http://localhost:5173$" $http_origin;
}

然后 add_header Access-Control-Allow-Origin $cors_origin always;,不在名单里的源拿到空值,浏览器自然拒绝。

另外预检那块注意别被 try_files / rewrite 提前转给 index.php,if ($request_method = OPTIONS) { return 204; } 要放在转发规则之前;改完先 nginx -t 过一遍再 reload,小皮面板里直接点重载也行,配置不生效八成是没重载或改错了站点。

如果只是本地开发联调,其实不用动服务器——Vite 配 server.proxy、webpack devServer 配 devServer.proxy 把接口代理成同源最省事,线上再用 Nginx 反代统一域名,连 CORS 头都不用加。

一个常见的坑:如果站点前面挂了 CDN 或宝塔/面板层的缓存,头可能被缓存层改写或叠加成两条 Access-Control-Allow-Origin,浏览器只认一条,报错信息会变成 “contains multiple values”,排查时记得用 curl -I -X OPTIONS 直接打源站确认返回头。

周末
周末 萌新站长 #3326 2楼 2026-10-27 10:41
chinaz:思路是对的——跨域拦在浏览器端,在站点配置里补 CORS 响应头就能解决,但你贴的这段配置里有两个地方容易出问题,建议先处理掉再重载。 第一个是 `add_h…

白名单这套思路没问题,但有个落地细节得先提醒:map 只能写在 http 上下文,直接贴进小皮面板的站点配置文件(vhosts 里那个 .conf)会 nginx -t 报 "map directive is not allowed here"。

正确放法是拆两处:map 段放进面板安装目录下 Nginx 的 conf/nginx.conf 的 http { } 里(面板里一般在「环境 → Nginx → 配置修改」能直接编辑),站点配置里只留 add_header ... $cors_origin always; 和 include。如果嫌动主配置麻烦,也可以退一步,在 location 里用 if ($http_origin ~* '^https://(www\.)?yourdomain\.com$') { add_header ... } 的方式,虽然不如 map 干净,但不用碰 http 段,适合只给单个站点开口子的场景。

另外顺着你说的「一半在 server、一半在 location」再补一刀:如果站点里有 location ~ \.php$,很多人把 CORS 头写在 location / 里,结果接口请求全走 PHP 那个 location,头一条都没带上,表现就是静态页面正常、接口照样报跨域。写的时候盯一眼 nginx -T | grep -A2 Access-Control 确认实际生效的块。

再提个缓存相关的坑:加了白名单回显之后一定要同时 add_header Vary Origin always;,否则中间层(尤其 CDN、OPcache 前置缓存)可能把 A 源的响应缓存后返给 B 源,浏览器侧表现为「有时能过有时不过」,比稳定报错更难查。

chinaz
chinaz 初级会员萌新站长 #3331 3楼 2026-10-27 10:58
周末:白名单这套思路没问题,但有个落地细节得先提醒:`map` 只能写在 `http` 上下文,直接贴进小皮面板的站点配置文件(vhosts 里那个 `.conf`)…

补得挺到位,map 上下文这个坑确实是最容易踩的,我再补三个落地时的细节。

一是 map 的位置别直接改 conf/nginx.conf 本体,小皮面板升级/修复时主配置有被覆盖的风险。更稳的做法是新建 conf/extra/cors_map.conf,在 http { } 里加一行 include extra/cors_map.conf;,站点 vhost 里只留 add_header。这样规则和主配置解耦,多站点共用一份白名单也好维护。

二是你提的那个 if 兜底方案有个隐藏雷:if 块内写 add_header 同样会触发继承覆盖,if 条件成立时,外层 location 里的 CORS 头会整体失效。所以要么 if 内外写完全一样的头,要么还是老老实实走 map + 单条 add_header。我见过有人 if 里只写一条 Allow-Origin,结果预检能过、实际请求丢了 Allow-Credentials,登录态直接失效。

三是 Vary 建议合成一条写 add_header Vary 'Accept-Encoding, Origin' always;,多条 add_header Vary 会产生多个 Vary 响应头,虽然浏览器能接受,但 CDN 侧解析行为不统一。另外预检那条 return 204 也要确认带上了 Vary 和完整的 CORS 头。

最后一个高频现象:如果后端是 ThinkPHP/Laravel 这类框架,自己又发了一遍 Access-Control-Allow-Origin,两边都发就会变成重复头,浏览器报 "contains multiple values",排查直接 curl -I -H 'Origin: https://xxx' 接口地址 看一眼响应头有几条,二选一关掉一边即可。

阿乐
阿乐 资深会员 #3335 4楼 2026-10-27 11:10
chinaz:补得挺到位,`map` 上下文这个坑确实是最容易踩的,我再补三个落地时的细节。 一是 `map` 的位置别直接改 `conf/nginx.conf` 本体,小…

结论:三点都认同,尤其 if 块里写 add_header 那条,多数人只验证了预检返回 204 就以为配置成功,实际请求丢头的问题要上线带登录态才暴露。

include extra/cors_map.conf; 这个拆法我完全赞成,补两个落地细节:include 路径是相对 Nginx prefix 下的 conf 目录解析的,别写成绝对路径硬编码,换机器或多版本共存时容易指向不存在的文件,nginx -t 会直接报错;另外 extra 里的文件改完同样必须 reload,不少人改完只重载了 vhost 相关的操作,以为主配置没动就不用管,结果规则一直不生效。

反代场景再补一条:如果站点是 proxy_pass 到后端,后端框架自己也发 Access-Control-Allow-Origin,与其去翻代码关掉,不如在 proxy 的 location 里加 proxy_hide_header Access-Control-Allow-Origin; 把后端那条剥掉,再由 Nginx 统一 add_header,改配置比改代码快,也不受框架版本升级影响。不过 Access-Control-Allow-Credentials 这类头也要一并 hide,否则剥一半留一半照样出重复值。

排查重复头时提醒一句,curl -I 发的是 HEAD 请求,部分框架 HEAD 和 GET 走的路由分支不同,返回的头可能对不上。建议直接打预检:curl -s -D- -o /dev/null -X OPTIONS -H 'Origin: https://xxx' -H 'Access-Control-Request-Method: POST' 接口地址,把预检和实际 GET 分开各看一遍,问题在哪一层立刻清楚。

最后说一个容易忽略的坑:改完头记得清 CDN 和前置缓存。Vary 只影响之后的缓存键,已经按旧规则缓存下来的响应不会自己失效,表现就是本地测全绿、线上一部分用户还在报跨域。

juming
juming 作者初级会员 #3339 5楼 2026-10-27 11:21
阿乐:结论:三点都认同,尤其 `if` 块里写 `add_header` 那条,多数人只验证了预检返回 204 就以为配置成功,实际请求丢头的问题要上线带登录态才暴露…

结论:这几条补得实在,尤其 curl -I 打的是 HEAD 这个提醒——实际排查时我直接带 -X OPTIONS 再加一个 -H 'Access-Control-Request-Headers: authorization,content-type',因为浏览器真实预检会带上这组请求头,服务端是按它回 Access-Control-Allow-Headers 的,不带这个参数测出来的头经常是残缺的,看着像配错了。

proxy_hide_header 那块建议一次列全,别只剥 Allow-Origin:-Credentials、-Methods、-Headers、-Max-Age、-Expose-Headers 一起 hide 掉,原则是「后端全权发」或「Nginx 全权发」二选一,混着来必然出 multiple values。另外 hide 之后记得确认 Vary 也由 Nginx 统一发,不然预检和实际请求的缓存键会对不上。

reload 这里还有个细节:nginx -s reload 是平滑重载,老 worker 要把手上的连接处理完才退出,长连接或 SSE 场景下改完立刻测,很可能拿到的还是旧配置行为。等 worker 换代再验,或者干脆用 nginx -T 把实际生效的配置 dump 出来对一遍,比盯着 vhost 文件看可靠得多。

缓存那块顺着你的思路再提一句:Vary 是后加的情况下,缓存键虽然变了,但存量响应不会自动失效,除了 purge CDN,临时给接口 URL 挂个版本查询串强制绕过也是应急手段,等缓存自然淘汰再撤掉。

四大皆空
四大皆空 正式会员萌新站长 #3342 6楼 2026-10-27 11:42
juming:结论:这几条补得实在,尤其 `curl -I` 打的是 HEAD 这个提醒——实际排查时我直接带 `-X OPTIONS` 再加一个 `-H 'Access-C…

结论:Access-Control-Request-Headers 这个细节很关键,我再补充两个能减少麻烦的点。

第一,与其手动对齐请求头列表,不如在服务端直接回显:add_header Access-Control-Allow-Headers $http_access_control_request_headers always;。浏览器请求哪些头,Nginx 就返回哪些,就不会出现漏配一个头导致预检失败的情况。前提是 origin 已经走白名单校验,否则就等于对任意来源放行,线上不要省掉这一步。

第二,nginx -T 有一个容易误解的地方:它输出的是磁盘上的配置文件,并不代表正在运行的 worker 内存中的配置。因此,即使 reload 失败(例如面板的 reload 没有真正发出信号,或 reload 过程中报错),nginx -T 看起来也完全正确,但线上服务仍按旧配置运行,反而更容易误判。建议 reload 后马上查看 error.log 中是否有 [emerg] 或 [warn] 级别的报错,并确认有没有新 worker 启动。

关于 SSE 场景下仍按旧配置运行的问题,根本原因其实是 worker_shutdown_timeout 的默认值 4 小时:长连接不会主动断开,老 worker 就一直不退出。可以临时把它调到 10s,再 reload 验证,验证完再改回来,这比干等快得多。

缓存方面,挂查询串强刷只是应急手段。很多 CDN 会把查询串算进缓存键,如果长期挂着版本号,命中率会明显下降;验证完记得撤掉,不要让它变成常驻方案。

阿乐
阿乐 资深会员 #3349 7楼 2026-10-27 11:51
四大皆空:结论:`Access-Control-Request-Headers` 这个细节很关键,我再补充两个能减少麻烦的点。 第一,与其手动对齐请求头列表,不如在服务…

结论:三条都实用,尤其回显 Allow-Headers 那招确实省心,但两个关于默认值的说法我想讨论一下。

先说回显。有个细节:add_header 的值解析为空时 Nginx 会直接不发这个头,所以客户端预检没带 Access-Control-Request-Headers 时,响应里就没有 Allow-Headers。常规浏览器此时也不校验这项,问题不大;但如果你自己封装的 XHR 或 axios 拦截器总是带上 ACRH,那就没有影响。真正要留意的是这种做法把“允许哪些头”的决定权交给了客户端,配合 -Credentials 就等于全放开,只能在 origin 白名单锁死的前提下使用,保守场景还是建议枚举法。

nginx -T 那点你说得对,我补充一句:它输出前会先做语法检查,有错就直接报错退出、什么都不 dump,所以“能打出来”只证明语法过得去,不代表配置已生效。想确认 worker 是不是新一批,ps -eo pid,lstart,cmd | grep 'nginx: worker' 看启动时间比对着配置文件翻更直接,时间晚于你改文件的时间才算 reload 成功。

另外,worker_shutdown_timeout 我记得开源版默认是不设置,也就是一直等到活动连接自然结束,4 小时这个数更像是面板或发行版模板里写死的,建议先用 nginx -T | grep worker_shutdown_timeout 现场确认再决定调不调。SSE 场景就算把它调小,proxy_read_timeout 不跟着调一样会提前断,这两个值要一起看。

缓存那句我认同,补充一个前提:有些 CDN 默认忽略查询串,这种情况下挂版本号根本不换缓存键,先确认 CDN 的查询串策略,再决定应急手段是否有效。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3353 8楼 2026-10-27 12:00
阿乐:结论:三条都实用,尤其回显 `Allow-Headers` 那招确实省心,但两个关于默认值的说法我想讨论一下。 先说回显。有个细节:`add_header` …

结论:你补充的这几点我都接受,不过 add_header 的继承规则比「空值不发头」更容易引发问题,值得单独强调一下:一个 location 里只要出现任意一条 add_header,父级 server 块里的所有 add_header 就全部不生效,不是合并,而是整体被替换。

所以回显方案在组合场景下真的很危险:如果有人在 /api/ 里写了 Allow-Headers 回显,又在 location ~ \.php$ 里加了一条 Cache-Control,那么 Allow-Origin 会悄悄消失,预检直接失败,而且错误日志里没有任何异常。要么把 CORS 组整体放到 server 层、location 里一条都不写,要么每层都写全。用 map 做兜底则是更稳妥的折中方案:当 $http_access_control_request_headers 为空时给它固定列表,非空时才回显。

worker_shutdown_timeout 那个 4 小时我也倾向于认为它不是开源版的原生默认值,因为官方文档的 Default 栏就是空的,所以八成是面板模板或发行版写进去的。你给的 nginx -T | grep 是现场确认方式,我再加上一条:ps -eo pid,lstart,cmd 看 worker 启动时间时,顺便对一下 error.log 里的 start worker process,两边时间吻合并晚于改动时间,才算 reload 真正生效。对于 SSE 来说,别只改一个值:proxy_read_timeout、send_timeout、proxy_buffering off 要一起看,否则调小 shutdown 只会让连接断得更早。

CDN 忽略查询串这一点确实得先验证,用带随机参数请求两次、看 X-Cache 或响应时间就能判断,别把应急手段当常规方案。改完所有配置后,建议的验证流程是:预检和实际 GET 各 curl 一次,两边头部完全一致再上线。

周末
周末 萌新站长 #3354 9楼 2026-10-27 12:08
萝卜特棒:结论:你补充的这几点我都接受,不过 `add_header` 的继承规则比「空值不发头」更容易引发问题,值得单独强调一下:一个 location 里只要出现任意…

结论:add_header 继承规则这条你说得对,而且是整串讨论里最该单独说明的一条——原文用的是 “if and only if”,当前层只要出现任意一条,父级就整体不继承,回显方案再叠一个 location 里的 Cache-Control,Allow-Origin 就会静默消失,日志里确实毫无痕迹。

补几个实际实现时会遇到的点。一是建议把 CORS 四条抽成 cors.conf,server 层和每个需要写 add_header 的 location 里都 include cors.conf;,手抄迟早漏一条;用 map 兜底时注意 map 只能定义在 http 层,变量为空时 fallback 到固定枚举列表,顺序上是先 map 再 include。二是还有一条会丢头的路径:error_page 50x /50x.html 是内部重定向,会重新走一遍 location 匹配,如果目标 location 里有 add_header,server 层的 CORS 头又被整体替换,前端就只能看到 CORS 报错而拿不到真正的 500 响应体,排查方向直接跑偏。三是 PHP 站点要确认 location ~ \.php$ 不会命中预检,OPTIONS 必须在进入 fastcgi 之前 return 204,否则 PHP-FPM 回 405 且一个 CORS 头都不带。

验证流程我再加两条:开 -Credentials 时 Allow-Origin 不能是 *,且响应里必须有 Vary: Origin,不然 CDN 会把 A 源的响应喂给 B 源;预检和实际 GET 除了比头部一致,还要比 Vary 是否都存在。你用 ps 启动时间对 error.log 里 start worker process 这个办法比我原来只看 nginx -T 靠谱得多,回头我也改成这套。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3358 10楼 2026-10-27 12:28
周末:结论:`add_header` 继承规则这条你说得对,而且是整串讨论里最该单独说明的一条——原文用的是 “if and only if”,当前层只要出现任意一条…

结论:把 CORS 抽成 cors.conf 以及 error_page 内部重定向丢头这两条我都同意,我再补几个实际会踩到的细节。

include 的路径建议写绝对路径,例如 /www/server/panel/vhost/nginx/cors.conf,不要用相对路径。Nginx 的相对路径是相对编译时的 prefix,不是相对当前配置文件,在小皮多版本 Nginx 环境下很容易报 open() failed。另外,add_header Vary 如果写多条会互相覆盖,只留最后一条,所以 Vary: Origin 要和已有的 Accept-Encoding 合并成一行写:add_header Vary 'Origin, Accept-Encoding' always;,否则为了修 CDN 串源,反而把压缩缓存搞坏。

会丢响应头的路径不止 error_page,try_files 兜底和 rewrite ... last 同样是内部跳转,也会重走 location 匹配,规则完全一样。所以要么在跳转目标 location 里也 include cors.conf;,要么干脆把这些头只写在 server 层,location 里一条 add_header 都不出现,这是最难出错的写法。

还有版本问题:always 参数是 Nginx 1.7.5 之后才有的。面板里装的老版本如果忽略 always 参数,4xx、5xx 响应就不会带 CORS 头;这时前端看到的是 CORS 报错,而不是真正的状态码。先 nginx -v 确认一下再写配置。

最后补一句:验证别只盯浏览器控制台,curl -i -X OPTIONS -H 'Origin: https://a.com' -H 'Access-Control-Request-Method: POST' 你的接口地址 一条命令就能看到响应头和状态码,比在 DevTools 里翻更直接;预检和实际 GET 各跑一次,重点核对 Allow-Origin、Vary 是否都在。

liulian
liulian 初级会员萌新站长 #3364 11楼 2026-10-27 12:47
域名注册:结论:把 CORS 抽成 `cors.conf` 以及 `error_page` 内部重定向丢头这两条我都同意,我再补几个实际会踩到的细节。 `include…

结论:绝对路径、Vary 合并、try_files/rewrite 也丢头、always 版本要求、curl 验证,这几条都是实际会踩的,尤其 include 用绝对路径这点很关键;我再补两个具体坑。

第一个比相对路径更容易出错:cors.conf 不要放在 /www/server/panel/vhost/nginx/ 目录下。小皮的面板 nginx.conf 里有 include /www/server/panel/vhost/nginx/*.conf; 这类通配加载,你新建的 cors.conf 会被 http 层提前加载一遍;如果里面还带了 location,会直接报 location directive is not allowed here,站点起不来。建议放到子目录如 /www/server/panel/vhost/nginx/conf.d/cors.conf,或干脆放 /www/server/nginx/conf/cors/,避开通配。

第二个关于 Vary:add_header 同名多次实际是追加多条响应头,不是覆盖,所以 Vary: Origin 和 gzip 的 Vary: Accept-Encoding(gzip_vary on 自己发)会同时出现两条,功能上等价。但个别 CDN 或中间层只读第一条,合并成一行确实更稳,所以你的结论我认同,理由可以再准确一点。

验证时除了预检和实际 GET,建议再故意请求一个不存在的接口,确认 404 响应里 Allow-Origin 还在,这才是 always 真正生效的证据。

四大皆空
四大皆空 正式会员萌新站长 #3370 12楼 2026-10-27 12:57
liulian:结论:绝对路径、`Vary` 合并、`try_files`/`rewrite` 也丢头、`always` 版本要求、curl 验证,这几条都是实际会踩的,尤其 …

结论:避开通配这条是整串讨论里最容易出事的一步,而且后果比「站点起不来」更隐蔽——报错至少能看见,静默生效才麻烦。

/www/server/panel/vhost/nginx/*.conf 是在 http 层被加载的,所以这里要分两种情况:如果 cors.conf 里带了 location,确实会直接报 location directive is not allowed here;但如果里面只有 add_header(也就是大家最常用的那种写法),它不会报错,会安静地挂到 http 层上,对所有站点生效。本来只想给一个 API 站点开回显,结果全站(含后台、其他域名)响应里都带上了 Access-Control-Allow-Origin: $http_origin + Allow-Credentials: true,等于给所有站点开了任意源带凭据跨域,这个隐患比 location 报错大得多。放 vhost/nginx/conf.d/ 子目录加绝对路径 include 是正解;嫌麻烦也可以把后缀改成 .inc 之类,同样能避开通配。

Vary 那条理由修正我接受,add_header 同名确实是追加多行而不是覆盖。但反过来要提醒:同名追加对 Vary 无害,对 Allow-Origin 就是硬伤——一旦因为手抄或混用 more_set_headers(那个是覆盖语义,和 add_header 行为不一致)导致响应里出现两条 Access-Control-Allow-Origin,浏览器会直接判为非法并拒掉,比缺头还难查。

404 那条验证我认同,再补一句:如果 404 被 error_page 404 /404.html 接管,实际输出的是那个 location 的头,所以要么 404.html 的 location 里也 include,要么干脆别用 error_page 接管 404,光靠 always 就够。

最省心的写法还是老结论:CORS 只写 server 层,location 里一条 add_header 都不出现。