为什么 Apifox 能调通,浏览器却报跨域?一次“CORS Error + 403”联调排坑实录

在前后端联调中,很多开发者都遇到过这样一个让人抓狂的场景:同一个接口,在 Apifox 或 Postman 里请求稳如老狗,返回 200 OK;但只要放到前端页面里用浏览器发起请求,控制台立马飘红报错。

打开浏览器的开发者工具(F12 -> Network),还会看到一个诡异的现象:明明代码里只发了一次请求,网络面板里却出现了两条记录 ------第一条显示 CORS error,紧接着第二条显示 403 Forbidden。

本文将通过一次真实的联调案例,彻底扒开浏览器同源策略、OPTIONS 预检请求以及这套"CORS + 403"组合拳背后的底层逻辑,并给出生产环境最稳妥的解决方案。

案发现场还原

为方便说明,我们将本次联调涉及的两个服务分别用代号表示:

  • 前端服务 A(前端页面地址): http://[前端服务器IP]:9006

  • 后端服务 B(目标接口地址): http://[后端服务器IP]:5050/ai/workflow/token/execute

  • 现象对比:

    • Apifox 测试: 直接请求后端服务 B 的目标接口,携带 Token 和 JSON 参数,正常返回数据。

    • 浏览器测试: 在前端服务 A(9006 端口)的页面中向后端服务 B(5050 端口)发起请求,Network 面板先后出现两条报错记录,第一条状态为 CORS error,第二条状态为 403。

核心疑问一:为什么 Apifox 可以,浏览器却不行?

很多人误以为"跨域报错"是后端服务不通,或者误以为只有 Server(服务端)才能绕过跨域。其实跨域(CORS)和 OPTIONS 预检请求完完全全是浏览器给自己内部运行的前端 JS 代码戴上的"紧箍咒" ,并不是 TCP/IP 或 HTTP 网络协议本身的限制。

  1. 浏览器的"历史包袱"(沙盒环境限制) 用户的浏览器里通常保存着各个网站的 Cookie 和登录凭证。如果没有同源策略(Same-Origin Policy)限制,当你不小心点开一个恶意网站时,该网站的 JS 脚本就能在后台偷偷用你的浏览器向公司内网或网银接口发送请求。因此浏览器立下规矩:只要当前网页所在的源(协议 + IP/域名 + 端口)和目标接口不一致,就必须先经过严格的安全审查。

  2. Apifox 的"特权"(脱离浏览器沙盒的原生客户端) 常有人疑惑:"难道 Apifox、Postman、curl 或是后端代码里的 HttpClient 在本地都是独立的 Server 吗?" 其实它们并不是 Server(被动监听端口的服务端),而是脱离了浏览器沙盒的独立 Client(原生网络客户端 / 进程) 。只要不是运行在"浏览器沙盒环境"里的前端网页代码,任何直接调用操作系统底层网络接口(Socket)发出的请求,都没有跨域限制:

    • curl、Apifox 桌面端、Postman: 它们和微信、迅雷一样,是安装在操作系统上的普通客户端软件(桌面端通常基于 Electron,内置 Node.js 环境)。当你点击"发送"时,它们直接调用操作系统的网络接口建立 TCP 连接发包,既不在浏览器沙盒里运行,也不存在泄露浏览器各站 Cookie 的风险,自然无需理会同源策略。 (注:如果你使用的是 Apifox/Postman 的网页版,网页本身同样受浏览器跨域限制,因此它们会要求你在电脑上安装一个本地 Agent 插件,由网页把请求发给本地插件,再由插件调用系统网络发包来绕过限制。)

    • 后端代码里的 HttpClient: 比如 Spring Boot 或 FastAPI 跑起来监听 5050 端口时,它整体是一个 Server;但当代码内部使用 HttpClient、OkHttp 或 Python requests 去请求其他接口时,它在那次通信中扮演的角色也是纯粹的 Client,直接走操作系统网络发包,同样不受浏览器规则约束。

简单来说:Apifox 是直接走操作系统高速路的"专车",直奔后端;而浏览器是个极其谨慎的"保安",在放行网页里的请求前,非要自己先派个探子去问问后端同不同意。

核心疑问二:为什么一条请求变成了两条?为什么先显示 CORS,后显示 403?

在浏览器的 Network 面板中,看到第一行是 CORS error,第二行是 403,很容易让人误以为"是先发生了跨域错误,才导致的 403"。

实际上,Network 面板默认是按"请求发起的先后顺序"排列的,而不是按"报错产生的时间"排列的。 这两条请求在几毫秒内发生的真实剧本如下:

  1. 第 1 步(登记业务请求): 前端 JS 执行了 axios.post 或 fetch。浏览器立刻在 Network 面板的第 1 行登记了这个 POST 请求。但注意,此时浏览器发现从前端服务 A 请求后端服务 B 属于跨源请求,且携带了自定义 Header(如 Authorization)或 application/json,属于"非简单请求"。于是浏览器把第 1 行的 POST 请求暂时挂起(Pending),并没有真正发出去。

  2. 第 2 步(派生探路请求): 浏览器紧接着在第 2 行自动生成并发送了一条 OPTIONS 预检请求(Preflight Request) 去给后端服务 B 探路。

  3. 第 3 步(探路遭到 403 拒绝): 后端服务 B 收到了第 2 行的 OPTIONS 请求,直接回怼了一个 403 Forbidden(第 2 行变红,状态码定格为 403)。

  4. 第 4 步(业务请求被连带掐死): 浏览器看到探路的 OPTIONS 请求拿到了 403(没有拿到预期的 200/204 和允许跨域的响应头),判定"探路失败,对方不允许跨域",于是立刻回头把第 1 行还在排队等待的 POST 请求直接掐断,并在状态栏填上 CORS error。

所以真相是:第 2 行的 OPTIONS 请求报 403 是"因",第 1 行的业务请求报 CORS error 是"果"。

核心疑问三:为什么 OPTIONS 请求会报 403?

既然接口本身是通的,为什么浏览器派出去的 OPTIONS 探路请求会被后端服务 B 打回 403?通常有两个罪魁祸首:

  • 原因 A:权限拦截器误杀(最常见) W3C 标准明确规定,浏览器发送 OPTIONS 预检请求时,绝对不会携带业务自定义的 Authorization(Token)认证头 。如果后端服务 B 的登录拦截器(如 Spring Security、Shiro 或自定义网关)一视同仁地拦截所有请求,看到 OPTIONS 没带 Token,就会当成未授权访问,直接拦截并返回 401 或 403。

  • 原因 B:后端 CORS 白名单拦截 浏览器发请求时会强制塞入一个请求头 Origin: http://[前端服务器IP]:9006(而 Apifox 默认不带这个头)。如果后端框架配置了 CorsFilter,但允许的域名白名单里没有包含前端服务 A 的地址,后端的跨域过滤器就会直接拒绝该请求,返回 403 Forbidden(响应体里通常会写着 Invalid CORS request)。

小技巧:在 Apifox 中把请求方式改为 OPTIONS,并在 Header 中手动加上 Origin: http://[前端服务器IP]:9006,你就能在 Apifox 里完美复现出一模一样的 403 报错。

终极解决方案

搞清楚了原理,解决问题就变得非常清晰。工程实践中主要有两条路径:要么在前端侧用代理"绕过"浏览器的跨域判定,要么在后端侧给 OPTIONS 请求"开绿灯"。

方案一:Nginx 反向代理(最推荐,生产环境标准做法)

既然跨域是浏览器沙盒的限制,而服务端进程(Nginx)向后端发起的转发请求属于操作系统层面的客户端通信、没有跨域限制,我们只需要让前端页面请求自己的同源地址(前端服务 A),再由前端服务器的 Nginx 转发给目标接口(后端服务 B)即可。这样浏览器认为没有跨域,连 OPTIONS 预检请求都不会发送。

  1. 修改前端请求地址 将前端代码中的请求地址从 http://[后端服务器IP]:5050/ai/workflow/token/execute 改为同源路径或相对路径: /ai/workflow/token/execute

  2. 配置前端服务 A 所在服务器的 Nginx 在监听 9006 端口的 server 块中添加转发规则:

    Nginx 复制代码
    server {
        listen 9006;
        server_name [前端服务器IP或域名];
    
        location /ai/ {
            proxy_pass http://[后端服务器IP]:5050/ai/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
            # 若涉及 AI 接口流式输出(SSE),建议关闭缓冲防止卡顿
            proxy_buffering off;
            proxy_read_timeout 300s;
        }
    }
  3. 重载生效 执行 nginx -t && nginx -s reload。此时再看浏览器 Network 面板,只会发出一条干净的 POST 请求,状态码直接变为 200。

方案二:在后端网关或代码中放行 OPTIONS 并配置 CORS

如果不使用前端反向代理,让浏览器直接跨域请求后端服务 B,则必须在后端同时做到两件事:返回 CORS 允许响应头 + 无条件放行 OPTIONS 请求。

  • 做法 1:在后端服务 B 的 Nginx 网关层拦截处理

    Nginx 复制代码
    location / {
        add_header 'Access-Control-Allow-Origin' 'http://[前端服务器IP]:9006' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With, token' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
    
        # 核心:遇到 OPTIONS 预检请求直接返回 204,不让它进入后端的 Token 校验逻辑
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    
        proxy_pass http://127.0.0.1:5050;
    }
  • 做法 2:在后端服务 B 的代码中配置(以 Spring Boot 为例) 不仅要配置全局 CorsFilter 允许 http://[前端服务器IP]:9006,还必须在自定义 Token 拦截器的最外层放行 OPTIONS:

    Java 复制代码
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 预检请求不携带 Token,直接放行
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            return true;
        }
        // 下面再走正常的 Token 校验逻辑...
    }

结语

下次再遇到"Apifox 能通、浏览器报错,且伴随着一条 CORS 和一条 403"时,不用再怀疑人生:

  • Apifox 能通,因为它作为脱离浏览器沙盒的原生 Client,直接走操作系统网络发包,证明后端服务 B 的业务接口本身没问题;

  • 浏览器报 CORS + 403 ,说明浏览器沙盒自动发出的无 Token OPTIONS 预检请求撞上了后端的鉴权墙或跨域白名单墙。

优先采用 Nginx 反向代理 让前后端在浏览器视角下保持"同源",既能彻底消灭多余的 OPTIONS 预检请求、提升接口首包响应速度,也能避免在后端安全框架中到处打补丁,是最优雅的工程实践。

相关推荐
2603_965896622 小时前
JavaScript 原型与原型链
开发语言·前端·javascript
Amos_Web2 小时前
通过 Rspack 源码学 Rust(四):用 `Option<T>` 表达“查询可能没有结果”
前端
朋友圈自动点赞工具2 小时前
Ruby方法查找:对象不存方法
开发语言·前端·ruby·科技资讯
晓得迷路了2 小时前
栗子前端技术周刊第 152 期 - Rspack 2.3、TypeScript 7.1 Beta、Bun 1.4.4...
前端·javascript·css
普通人63 小时前
CSS-box-sizing-使用指南
开发语言·前端·css·1024程序员节
风骏时光牛马3 小时前
AI开源工具一站式尝鲜体验
前端
IT_陈寒3 小时前
Python项目实战经验分享:为什么你的多进程日志会丢失?
前端·人工智能·后端
柳杉3 小时前
让 AI 学会审美、建模和剪片:我收藏夹里的 14 个仓库
前端·ai编程·three.js
前端架构师-老李3 小时前
HTML与CSS基础第七课—HTML 中的a标签(跳转链接)
前端·css·html