火狐 CORS 跨域失败排查:预检请求与响应头配置实战
火狐浏览器提示 CORS 未能成功这几个字大概是前后端联调里最容易让人血压升高的一段文案。我做了七八年后端接口跨域的问题处理过几百次最典型的一个场景就是同一个接口在 Chrome 里跑得顺顺当当换到火狐浏览器直接红字一片控制台只丢下一句「已拦截跨源请求……原因CORS 请求未能成功」连到底缺哪个响应头都不告诉你。这时候很多人第一反应是去翻火狐的设置或者在代码里到处加 header越加越乱。其实火狐这条提示之所以让人抓狂是因为它把好几类完全不同的故障合并成了一句话。跨域失败可能是网络层没通、可能是预检请求被鉴权挡了、可能是响应头重复了、可能是Access-Control-Allow-Origin和credentials打架但火狐只告诉你「没成功」。Chrome 至少在多数情况下会把缺失的头名点出来火狐则更依赖你自己去分层排查。下面这篇东西我打算按真实的排查顺序来讲先教你怎么读出火狐那句含糊提示背后的真实故障点再把同源策略和预检请求的判定链路捋清楚然后把「反射 Origin credentialstrue」这个最容易翻车的组合拆开讲透接着给出 Nginx、FastAPI、Express、Spring 四套可以直接抄的配置最后补上火狐独占的几个干扰项——预检缓存、增强跟踪保护、扩展拦截以及那个容易被搜错的「北斗 CORS」。不管你是刚接触跨域的前端新人还是被运维和网关折腾过的老后端应该都能找到对得上号的那一段。1. 火狐为什么只给你一句CORS 请求未能成功先定位它在报哪一步的错火狐的 CORS 报错文案比 Chrome 粗这不是它做得差而是它把好几类故障合并到了一条提示里。Chrome 经常能告诉你net::ERR_FAILED或者明确点出No Access-Control-Allow-Origin header is present火狐很多时候只给一句Reason: CORS request did not succeed翻译过来就是「这次跨域请求没成功」约等于什么都没说。所以第一步不是急着改配置而是先把这句话「翻译」成具体的故障位置。我的经验是火狐的 CORS 提示基本可以归成三类每一类指向的排查方向完全不同。如果你把这三类搞混了就很容易出现「改了半小时响应头结果发现是端口没开」这种浪费时间的操作。1.1 三类报错文案分别对应什么故障火狐控制台文案真实含义优先排查方向CORS request did not succeed请求在传输层就没走通或者预检返回了非 2xx域名解析、端口连通性、证书、预检状态码CORS preflight response did not succeed预检请求OPTIONS本身失败网关是否放行 OPTIONS、鉴权是否拦截、是否重定向CORS header Access-Control-Allow-Origin missing请求通了但响应里压根没有这个头服务端或入口层漏配CORS header Access-Control-Allow-Origin does not match xxx请求通了响应头也有但值和当前源对不上白名单里少写了一个域或者多写了协议、端口第一类是坑最多的。CORS request did not succeed里那个 did not succeed 指的是这次网络请求根本没拿到一个正常的响应而不是「响应头不对」。它可能是后端服务没起来、可能是 HTTPS 证书不被信任、可能是内网域名在本地解析不了、也可能是预检请求返回了 500。浏览器在这种情况下不会去检查响应头因为压根没有响应头可查于是统一报成「请求未能成功」。第二类和第三类是「请求通了但头不对」这类相对好办。你把响应头打印出来对着白名单一个个比问题基本就浮出来了。1.2 用 Network 面板区分预检挂了还是主请求挂了判断到底是预检失败还是主请求失败最直接的办法是打开 F12 的网络面板把筛选器切到 XHR 或者全部然后勾上「持久日志」刷新页面。如果列表里能看到一条 OPTIONS 请求并且它是红色的或者状态码是 4xx/5xx那问题就在预检如果压根看不到 OPTIONS只有一条标红的业务请求那说明这是简单请求直接被拦或者预检根本没发出去。这里有个小细节很多人不知道火狐在网络面板里对失败的请求显示得比较「含蓄」有时候那一条请求的状态列会直接写「已拦截」而不是具体的状态码。一旦你看到「已拦截」就意味着浏览器根本没拿到响应这时候去纠结响应头是没意义的应该转回去查网络连通性和预检状态码。还有一个我常用的技巧在网络面板里右键那条失败的请求选「复制为 cURL」把命令贴到终端里跑一遍。你会拿到火狐实际发出去的完整请求头包括 Origin、Content-Type、有没有带上 Cookie。这一步能帮你确认前端代码到底加了哪些头很多「我以为我加了」的问题在这一步就露馅了。最后提醒一句火狐的控制台和网络面板偶尔会因为缓存显示旧数据尤其在你反复刷新调试的时候。养成习惯调试跨域时一律勾上「禁用缓存」能省掉一半的自我怀疑。2. 同源策略与 CORS 的判定链路浏览器到底在哪一环掐断请求要治好跨域得先知道浏览器是怎么判定「同源」的。规则其实很朴素协议、域名、端口三项完全一致才算同源任意一项不同就是跨域。http://localhost:5173和http://127.0.0.1:8000不同源因为它俩域名和端口都不一样http://localhost:5173和https://localhost:5173也不同源因为协议不同。这个规则看起来简单但实际项目里踩坑的地方全在细节上。比如前端跑在localhost后端写成127.0.0.1本地看着都是自己电脑浏览器眼里这是两个完全不同的站点。再比如前端https、后端http这就是混合内容火狐拦得比 Chrome 还严。所以排查跨域时第一件事就是把前端地址和后端地址并排写出来逐项对协议、域名、端口别凭感觉。2.1 简单请求与预检请求的分界线CORS 把请求分成两类这个分类直接决定了你会不会看到 OPTIONS。满足下面全部条件的叫「简单请求」浏览器会直接发出不预检方法只能是 GET、HEAD、POST只能带 Accept、Accept-Language、Content-Language、Content-Type 这几种头如果带 Content-Type值只能是application/x-www-form-urlencoded、multipart/form-data、text/plain之一不能带自定义头。只要有一条不满足浏览器就会先发一条 OPTIONS 预检。现实项目里最常见的触发条件就是Content-Type: application/json——现在前后端传数据基本都是 JSON所以绝大多数接口调用都会触发预检。另外加了Authorization头、加了自定义的X-Token、用了 PUT/DELETE也都会触发预检。这就解释了一个很常见的困惑为什么我明明只改了一行代码之前好好的接口突然就跨域了。多半是因为你把请求从表单提交改成了 JSON 提交或者加了一个自定义请求头请求类型从简单请求变成了需要预检的请求而服务端压根没处理 OPTIONS。2.2 一次带 Cookie 的 POST 到底走了几趟网络我们把这个过程完整走一遍。假设前端这样发请求fetch(https://api.example.com/api/user, { method: POST, credentials: include, headers: { Content-Type: application/json }, body: JSON.stringify({ name: test }) })浏览器实际会做四件事。第一步发一条 OPTIONS 预检带上Origin: https://app.example.com、Access-Control-Request-Method: POST、Access-Control-Request-Headers: content-type。第二步服务端必须返回 2xx并且带上Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers以及Access-Control-Allow-Credentials: true这几个头。第三步只有预检通过了浏览器才会真正发出那条 POST同时带上 Cookie 和 Origin。第四步POST 的响应里同样要带上正确的Access-Control-Allow-Origin和Access-Control-Allow-Credentials: true浏览器才把响应内容交给前端代码。这里有一个非常关键、但很少有人说清楚的点预检请求本身不携带 Cookie 和 Authorization。规范里就是这么定的预检是浏览器替业务请求「问路」不涉及用户凭证。所以如果你的网关或中间件写的是「没有登录态就返回 401」那么 OPTIONS 请求必然被打回来预检失败业务请求根本不会发出去。你在前端看到的就是一句含糊的 CORS 错误而真正的锅在鉴权逻辑上。2.3 预检通过不代表业务请求一定成功很多人以为预检过了就万事大吉其实第四步才是最容易翻车的。因为业务请求带了 Cookie属于「带凭证的请求」浏览器对它的响应头校验更严格。预检那一步的规则和业务请求那一步的规则是不完全一样的这一点下一节会详细展开。所以我排查跨域的顺序固定是先看 OPTIONS 有没有发出去再看 OPTIONS 的响应头最后看 POST 的响应头。三段分开看比笼统地盯着「CORS 报错」要高效得多。3. 反射 Origin credentialstrue带凭证跨域最容易翻车的一处配置如果你在搜索 CORS 配置的时候看到过「反射 origin credentialstrue」这种说法说的就是这一节的内容。这是带 Cookie 的跨域场景里最容易出错、也最容易被框架「兜住」导致你以为没问题的一处配置。3.1 为什么 Allow-Origin 写星号配上 Cookie 就必失败规范里有一条硬性规定当请求带凭证Cookie、HTTP 认证信息、客户端证书时响应头Access-Control-Allow-Origin不能是*必须是具体的一个源。而且必须同时带Access-Control-Allow-Credentials: true。两条缺一条浏览器就直接拦截并且不给你看响应内容。很多人的第一版配置是这样的后端统一回Access-Control-Allow-Origin: *跨域确实是通的因为没带 Cookie。等到要登录了前端把credentials: include打开接口立刻全挂。原因就是星号和 Cookie 不能共存。那正确的做法是什么服务端读请求里的Origin头判断它是不是在白名单里如果在就把这个Origin的值原样写回Access-Control-Allow-Origin。这就是所谓的「反射 Origin」。因为每次请求的源可能不同响应头的值跟着变所以浏览器缓存和中间层缓存必须知道「这个响应的内容跟 Origin 有关」于是还要额外加一个Vary: Origin。3.2 不写 Vary: Origin 会发生什么不写Vary: Origin的后果在本地调试时基本看不出来一上线就容易出玄学问题。假设你的接口在经过一层 CDN 或者共享缓存A 站点先访问缓存里存下了Access-Control-Allow-Origin: https://a.example.com这个响应。接着 B 站点来访问同一个 URL缓存直接把上面那份响应吐给 B。B 浏览器一看允许的源是 A不是自己拦截。于是你得到一个「有时候好、有时候坏换个浏览器又好了」的问题而且极难复现。所以我的习惯是只要用了反射 OriginVary: Origin必加不管现在有没有上 CDN。这条成本几乎为零但省下来的排查时间是以小时计的。3.3 白名单匹配别用模糊包含另外一个坑是白名单的匹配写法。我见过有人用origin.endsWith(example.com)来做判断这个写法能被evil-example.com绕过——因为它也以example.com结尾。正确的后缀匹配至少要带上点号比如判断是否以.example.com结尾并且把根域单独列进去。更省事的做法是维护一个精确的字符串数组用includes做全等比较虽然要多写几行但不会有歧义。还有一点新手经常忽略端口也是源的一部分。https://app.example.com和https://app.example.com:8443是两个不同的源白名单里如果不带端口测试环境的请求就过不去。我把常见的对应关系整理成一张表配置的时候直接照着比前端实际来源允许的 Allow-Origin 值是否匹配https://app.example.comhttps://app.example.com匹配https://app.example.com*不带凭证时匹配带凭证时失败https://app.example.com:8443https://app.example.com不匹配端口不同http://app.example.comhttps://app.example.com不匹配协议不同https://sub.app.example.comhttps://app.example.com不匹配子域不是同源4. 配置落到代码里Nginx、FastAPI、Express、Spring 四套能直接抄的模板前面讲的是原理这一节给的是可以直接拿去改的配置。我的建议是跨域这件事只在一个地方处理要么全在入口层做要么全在应用层做千万别两头都做。两头都做最常见的后果就是响应里出现两个Access-Control-Allow-Origin头浏览器直接判定失败而且报错信息很有迷惑性。4.1 Nginx 入口层的写法在入口层处理的前提是你能管住这个 Nginx。先定义一个白名单映射别偷懒直接回显$http_origin那等于对所有站点敞开map $http_origin $cors_origin { default ; ~^https://app\.example\.com$ $http_origin; ~^https://admin\.example\.com$ $http_origin; } server { listen 443 ssl; server_name api.example.com; location /api/ { # 隐藏后端可能已经加过的跨域头避免重复 proxy_hide_header Access-Control-Allow-Origin; proxy_hide_header Access-Control-Allow-Credentials; if ($request_method OPTIONS) { 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, OPTIONS always; add_header Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With always; add_header Access-Control-Max-Age 600 always; add_header Vary Origin always; return 204; } add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials true always; add_header Vary Origin always; proxy_pass http://backend_upstream; } }几个要点解释一下。map里匹配不上的源变量结果是空字符串而 Nginx 在 add_header 的值为空时不会写入这个头所以非法来源拿不到允许头浏览器自然拦截这个设计很干净。always参数保证即使返回的是 4xx、5xx 也会带上这些头否则接口报错的时候你会同时看到 CORS 报错和业务报错干扰判断。proxy_hide_header是为了防止后端也加了同名头造成重复。4.2 FastAPI 的 CORSMiddlewareFastAPI 用户量很大这块的配置也是最容易出问题的。基础写法from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app FastAPI() app.add_middleware( CORSMiddleware, allow_origins[https://app.example.com, https://admin.example.com], allow_credentialsTrue, allow_methods[GET, POST, PUT, DELETE, OPTIONS], allow_headers[Content-Type, Authorization, X-Requested-With], expose_headers[X-Total-Count], max_age600, )第一个坑是中间件顺序。FastAPI 底层用的 Starletteadd_middleware是往栈顶插的也就是说最后添加的中间件在最外层。如果你的项目里有自定义的鉴权中间件而 CORSMiddleware 加在它前面那鉴权会先跑OPTIONS 请求会被鉴权拦掉返回 401预检直接失败。解决办法是把 CORSMiddleware 放到最后添加让它在最外层OPTIONS 请求能在进入鉴权逻辑之前就被它接住并返回。第二个坑是allow_headers[*]配合allow_credentialsTrue。这里有一个规范层面的细节Access-Control-Allow-Headers的星号通配符只在「不带凭证」的请求里才被当成通配符一旦请求带了凭证星号会被当成一个字面值为*的头名于是浏览器认为你并没有允许content-type预检照样失败。火狐是按规范实现的所以这个坑在火狐上暴露得特别明显Chrome 某些版本反而会「宽容」一点这也是「Chrome 好好的火狐不行」的一个经典来源。稳妥做法是老老实实把需要的头名列出来。第三个坑是expose_headers。默认情况下前端只能读到几个基础响应头自定义的响应头比如分页总数读不到。这个不是跨域失败但经常被误认为是所以顺手提一句。4.3 Express 的 cors 中间件const cors require(cors); const whitelist [https://app.example.com, https://admin.example.com]; app.use(cors({ origin(origin, callback) { if (!origin || whitelist.includes(origin)) { return callback(null, true); } return callback(null, false); }, credentials: true, methods: [GET, POST, PUT, DELETE, OPTIONS], allowedHeaders: [Content-Type, Authorization, X-Requested-With], maxAge: 600 }));这里要注意origin: *和credentials: true不能同时用。在 cors 这个包里如果你写origin: *它会把Access-Control-Allow-Origin设成星号而credentials: true又会让它加上Access-Control-Allow-Credentials: true两个头一起出现就是无效组合浏览器必拦。用上面这种回调函数的形式逐个判断来源最省心。回调里那个!origin的判断是给非浏览器请求用的。用 curl 或者服务端之间调用时请求里没有 Origin 头这种请求不走 CORS 规则直接放行是合理的。4.4 Spring Boot 的两种做法用WebMvcConfigurer的方式Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(https://app.example.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(Content-Type, Authorization) .allowCredentials(true) .maxAge(600); } }Spring 在这里比很多框架都友好如果你写allowedOrigins(*)同时allowCredentials(true)它启动的时候就直接抛异常告诉你配置非法而不是默默运行时失败。我其实挺欣赏这种做法错误暴露在启动阶段比让用户在前端看红字强多了。另一种做法是用CorsFilter适合用了 Spring Security 的项目。这里有个顺序问题自定义的认证过滤器必须在CorsFilter之后执行或者你要在安全配置里显式放行 OPTIONS 请求比如http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, /**).permitAll()。不放行的结果就是预检被 401 拦掉表现和前面 FastAPI 那个坑一模一样。5. OPTIONS 预检被鉴权中间件或重定向拦掉最隐蔽的一类假性 CORS这一类问题我单独拿出来讲因为它太隐蔽了。表面上是 CORS 报错实际上服务端的跨域配置一个字都没错问题出在预检请求在到达跨域处理逻辑之前就被别的环节拦住了。5.1 401 和 403 的预检长什么样前面反复强调过预检请求不带 Cookie 和 Authorization。所以任何「未登录就拒绝」的规则都会把 OPTIONS 挡下来。网关层的 JWT 校验、应用层的登录拦截器、反向转发层配置的鉴权插件都可能造成这个问题。它的表现是你在火狐控制台看到一条CORS preflight response did not succeed或者CORS request did not succeed网络面板里那条 OPTIONS 的状态码是 401 或 403。这时候你去改Access-Control-Allow-Origin是白费劲因为响应头是鉴权模块生成的压根没走到跨域逻辑。处理方法有两种。一种是显式放行在网关和拦截器里统一把 OPTIONS 请求放行让它直接返回 2xx。另一种是让跨域处理模块的位置尽可能靠外在鉴权之前就把它接住并返回这也是为什么我前面建议中间件顺序要调整。两种方式选一种别两套都做容易打架。5.2 尾斜杠重定向把预检带跑偏第二种假性 CORS 是重定向。举个例子后端注册的路由是/api/user/前端请求的是/api/user很多框架会自动做一次 301 或 308 重定向。预检请求遇到重定向浏览器会判定失败——因为跨域预检不允许被重定向。还有一种更常见的站点配了 HTTP 跳 HTTPS但预检请求打到的是 HTTP 地址被 301 跳走同样失败。表现就是本地开发一切正常测试环境一上就报错。排查方法很简单用 curl 直接发一条 OPTIONS看返回码是不是 301/308。如果是说明路由没对齐把前后端的路径统一一下就行。别小看这个尾斜杠我见过团队里因为这个折腾了整整一个下午。5.3 响应里出现两个 Allow-Origin 头第三种情况是响应里出现了重复的Access-Control-Allow-Origin。前面 4.1 节我特意加了proxy_hide_header就是为了防这个。当入口层和应用层同时加了这个头浏览器解析到两个值会认为「这个头的值和期望的不匹配」报错文案通常是does not match或者提示头包含多个值。排查手段就是用 curl 带上-i参数把响应头完整打印出来看一眼。这种问题肉眼一看就明白比在浏览器里猜快得多。除了入口层和应用层打架还有一种情况是某些安全加固组件、WAF、CDN 的默认配置里也带了跨域头这时候要去对应平台的配置里关掉。5.4 把预检单独拿出来测我建议所有做接口的同学都养成一个习惯写完之后直接手搓一条预检请求测一下别等到前端联调才发现问题。curl -i -X OPTIONS https://api.example.com/api/user \ -H Origin: https://app.example.com \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: content-type看返回的状态码是不是 2xx看有没有Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Allow-Credentials。这四条命令十秒钟就能跑完能挡掉后面两小时的扯皮。6. 火狐独占的几个干扰项预检缓存、跟踪保护、扩展与网络认证页到此为止讲的都是通用问题。这一节专门讲火狐上那些「别的浏览器不这样就它这样」的情况。因为标题问的就是火狐这部分反而是最贴题的。6.1 预检缓存最长能存 24 小时Access-Control-Max-Age这个响应头控制预检结果的缓存时间。各家浏览器对这个值的上限不一样火狐的上限是 86400 秒也就是 24 小时Chrome 的上限要短得多。这意味着一个非常坑的场景你上午把服务端配置改对了刷新火狐还是不生效因为火狐还在用几个小时前缓存的那份失败的预检结果。判断方法很简单同一个请求用无痕窗口打开如果无痕里正常、普通窗口报错基本就是缓存的问题。解决办法有三个调试期间把max_age设小一点甚至设成 0刷新时用Ctrl Shift R强制刷新或者在开发者工具的网络面板里勾上「禁用缓存」并保持打开也可以手动清一次浏览器缓存。我的习惯是开发环境max_age直接给 0生产环境给 600 秒左右。开发环境少一层缓存能省掉很多「我改了呀怎么没用」的对话。6.2 增强跟踪保护与 Cookie 隔离会偷走你的凭证火狐默认开启增强型跟踪保护严格模式下会拦截已知的跟踪域名还会对第三方 Cookie 做按站点隔离。这带来的一个后果是你的跨站请求里Cookie 可能压根没带上。表现很有意思。后端因为收不到 Cookie把它当未登录处理返回 401。前端拿到的是一句 CORS 报错——因为响应里没有正确的Access-Control-Allow-Origin很多框架在异常分支不会加跨域头浏览器就把响应屏蔽了。于是你看到的是「跨域失败」真实原因是「Cookie 没带上」。排查手段看地址栏左边有没有盾牌图标点开看看有没有被拦截的记录。临时把保护级别降一档如果问题消失那就确认是这个原因。但不要把降级别当成最终方案正确的做法是前端和后端放在同一个站点下或者把 Cookie 的属性配好。这里必须提一句 Cookie 的属性。如果前端在app.example.com后端在api.other.com那这个 Cookie 属于第三方 Cookie必须设置SameSiteNone; Secure才能跨站发送。少了这两个属性火狐是不会把 Cookie 发出去的。这个问题和 CORS 在症状上高度重合很容易混在一起。6.3 扩展、网络认证页与旧版本火狐广告拦截类、隐私保护类扩展会主动拦截请求。被拦掉的请求在火狐里有时也会显示成交叉域失败的样子因为请求根本没发出去。判定方法是最快的开一个无痕窗口试一下无痕默认禁用扩展如果无痕正常那就是扩展的问题去扩展列表里逐个排查。另一个特别容易误导人的场景是公共网络。连着酒店或者商场的网络时火狐会提示「您必须先登录此网络才能访问互联网」。这个时候所有请求实际上都被重定向到了登录页面接口请求自然全部失败而且报错形态和跨域完全不一样但看起来同样刺眼。我之前就遇到过同事在会议室里排查了半小时跨域最后发现是 Wi-Fi 还没认证。所以排查的第一步永远是随便打开一个网页确认网络是通的。还有旧版本的火狐包括一些平板上的老版本、某些设备内置的旧版内核。老版本对Access-Control-Allow-Headers里通配符的处理、对Authorization头在预检中的解析都可能和新版不一样。如果你在旧设备上遇到莫名的跨域失败先用最新版火狐对照一遍确认是不是版本差异。6.4 搜 CORS 的时候别搜到测绘那个 CORS这一点属于题外话但确实有人被带偏过。测绘行业里有个东西也叫 CORS全称是连续运行参考站系统做高精度定位用的涉及基准站、差分数据、账号设置这些东西。它跟浏览器跨域完全是两个不相干的概念只是缩写撞车了。所以搜索的时候关键词里带上「跨域」「浏览器」「Access-Control-Allow-Origin」这几个词能过滤掉一大半无关结果。同理那些「火狐开发者模式修改 PDF」「收藏夹怎么设置」「主页被改成别的页面」之类的热词跟跨域一点关系都没有别在设置里到处乱翻。跨域问题只有一个源头服务端的响应头以及浏览器对它的校验。6.5 关于关掉浏览器安全策略这件事网上有些教程会让你去配置页里关掉相关开关或者用启动参数关掉同源策略。我的态度很明确这只应该出现在本地临时验证的场景而且关掉之后这台浏览器就不要用来登录任何真实账号了。同源策略是浏览器最重要的安全护栏之一你把它拆了任何网页都能读取你在其他站点的数据。真要在本地联调更合适的做法是用开发服务器的转发能力把接口请求转发到同一个源下。前端发的是/api/user实际被转发到后端地址浏览器看到的是同源请求压根不触发 CORS。这样既解决了问题又不用动浏览器的安全设置还能顺带把路径统一了一举三得。7. 一次完整的排查链路从无痕窗口到 curl 逐层验证前面几节拆得比较细这一节把它们串成一条可以照着走的排查路径。我给团队里的新人讲的时候就是按这个顺序讲的一般不超出五步。7.1 第一步排除环境和扩展的干扰先开一个无痕窗口把请求重发一遍。如果无痕正常、普通窗口报错就是扩展或者缓存的问题去查扩展和预检缓存。如果无痕也报错说明是代码或者服务端的问题继续往下走。同时确认一下网络是通的随便打开一个外部网页看看。这一步花十秒钟能挡掉网络认证页这种低级但高频的坑。7.2 第二步用 curl 绕开浏览器验证服务端这一步是分水岭。很多人一直在浏览器里改代码其实服务端压根没配对。用 curl 直接把请求打到服务端看看真实的响应头curl -i https://api.example.com/api/user \ -H Origin: https://app.example.com \ -H Content-Type: application/json \ -X POST \ -d {name:test}重点看三件事响应里有没有Access-Control-Allow-Origin值是不是和Origin完全一致带了Access-Control-Allow-Credentials: true没有。如果 curl 里就没有这些头那问题百分之百在服务端浏览器这边怎么调都没用。然后再发一条 OPTIONS 预检看状态码和四个头齐不齐。这两条命令跑完服务端的问题基本能定性。7.3 第三步看火狐网络面板里的原始请求回到火狐打开网络面板勾上「持久日志」和「禁用缓存」重新触发一次请求。找到那条 OPTIONS看它的状态码和响应头。如果状态码是 4xx问题在鉴权或路由如果是 2xx 但头不全问题在跨域配置如果连 OPTIONS 都看不到说明这是简单请求直接看业务请求的响应头。右键复制为 cURL把火狐实际发出的请求头和你以为发出的请求头对比一下。我遇到过好几次「代码里写了 credentials但实际请求里没有 Cookie」的情况一对比就发现了。7.4 第四步核对前端代码里的几个开关不同请求库开启凭证的方式不一样很容易漏请求方式开启凭证的写法常见遗漏fetchcredentials: include默认是 same-origin跨域不会带 CookieXMLHttpRequestxhr.withCredentials true只写了 URL忘了设这个属性axioswithCredentials: true只在全局配了某个实例覆盖掉了第三方 SDK看文档有些 SDK 默认不开需要传配置另外别忘了检查请求头。Content-Type: application/json会触发预检这是正常的不是错误。但如果加了不必要的自定义头会额外增加预检的复杂度能不加就不加。7.5 第五步按症状对照处理到这一步问题基本已经定位。我把常见症状和处理方式整理成一张对照表方便快速查阅症状关键判据处理方向火狐报错Chrome 正常检查响应头的 Allow-Headers 是否用了星号且请求带凭证把星号改成显式列举改了配置刷新无效无痕窗口正常普通窗口报错清预检缓存开发环境 max-age 设 0OPTIONS 返回 401网络面板里预检状态码鉴权放行 OPTIONS或调整中间件顺序OPTIONS 返回 301curl 里看状态码统一前后端路由去掉尾斜杠差异响应头出现两个 Allow-Origincurl -i 打印响应头入口层隐藏后端同名头只在一处配置登录态丢失导致 401请求头里没有 Cookie检查 SameSite、Secure检查增强跟踪保护请求根本没发出去网络面板显示已拦截排查扩展拦截和网络认证页8. 上线前的自查清单几条我每次都会过一遍的检查项调试阶段解决完问题只是第一步跨域配置上线前还得再过一遍因为很多问题只在生产环境的域名、CDN、多级转发下才暴露。下面这份清单是我自己每次上线前都会核对的你可以直接拿去用。第一条Access-Control-Allow-Origin的值是不是具体回显了请求的源而不是星号。带凭证的接口这一条是硬性的。第二条反射 Origin 的场景有没有加上Vary: Origin。本地不写看不出问题上了 CDN 就是玄学故障。第三条Access-Control-Allow-Credentials是不是只在真正需要凭证的接口上开启。能不开就不开开得越多安全面越大。第四条Access-Control-Allow-Headers有没有显式列举。带凭证的情况下不要用星号这是火狐按规范实现、Chrome 相对宽容造成差异的重灾区。第五条预检请求的路径会不会被重定向路由有没有尾斜杠不一致的情况。第六条响应里是不是只有一个Access-Control-Allow-Origin。第七条Access-Control-Max-Age的值合不合理。开发环境给 0 或者一个很小的值生产环境给个几百秒。给太大出问题时排查成本高给太小预检请求会明显变多。第八条Access-Control-Expose-Headers有没有把前端要读的自定义响应头声明出来比如总数、分页游标这类。第九条跨站 Cookie 的SameSite和Secure属性配了没有。这个和 CORS 是两件事但症状高度重合一起检查能省很多来回。第十条生产环境的来源白名单收窄到实际使用的域名别把测试域名和通配的子域留在里面。最后分享一个小习惯。我在每个项目里都会放一个 shell 脚本把前面那两条 curl 命令固化进去参数化域名和路径部署完之后跑一下看着四个头都在心里才踏实。这个脚本不到二十行但帮我在上线前拦下过好几次配置漏推的问题。跨域这事本身不难难的是它有太多看起来一样的错误表现把排查路径固化下来比记住任何一条具体配置都管用。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →