浏览器跨域全解析:同源策略、CORS、预检与 Nginx 代理实战
上周帮一个朋友看他的后台系统前端页面能打开登录按钮点下去控制台一片红满屏都是Access to XMLHttpRequest at http://xxx from origin http://yyy has been blocked by CORS policy。他折腾了一下午改了三版 Nginx 配置重启了七八次服务最后发现问题根本不在 Nginx 上——请求压根没走到网关被前端的代理规则拦下来了。这件事让我觉得有必要把浏览器跨域这套东西从头到尾捋一遍。跨域不是一个配置项它是浏览器同源策略在具体请求上的一次判定结果。同一个报错背后可能是协议不同、端口不同、域名不同也可能是预检请求被拒、凭证模式不匹配、响应头被中间层吃掉。所以9 种解决方法这种说法本质上是 9 种绕开或满足同源策略的路径每一条都有明确的适用边界用错了比不用还麻烦。这篇内容适合正在被跨域卡住的前端、后端、运维也适合想把这套机制一次性搞明白的同学我会从原理讲到落地配置把踩过的坑一并写上。1. 先把这堵墙看清楚跨域到底卡在哪个环节很多人一遇到跨域就去搜怎么解决结果抄了一段配置贴上去有时候好了有时候还是红的原因就是没搞清到底是谁在拦截。跨域拦截发生在浏览器侧是浏览器主动执行的策略服务端其实早就把数据返回了只是浏览器读到响应头之后觉得这个响应不该给这个页面的 JS 看于是把响应体丢掉同时在控制台抛一个错误。理解这一点非常关键你在后端打断点能看到请求进来了、数据出去了但前端就是拿不到这不是后端的问题。1.1 同源策略的三个判定维度同源的定义很干脆协议、域名、端口三者完全一致才算同源任何一项不同都算跨域。注意这里有个容易踩的点域名比较是字符串比较不做 IP 归一化。也就是说http://localhost:8080和http://127.0.0.1:8080在你眼里是同一台机器在浏览器眼里是两个完全不同的源。我自己就被这个坑过一次本地调接口一直通同事拉下来代码死活不行最后发现他习惯用127.0.0.1打开页面而我用的是localhost。端口这一项也有个隐藏细节协议的默认端口会被省略http://a.com等价于http://a.com:80https://a.com等价于https://a.com:443但http://a.com:80和https://a.com:443依然跨域因为协议不同。子域名同样算跨域api.a.com和www.a.com是两个源这一点在做微服务拆分的时候特别容易出现——主站和接口站分到不同二级域名前端一上线就全红。还有一个常被忽略的维度Cookie 的作用域是按域划分的不是按源。这解释了为什么跨域请求默认不带 Cookie即使你把Access-Control-Allow-Origin配对了凭证还是丢的因为还有一道withCredentials和Access-Control-Allow-Credentials的校验在等着。1.2 简单请求与预检请求的分界线浏览器不是对每个跨域请求都发预检的它先给请求分个类。满足下面全部条件的叫简单请求直接发出去不回预检方法是GET、HEAD、POST三者之一请求头只用了Accept、Accept-Language、Content-Language、Content-Type这几个安全头Content-Type的值只能是text/plain、multipart/form-data、application/x-www-form-urlencoded。只要有一条不满足比如你用了PUT、DELETE或者Content-Type: application/json又或者加了个自定义头Authorization浏览器就会先发一个OPTIONS请求去问服务器我能不能这么干。这个OPTIONS就是预检请求它不带业务数据只带Access-Control-Request-Method和Access-Control-Request-Headers两个头服务器必须正确回应它真正的 POST 才会被发出去。提示预检结果会被缓存缓存时长由Access-Control-Max-Age决定默认各家浏览器不一样Chrome 大概 5 秒Firefox 上限 24 小时。这就解释了为什么改完配置之后有时候刷新一下就好了有时候要等一会儿——不是配置没生效是缓存还没过期。理解了这个分界线后面 9 种方案就好对号入座了有些方案是让请求变成不跨域有些是让服务端正确回应预检有些则是干脆绕开浏览器的这套判定。2. 九种解法逐个拆解下面这 9 种方案我按从老到新、从土到稳的顺序排不是说后面的就一定比前面的好每种的适用场景我在最后一节做了对照表。先说清楚一点没有任何一种方案是万能的选择的关键在于你能控制哪一端。能改后端就改后端能改网关就改网关只能改前端那就走代理什么都改不了只能换思路。2.1 方法一JSONP——最老的那把钥匙JSONP 的思路是钻了一个历史空子script标签的src不受同源策略限制可以加载任意域名的 JS 文件。所以服务端把数据包成一个函数调用返回前端提前定义好这个函数脚本一加载就自动执行了。前端写法大致是这样function handleData(res) { console.log(拿到数据了, res); } const script document.createElement(script); script.src http://api.a.com/user?callbackhandleData; document.body.appendChild(script);服务端返回的内容要长这样注意Content-Type必须是application/javascripthandleData({id: 1, name: 张三});JSONP 的硬伤有三个而且是致命的。第一只能发 GET 请求因为script加载天然就是 GET你想传个 JSON body 是做不到的。第二没有任何错误处理能力脚本加载失败只会在控制台报个语法错误或者网络错误你拿不到状态码没法区分是 404 还是 500。第三安全性很成问题服务端返回的内容会被当成 JS 直接执行如果这个接口被第三方控制等于把任意代码执行的权限交了出去。注意JSONP 在现代项目里基本不该用了。但如果你的接口要对接一些老系统或者面试被问到还是得知道它是怎么回事特别是它的回调参数名不一定要叫callback很多老框架用的是jsoncallback或者cb对接之前一定要问清楚。2.2 方法二CORS 响应头配置——现在的主力方案CORS 是 W3C 的标准方案核心思想是服务端通过响应头告诉浏览器这个源我允许。它不需要前端做任何特殊处理前端还是正常发fetch或者axios只要服务端把头发对了浏览器自然就放行。响应头里最关键的是Access-Control-Allow-Origin它的值可以是一个具体的源也可以是*。这里有个必须记住的规则如果请求带了凭证Cookie、HTTP 认证这个头就不能是*必须回显具体的源。很多人配*之后发现登录态丢了就是这个原因。Access-Control-Allow-Origin: https://www.a.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With Access-Control-Expose-Headers: X-Total-Count Access-Control-Max-Age: 86400几个头的含义得说透。Allow-Methods和Allow-Headers是给预检请求看的值必须覆盖你实际用到的所有方法和自定义头漏一个都会被拒。Expose-Headers决定了前端 JS 能读到哪些响应头默认只能读到Cache-Control、Content-Language、Content-Type等几个基础头如果你想在前端读X-Total-Count做分页就必须显式暴露出来这个坑我在做分页表格的时候踩过后端明明返回了前端response.headers里就是没有。后端框架的配置方式不太一样Node/Express 可以用中间件app.use((req, res, next) { const origin req.headers.origin; const allowList [https://www.a.com, https://m.a.com]; if (allowList.includes(origin)) { res.setHeader(Access-Control-Allow-Origin, origin); } res.setHeader(Access-Control-Allow-Credentials, true); res.setHeader(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type,Authorization); if (req.method OPTIONS) { return res.sendStatus(204); } next(); });注意OPTIONS 请求必须在业务逻辑之前被拦截并直接返回 204不能让它走到需要鉴权的中间件里。我见过一个项目鉴权中间件放在最前面预检请求没带 token直接被 401 拒掉然后真正的 POST 就没发出去前端看到的错误信息是预检请求响应无效排查了半天。2.3 方法三开发环境代理——本地调试的标配本地开发的时候前端跑在localhost:8080后端跑在localhost:3000端口不同就是跨域。这时候最舒服的做法是让前端开发服务器转发请求让浏览器以为自己在访问同源接口。Vue CLI 的vue.config.js里这么写module.exports { devServer: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true, pathRewrite: { ^/api: } } } } };Vite 的写法略有不同但思路一样export default { server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } };changeOrigin: true这个参数一定要加它的作用是让代理服务器在转发时把Host头改成目标地址的域名。有些后端框架会根据Host头做校验或者生成回调地址不改的话会出各种奇怪的问题。pathRewrite则用来去掉前缀让后端收到的还是原本的路径。这里有个很多人问的问题配了代理之后怎么拿到自己的真实请求地址答案是在浏览器控制台里看到的http://localhost:8080/api/user是发给代理的代理再转给http://localhost:3000/user真实的第三方地址前端根本看不到。如果你需要知道最终转发的地址只能去看代理的日志或者在后端打印req.originalUrl。这也是为什么代理方案只适合开发环境——它掩盖了真实的请求链路线上出了问题没法排查。2.4 方法四Nginx 反向代理——生产环境的常规选择线上没有devServer代理这活儿交给 Nginx。核心就是配一个location把匹配到的请求转发到后端并且保持路径一致让浏览器认为前后端同源。server { listen 80; server_name www.a.com; location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:3000/; 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://backend:3000/会做路径替换/api/user变成/user写成http://backend:3000不做替换后端收到的还是/api/user。这个细节我在给一个团队做迁移的时候专门强调过他们的后端接口路径本来就带/api前缀结果配了斜杠之后全部 404找了一小时才想起来是这里。X-Forwarded-For和X-Forwarded-Proto这两个头也建议加上。前者用来让后端拿到客户端真实 IP否则日志里全是 Nginx 的地址后者让后端知道用户实际用的是 http 还是 https涉及生成回调链接、判断安全 Cookie 的时候会用到。2.5 方法五同源部署——让跨域从根上不存在这一条严格说不是解决跨域而是消除跨域。把前端静态资源和后端接口挂在同一个域名、同一个端口下浏览器压根不会触发同源检查什么 CORS、什么预检统统不需要。具体做法有两种。一种是后端直接托管前端打包产物比如 Spring Boot 把dist目录配置成静态资源目录Node 的 Express 用express.static指向dist。另一种是 Nginx 同时承担静态资源服务和接口转发就是我上面那个配置——location /给前端location /api/给后端。提示同源部署还有个附带好处Cookie 的SameSite属性可以设成Lax甚至Strict安全性比跨域场景下的None高不少。如果你的项目对安全有要求能从架构上做成同源就不要偷懒走 CORS。这种方式最适合中小项目一套 Nginx 配置全搞定。但它也有代价前后端必须一起发版前端改了 Nginx 里的路径规则后端也得跟着调。如果团队是前后端完全分离、各自独立发布的模式这个方案的协调成本会比较高。2.6 方法六postMessage——窗口之间的通信通道前面几种方案都在解决页面和接口的跨域但还有一种场景是页面和页面之间的跨域最典型的是父页面嵌入了一个第三方域名的 iframe需要互相传数据。这时候postMessage就是标准答案。父页面向 iframe 发消息const iframe document.getElementById(child); iframe.contentWindow.postMessage( { type: userInfo, data: { id: 1 } }, https://child.a.com );iframe 内部接收并回消息window.addEventListener(message, (event) { if (event.origin ! https://parent.a.com) return; if (event.data.type userInfo) { console.log(收到, event.data.data); event.source.postMessage({ type: ack }, event.origin); } });这里最重要的就是必须校验event.origin。message事件任何窗口都能发过来如果你不判断来源第三方页面可以给你的页面发任意消息诱导你的代码执行危险操作。我在别人的项目里见过一段代码收到消息直接eval(event.data)这等于开了一个远程代码执行的口子。注意第二个参数targetOrigin千万别写成*。写*意味着消息可以被发送到任意域名的窗口如果 iframe 中途被跳转到恶意页面你的数据就泄露出去了。一定要写明确的完整源。2.7 方法七WebSocket——协议层面不受同源限制WebSocket 是个特殊存在。它的握手过程是一个 HTTP 请求但这个请求不适用同源策略浏览器不会因为源不同就拦截连接服务端也不会自动检查来源。也就是说ws://a.com的页面可以随意连接ws://b.com的服务不需要任何 CORS 头。这听起来很方便但恰恰意味着安全责任全部落在服务端。因为谁都能连你的 WebSocket你必须自己校验。常用的做法是在握手阶段检查Origin请求头const WebSocket require(ws); const wss new WebSocket.Server({ noServer: true }); server.on(upgrade, (req, socket, head) { const origin req.headers.origin; const allowList [https://www.a.com]; if (!allowList.includes(origin)) { socket.write(HTTP/1.1 403 Forbidden\r\n\r\n); socket.destroy(); return; } wss.handleUpgrade(req, socket, head, (ws) { wss.emit(connection, ws, req); }); });同时还得做身份鉴权通常是在连接 URL 上带一个短时效的 token服务端验证通过才建立连接。我见过一些项目直接用 WebSocket 传敏感数据却不做任何校验这就相当于把一个没有门锁的接口挂在公网上。WebSocket 适合的场景是实时推送、协同编辑、在线聊天这类需要长连接和双向通信的业务。只是为了传个表单数据就用它属于杀鸡用牛刀复杂度反而上去了。2.8 方法八document.domain——主域相同时的降级手法当两个页面来自同一个主域的不同子域比如a.example.com和b.example.com可以通过把双方的document.domain都设成example.com来让它们看起来同源从而互相访问 DOM 或者共享 Cookie。// 两个页面都执行这一行 document.domain example.com;这个方案现在已经被废弃了。Chrome 从 115 版本开始如果响应头带Origin-Agent-Cluster: ?1设置document.domain会直接报错因为这种写法会破坏源隔离带来的安全边界。而且它只能解决相同主域的情况主域不同就没戏。注意新项目绝对不要用这个方案。维护老系统的时候如果看到这行代码先确认一下 Chrome 版本和响应头很可能升级之后就直接挂了。现在推荐的做法是postMessage加明确的源校验或者干脆把子域合并成一个源。2.9 方法九服务端转发与网关聚合最后一种是彻底绕开浏览器限制的思路既然浏览器不让我跨域那我让自己的后端去请求第三方接口前端只跟自己的后端打交道自然就不跨域了。用 Node 做一个很简单的转发const express require(express); const axios require(axios); const app express(); app.get(/api/weather, async (req, res) { try { const resp await axios.get(https://third-party.example.com/weather, { params: { city: req.query.city }, timeout: 5000 }); res.json(resp.data); } catch (err) { res.status(502).json({ message: 上游服务异常 }); } });这种方式的好处是前端调用路径统一不用关心第三方接口的域名坏处是后端多了一层转发接口耗时增加而且必须做好超时和错误处理否则第三方接口一慢你的后端线程全被占满。如果公司有统一的 API 网关那就把这件事交给网关做。网关可以根据路径规则把/third/weather路由到第三方同时统一处理鉴权、限流、日志。这种方式在微服务架构里很常见前端只需要知道一个网关地址就够了。3. 从本地开发到线上部署的完整实操链路前面讲了 9 种方法但实际项目里你往往不是只用一种而是组合使用。这一节我把一个典型 Vue Node 项目从本地开发到上线部署的完整链路走一遍把每一步为什么这么做讲清楚。3.1 本地开发阶段的配置顺序第一步先确认你能改哪一端。如果后端就在同一台机器上跑你其实有两条路让后端加 CORS 头或者让前端配代理。我的建议是优先配代理原因是 CORS 头一旦加上很容易忘记在生产环境收窄白名单留一个*在那里等保测评的时候会被挑出来。代理只在开发服务器生效打包之后自动失效不会有这个隐患。第二步配置路径前缀。约定所有接口以/api开头前端请求统一写相对路径const service axios.create({ baseURL: /api, timeout: 10000 });用相对路径而不是绝对地址好处是开发环境和生产环境可以复用同一份代码。开发环境/api被 devServer 拦下来转发生产环境/api被 Nginx 拦下来转发。如果你写死了http://localhost:3000打包上线之后必然要改代码一不小心就漏了。第三步处理凭证。如果需要带 Cookie:withCredentials必须设成trueconst service axios.create({ baseURL: /api, timeout: 10000, withCredentials: true });同时后端要配Access-Control-Allow-Credentials: true并且Allow-Origin不能用*。这三处必须同时对少一处登录态就丢。3.2 打包部署后接口 404 的两种典型成因项目上线之后最常见的现象是页面能打开接口全 404。这种情况我遇到过很多次成因基本就两类。第一类是代理规则没写到 Nginx 里。开发环境靠devServer.proxy转发打包之后devServer不存在了必须由 Nginx 接手。很多人以为打包会把代理配置一起打进去实际上vue.config.js里的devServer只在开发期生效vite.config.js里的server.proxy同理。所以上线前一定要检查 Nginx 配置里有没有对应的location。第二类是路径前缀被吃掉了。比如前端请求/api/userNginx 配的是proxy_pass http://backend:3000/末尾带斜杠那么转发过去就变成了/user而后端的路由是/api/user自然 404。解决方式有两个把proxy_pass末尾的斜杠删掉或者在pathRewrite里把前缀去掉两边保持一致就行关键是前后端要约定好前缀归谁处理。提示排查这类问题最快的办法是看 Nginx 的 access log里面会打印实际的请求路径。tail -f /var/log/nginx/access.log然后刷新页面看请求打到哪了、返回什么状态码一目了然。比在前端反复改配置快得多。还有一个容易忽略的点如果前端用的是 history 模式路由Nginx 里必须加try_files $uri $uri/ /index.html;。否则用户刷新www.a.com/user/123这种地址Nginx 会去找真实的文件路径找不到就 404。这个跟跨域没关系但经常和跨域问题一起出现容易被混为一谈。3.3 生产环境的最终形态一个稳定运行的生产环境通常是这样的形态Nginx 监听 443 端口证书配好location /指向前端静态目录location /api/转发到后端服务后端服务只在内网监听不暴露公网端口。这样前端和后端在浏览器看来是同一个源跨域问题从根本上不存在。如果架构上确实需要跨域比如前端部署在对象存储、后端在另一台服务器那就老老实实配 CORS。这时候要注意三点白名单用具体的域名列表而不是*Max-Age设大一点减少预检次数Expose-Headers把需要前端读取的自定义头都列上。这三条做好了基本不会出问题。4. 报错信息对照表与排查手法跨域的报错信息其实很明确浏览器会告诉你具体是哪一条规则没满足。难点在于报错文本比较长很多人扫一眼就去搜解决方案其实里面已经把答案写出来了。4.1 常见控制台报错逐条翻译报错关键词真实含义处理方向No Access-Control-Allow-Origin header is present响应里根本没有这个头服务端加 CORS 配置或检查中间件是否生效has been blocked by CORS policy: The Access-Control-Allow-Origin header contains multiple values这个头出现了两次检查 Nginx 和业务代码是不是都加了一遍credential mode is include but the Access-Control-Allow-Origin header is *带凭证时用了通配符把*换成具体源并回显Request header field authorization is not allowed自定义头没在允许列表里在Allow-Headers里补上Method PUT is not allowed方法没在允许列表里在Allow-Methods里补上Response to preflight request doesnt pass access control check预检请求本身失败了检查 OPTIONS 请求是否被鉴权中间件拦截net::ERR_CONNECTION_REFUSED压根没连上这不是跨域问题是服务没起来或端口不对这张表里最后一行特别值得强调。很多问题看起来像跨域其实是服务根本没启动、端口写错了、或者防火墙拦了。请求发不出去的时候浏览器不会给你 CORS 相关的提示只有一个连接错误。所以排查的第一步永远是确认目标服务是否可达别一上来就改 CORS 配置。4.2 排查顺序从浏览器到网关逐层定位我自己的排查顺序是这样的从外往里一层层剥。第一步打开浏览器开发者工具的 Network 面板找到那条失败的请求看它的Request URL和Status。如果是(failed)或者net::开头的错误说明请求根本没到服务端检查地址和网络。如果是 200 但带 CORS 报错说明服务端返回了只是头不对。第二步看有没有OPTIONS请求。如果只有一条失败的请求没有OPTIONS说明是简单请求问题出在响应头缺失。如果有OPTIONS且它失败了那问题就在预检配置上。第三步点开请求的Headers标签看 Response Headers 里到底有没有Access-Control-Allow-Origin值是什么。这一步能直接定位问题。如果没有这个头说明你的 CORS 中间件没生效如果有两个说明重复添加了。第四步如果浏览器里看不出问题用curl直接打服务端绕过浏览器的所有限制curl -i -X OPTIONS http://backend:3000/api/user \ -H Origin: https://www.a.com \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: content-type,authorization看返回的响应头。如果curl能拿到正确的 CORS 头但浏览器还是报错那问题一定在浏览器和你们服务之间的中间层上比如 CDN、负载均衡、WAF。4.3 几个容易被忽略的坑坑一响应头被中间层覆盖。有些 CDN 或者网关会重写响应头把你后端设的Access-Control-Allow-Origin改成自己的值或者干脆删掉。我就遇到过这种情况直连后端一切正常走域名就报错最后发现是 CDN 的响应头改写规则在作怪。坑二错误响应没有 CORS 头。很多框架的 CORS 中间件只对正常响应加头一旦业务代码抛异常走了全局异常处理器头就没了。结果就是接口正常时能调通报错时浏览器提示的是 CORS 错误把真正的业务错误掩盖了。解决方式是在异常处理器里也加上 CORS 头或者把 CORS 中间件放在最外层。坑三Cookie 的 SameSite 属性。跨域带 Cookie 时SameSite必须是None而且必须同时设Secure也就是说必须走 HTTPS。如果你在 http 环境下测跨域 Cookie浏览器会直接拒绝写入。这个坑在本地测试第三方登录回调的时候特别常见。坑四Max-Age设得太大。有人为了减少预检请求把Access-Control-Max-Age设成 86400 甚至更大。结果后来改了允许的方法或头前端一直不生效因为预检结果被缓存了。调试阶段建议设小一点稳定之后再调大。坑五多个域名共用一个后端。如果有多个前端域名要调同一个后端回显的时候要注意Access-Control-Allow-Origin只能填一个值。这时候要么用白名单动态回显要么用 Nginx 做一层分发让每个域名走各自的location。5. 九种方案的选型对照与我的实际取舍讲完了原理和实操最后一节聊聊怎么选。我把这 9 种方案的关键特性整理成一张表方便你对号入座。5.1 一张表看清每种方案的适用边界方案适用场景能否带 Cookie是否改动后端主要缺点JSONP老系统对接、只读接口不能需要只支持 GET无错误处理安全性差CORS前后端分离的正式方案能需要配置项多容易漏开发代理本地调试能不需要仅开发环境有效Nginx 反代生产环境常规做法能不需要需要运维配合同源部署中小项目、单体架构能需要前后端发版耦合postMessage页面与 iframe 通信不涉及不需要需严格校验来源WebSocket实时推送、双向通信不涉及需要安全责任全在服务端document.domain老系统遗留能不需要已被废弃新项目禁用服务端转发调第三方接口能需要增加一层转发开销看完这张表其实结论已经很清楚了。新项目要做前后端分离就走 CORS能做成同源就做成同源本地开发一律用代理跨窗口通信用 postMessage实时业务用 WebSocket调第三方接口用服务端转发。JSONP 和document.domain留着看老代码用别在新代码里出现。5.2 我自己常用的一套组合以我现在手上的项目为例前端是 Vue后端是 Node本地开发环境用 Vite 的server.proxy把/api转发到localhost:3000前端代码里所有请求都用相对路径不带任何域名。生产环境用 Nginx 部署静态资源和接口在同一个域名下location /api/转发到内网的 Node 服务浏览器看来完全是同源一行 CORS 配置都不需要写。需要调第三方接口的时候我在 Node 层加一个转发路由前端调自己的/api/proxy/xxx由后端去请求第三方。这样第三方接口的密钥不会暴露在前端超时和重试也能统一控制。唯一需要注意的是加上超时和熔断别因为第三方接口慢把整个服务拖垮。如果哪天因为业务需要前端确实要部署到别的域名上那我会在 Nginx 层面加 CORS 头用一个白名单变量动态回显Origin同时把Max-Age设成 600 秒方便调试。这样即使出问题十分钟之后缓存也会过期改了配置就能看到效果。提示CORS 配置上线之前一定要用真实的域名测一遍完整的预检流程别只在 Postman 里测。Postman 不执行同源策略你在里面调一百遍都是通的一放到浏览器里就全红。这是我见过最多的明明我测过了的翻车现场。最后再分享一个小经验如果你的项目里出现了有时候能调通、有时候不行这种玄学现象八成是预检缓存和某个中间层的头重写共同作用的结果。这时候别急着改代码先打开无痕窗口复现一遍无痕模式不共享缓存能最快确认是不是缓存问题。确认之后再逐层看头比盲目改配置效率高得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →