
跨域配置不生效这个事在阿里云 OSS 的日常排查里出现频率非常高。我见过不少开发者在控制台里把“跨域设置”那一页从上到下填满保存完回到浏览器一刷新控制台照样报Access to XMLHttpRequest at https://xxx.oss-cn-hangzhou.aliyuncs.com/xxx from origin http://localhost:5173 has been blocked by CORS policy。这篇文章不打算只告诉你按钮在哪而是把这几年实际排查中见到的原因拆开讲尤其是浏览器缓存、规则顺序、请求头匹配这几个最隐蔽的坑。如果你正在用前端直传文件到 OSS、通过 AJAX 或 Fetch 访问 OSS 上的公开文件、或者用 SDK 和存储桶交互时遇到跨域报错下面这些内容应该能直接对上号。1. 先弄清楚跨域问题到底卡在哪一环1.1 报错和现象不能只看第一行很多人拿到跨域报错第一反应是去控制台改配置但改完发现还是老样子这时候容易陷入死循环。我建议先停下来把浏览器 console 里完整的报错信息截下来因为同样叫“跨域”实际卡住的环节完全不一样。常见的报错分两类。一类是Preflight response is not successful意思是浏览器发出去的预检请求OPTIONS没有得到合法的 CORS 响应头请求在真正发出之前就被拦下来了。另一类比较隐蔽页面看起来一切正常请求也返回了 200但 JS 代码里读不到响应头拿不到ETag或者自定义的x-oss-meta-*字段这往往是暴露头配置缺失而不是来源配置的问题。另一个容易被误判的点是“简单请求”和“预检请求”的区别。如果你的请求是 GET没有自定义请求头Content-Type也不是application/json那么浏览器根本不会发 OPTIONS 预检直接发真实请求。这种情况如果还报跨域问题基本不在预检上而是服务端返回的响应里根本没有Access-Control-Allow-Origin头。假设你经过 CDN、自定义域名或者网关转发中间任意一层把这个头吞掉前端就会报跨域。1.2 OSS 的 CORS 规则到底管了什么OSS 的跨域规则和普通后端写一段 CORS 中间件还不完全一样。普通后端可以针对每个请求动态计算允许的来源、方法和请求头OSS 存储桶则是一套静态规则表。前端请求到达 OSS 后OSS 会拿出来和规则逐条比较完全匹配才在响应里返回 CORS 头。这套规则表包含五个核心字段来源AllowedOrigin、允许方法AllowedMethod、允许请求头AllowedHeader、暴露请求头ExposeHeader、缓存时间MaxAge。很多“配置不生效”的案例最后都落在来源没匹配上、方法没勾全、请求头没列出来这几类问题里。还有一点需要明确OSS 的多条规则是按顺序匹配的命中了第一条就不再往后看不会把多条规则的权限合并起来。这一点很多人不知道也是后面要展开讲的重点。2. 最容易被忽视的元凶浏览器缓存与预检缓存2.1 为什么明明改了规则却不生效这是我排查时遇到最多的情形。开发者打开 OSS 控制台把来源从https://a.example.com改成https://b.example.com保存后回浏览器刷新还是报错。第一反应往往是“OSS 配置是不是有缓存”其实 OSS 侧配置通常秒级生效真正不生效的是浏览器。关键在那个“缓存时间”字段上。当浏览器第一次发现跨域请求需要预检时会向 OSS 发 OPTIONS 请求OSS 返回的响应头里带有Access-Control-Max-Age。这个值告诉浏览器你可以把这次预检结果缓存这么多秒。在缓存有效期内浏览器不会再发 OPTIONS而是直接拿缓存里的判断结果来决定是否放行。想象一下这个场景你之前配置的Max-Age是 86400也就是一天。今天上午前端第一次请求时预检结果被缓存了下午你改了 OSS 配置把所有规则修正了但浏览器到明天上午之前一直用的是旧缓存。所以你会觉得“怎么改了没反应”实际上缓存过期之后一切正常。有人描述为“OSS 跨域配置隔了一天自己好了”真相就在这里。2.2 缓存到底存在哪、怎么绕过浏览器缓存预检结果的位置并不在 Service Worker也不在普通的 HTTP 缓存里而是浏览器内部的 CORS 预检缓存。Chrome 开发者工具里你可能看不到特别明显的缓存条目但它确实存在。绕过的办法很简单也很土开一个无痕窗口重新访问页面。无痕窗口默认没有历史缓存和预检缓存能立刻看到配置修改后的真实效果。但要注意无痕窗口只是绕开了浏览器缓存如果你在原来的普通窗口里测试即使按 CtrlShiftR 强刷预检缓存也不会被清掉因为强刷主要针对普通 HTTP 缓存。所以测试跨域配置直接无痕窗口起步不要和自己的浏览器较劲。还有一种更彻底的办法是用 curl 直接模拟 OPTIONS 请求完全跳过浏览器。curl 没有预检缓存OSS 返回什么头就是什么头这能直接验证配置是否生效。2.3 配置里的“缓存时间”是双刃剑很多人图省事把缓存时间设置得很大比如 86400目的是一天内反复上传文件时减少预检请求的次数性能更好。但代价是每次修改配置后都可能要等一天才全局生效。这在线上调试阶段非常痛苦。我自己的习惯是测试环境先把缓存时间设成 60 秒甚至 0。这样改完配置后最多等一分钟就能验证。等配置完全稳定了再根据线上场景把缓存时间调回 600 或者 3600。如果你已经踩了“改了不生效”的坑第一件事就是检查之前的 Max-Age 是不是设得太大同时把无痕窗口拉出来做对照测试。3. 规则内容本身的问题Origin、顺序与匹配模式3.1 Origin 不匹配的真实原因HTTPS、端口和路径来源匹配是跨域配置里最容易出错、也最让人抓狂的一环。浏览器发送的Origin包含三部分协议、域名、端口三者必须完全一致才算匹配。http://localhost:5173和http://localhost:4173不是同一个来源https://admin.example.com和http://admin.example.com也不是同一个来源。有一种典型失误线上页面用的是 HTTPS但 OSS 控制台里来源填的是https://admin.example.com看起来没错但如果实际页面的 HTTP 协议、浏览器地址栏显示为https://admin.example.com/注意末尾斜杠控制台里的来源不加斜杠其实没问题浏览器比较的本来就是去掉路径和斜杠的 Origin。真正容易出问题的反而是端口和协议。另外还有一类本地开发环境是http://localhost:8080生产环境是https://admin.example.com有些人在控制台只填了生产域名本地测试时自然一直报跨域。这里推荐把开发、测试、生产三个环境的 Origin 都单独列出来而不是图省事只填一个。OSS 控制台的“来源”字段不要填带路径的地址比如https://admin.example.com/api这种。Origin 本身不含路径你填了 OSS 也匹配不上。我之前见过单子开发者在来源里把完整 URL 复制进去甚至带了查询参数结果自然一直失败。通配符*也可以用OSS 支持https://*.example.com这种子域通配也支持全通配*。但要注意两条限制第一通配符配置无法和带凭据的请求同时使用规范里Access-Control-Allow-Origin: *不允许和Access-Control-Allow-Credentials: true一起出现第二如果你的前端是带签名信息访问 OSS来源填*时有概率触发 OSS 的保守策略响应里不会给出预期的 CORS 头。遇到这类场景最稳妥的做法就是把来源域名写具体不要偷懒。3.2 多条规则不是合并而是第一个匹配OSS 控制台支持配置多条跨域规则这个功能本意是方便你按前端来源区分权限但很多人不知道它的匹配逻辑是“顺序匹配、命中即止”。什么意思假设你有两条规则规则一来源*允许方法 GET规则二来源https://admin.example.com允许方法 PUT当https://admin.example.com发来一个 PUT 请求时OSS 会先看规则一。规则一的来源*能匹配这个地址于是直接采用规则一的允许方法列表里面没有 PUT预检失败。规则二根本没有执行机会。这不是 BUG设计上就是如此。在实际配置里你应该把限制最严格、作用范围最小的规则放在最前面把*这种宽松规则放到最后甚至单独用一个规则兜底。如果从头到尾只有一条规则也要注意想让某个来源既支持 GET 又支持 PUT必须把 GET、PUT 都勾上而不是配置两条规则分别勾不同方法。3.3 通配符要只用在合适的位置通配符*在处理“允许请求头”时很省事但在“来源”上滥用会带来额外问题。比如你确实想允许任何网站读取存储桶里的公开图片来源填*没问题但如果你要配合签名 URL 或 Authorization 头做跨域读取这就不合适了。实践中来源*和带凭证的请求组合浏览器和 OSS 之间的行为受规范和安全策略约束调试起来很麻烦。我的建议是公开只读场景可以用*凡是涉及上传、删除、修改这一类敏感操作来源一定要写具体域名至少写公司主域名的子域通配比如https://*.yourcompany.com。这样既不牺牲多子域使用的便利性也避免给自己埋安全隐患。4. 方法与 Header 配置不全会导致预检失败4.1 允许方法列表要包含实际使用的动作允许方法这个字段看起来直白但它非常容易漏。比如只勾了 GET前端却用 axios 发了一个 PUT 上传预检时浏览器会携带Access-Control-Request-Method: PUTOSS 检查规则里没有 PUT返回的响应头里就不会允许 PUT预检直接失败。这种情况常见于 Web 端直传 OSS。很多上传库底层用的是 PUT 或 POST但开发者在控制台只勾了 GET 和 HEAD于是上传时一直报预检失败。正确的做法是把前端可能用到的上传动作全部勾上GET、HEAD、PUT、POST、DELETE。如果拿不准全勾上是最省心的选择因为 OSS 不是你的业务接口多开放方法本身不会直接暴露数据真正的权限由签名策略控制。4.2 请求头AllowedHeader是过预检的关键相比方法请求头配置更隐蔽。浏览器在正式发送跨域请求之前如果发现请求带有自定义 Header会在 OPTIONS 预检请求里带上Access-Control-Request-Headers列出所有要发送的头字段。OSS 收到后会检查这些字段是否都出现在规则的“允许请求头”里只要有任何一个不在预检就失败。常见的漏网之鱼包括Authorization、Content-Type、x-requested-with以及前端自定义的一些业务头。尤其是Content-Type: application/json这个问题出现率极高。前端 API 请求用 axios 默认 JSON 格式会自动带Content-Type: application/json预检时浏览器问 OSS“允许这个请求头吗”OSS 的规则里没写直接拒绝。解决方式有两种。一种是具体的Content-Type,Authorization,x-requested-with,x-oss-*等等都列出来。另一种是干脆填*表示允许所有请求头。OSS 控制台是支持通配符的。从我的经验看只要不是极端的签名场景填*兼容性最好后续新增业务头也不用再改规则。有一种情况要特别注意如果前端使用了withCredentials: true或credentials: include也就是跨域请求携带 Cookie那么浏览器不仅会检查 AllowedHeader还会要求服务端返回Access-Control-Allow-Credentials。OSS 的 CORS 规则里没有单独提供这个开关带凭据访问 OSS 本身也不常见因为 OSS 通常靠签名或临时凭证鉴权。如果你真的遇到这类需求先把来源从*改成具体域名再检查整个链路里是否有其他服务在转发时影响 CORS 头。4.3 暴露头ExposeHeader决定 JS 能读什么暴露头是另一个容易被忽略的配置项。CORS 规范规定跨域请求的响应头默认只有少量“简单响应头”能被 JS 读取除此之外的任何响应头都必须出现在Access-Control-Expose-Headers里否则浏览器虽然拿到了数据但你的 JS 代码读不到。最常见的需求是读取ETag和x-oss-meta-*。上传组件可能需要通过ETag判断文件是否真的传成功了或者防止断点上传时校验数据一些业务系统会通过自定义元数据x-oss-meta-*保存文件描述前端上传后需要获取这些值回显。如果暴露头没配置控制台里数据明明有字段代码里却打印出undefined非常容易误判为“接口没返回”。配置方法很简单把需要读取的响应头用英文逗号分隔填进去ETag,x-oss-meta-*。这里的*是针对x-oss-meta-前缀元数据的通配OSS 控制台是支持的。建议在配置暴露头时不要省直接把ETag和x-oss-meta-*都写上避免以后接各种第三方组件时踩“读不到头”的坑。5. 完整排查路径从浏览器到 OSS 日志5.1 从零开始的一套排查流程遇到跨域问题不要急着一股脑改配置。我习惯按下面这套流程排查基本上 80% 的问题都能快速定位。第一步先看 console 报错确定是预检失败还是暴露头读取不到。如果是预检失败点开 Network 面板找到那条红色 OPTIONS 请求看它的状态码和响应头。第二步用无痕窗口重新打开页面复现一次。如果无痕窗口里一切正常说明是浏览器缓存了旧配置按第 2 节的方法处理缓存时间即可。第三步随手写一个 curl 命令直接模拟 OPTIONS 请求打向 OSS把返回的 CORS 响应头全部拉出来逐一核对。第四步如果 curl 也拿不到应有的 CORS 头检查请求的域名是不是真的指向 OSS。如果你用的是自定义域名并且走了 CDN先去掉 CDN直接用存储桶的默认域名测试。这一步能快速排除“CDN 缓存了不带 CORS 头的旧响应”这个可能。第五步最后回到 OSS 控制台按照请求里的 Origin、方法和请求头逐条比对规则表重点看来源是否完全匹配、规则顺序是否把严格规则放在了宽泛规则后面。5.2 用 curl 直接看预检返回头下面这个命令能模拟浏览器发出的 OPTIONS 预检请求。假设你的 Bucket 在杭州区域文件路径是example.txt前端页面来源是https://admin.example.com要发送 PUT 请求且带Content-Type请求头curl -i -X OPTIONS https://bucket.oss-cn-hangzhou.aliyuncs.com/example.txt \ -H Origin: https://admin.example.com \ -H Access-Control-Request-Method: PUT \ -H Access-Control-Request-Headers: content-type正常的响应头里应该包含这些字段HTTP/1.1 200 OK access-control-allow-origin: https://admin.example.com access-control-allow-methods: PUT access-control-allow-headers: content-type access-control-expose-headers: ETag,x-oss-meta-* access-control-max-age: 600对照看access-control-allow-origin有没有回显你的 Originaccess-control-allow-methods里有没有 PUTaccess-control-allow-headers里有没有content-type哪一项缺失问题就出在哪一项上。有一个特殊点值得注意如果来源配置的是*某些情况下 OSS 可能返回access-control-allow-origin: *而不是具体的 Origin这也是符合预期的。但在带签名或带自定义请求头的高安全场景里这条行为可能有变化。遇到这种情况把来源改成具体域名再测试一次。5.3 OSS 访问日志与常见错误对照表OSS 访问日志可以帮你确认请求到底有没有到达 OSS。你可以在控制台开启实时日志或者下载离线访问日志按请求时间和文件路径过滤看是否出现了 OPTIONS 方法。如果日志里根本没有 OPTIONS 请求说明请求在到达 OSS 之前就被某一层拦截或缓存了重点排查 CDN、网关或其他代理如果 OPTIONS 请求到达了但返回异常那就是 CORS 规则本身有问题。下面是我整理的一张常见问题对照表基本覆盖了大多数情况现象可能原因排查方向无痕窗口正常普通窗口报错浏览器预检缓存缩短 Max-Age测试用无痕改了配置一小时后才正常之前 Max-Age 设置过大把缓存时间设小再验证来源已配置为https://a.com仍失败页面实际是http://a.com或带端口检查浏览器 Origin 和端口curl里 OPTIONS 响应头不完整来源、方法、请求头不匹配逐项比对规则请求返回 200但 JS 读不到 ETagExposeHeader 没配置加上ETag和x-oss-meta-*自定义域名测试失败默认域名正常CDN 缓存或头被剥离清除 CDN 缓存或回源验证多条规则配置了但后一条总不生效规则命中顺序问题把严格规则放前面6. 按场景套用的配置模板6.1 场景 A前端网页读取公开文件如果你的页面只是读取存储桶里的公开文件比如图片预览、文档下载配置最简单。来源可以直接用*方法勾 GET、HEAD允许请求头也是*暴露头把ETag和x-oss-meta-*写上缓存时间 600 秒。这个模板能覆盖绝大多数前端展示需求。6.2 场景 BWeb 端直传 OSSWeb 直传是最容易出跨域问题的场景因为上传动作通常涉及 PUT 或 POST而且会带Content-Type。配置时不要把方法只勾 GET要把 PUT、POST 都加进去允许请求头建议直接填*否则你会在Authorization、content-type、x-oss-*这些头上反复折腾。来源方面建议把实际前端域名明确列出来开发、测试、生产分三条规则或者写一个子域通配https://*.yourcompany.com。暴露头别忘了ETag和x-oss-meta-*因为一些前端上传库会在完成后读取这些信息。缓存时间测试期 60 秒稳定后调到 600。6.3 场景 C多环境多域名前端如果公司内部有多个子系统都接入同一个 Bucket一个简单粗暴的做法是写一条来源为*的规则全部放行。短期看着方便但长期并不好一是后续出现安全问题很难界定来源二是签名场景下*有兼容性风险。我更推荐按环境分别列规则比如开发环境来源http://localhost:8080测试环境来源https://test.admin.example.com生产环境来源https://admin.example.com规则之间不要用*覆盖。表格总结如下场景来源允许方法允许请求头暴露请求头缓存时间公开文件读取*GET, HEAD*ETag, x-oss-meta-*600Web 直传上传https://admin.example.comGET, PUT, POST, HEAD*ETag, x-oss-meta-*600多环境前端分环境列来源GET, PUT, POST, DELETE, HEAD*ETag, x-oss-meta-*36007. 我踩过几次坑后沉淀的经验跨域配置本身只是 OSS 使用中很小的一块但越是小事越容易让人折腾大半天。我总结了几个能救命的小习惯分享出来供参考。第一所有涉及跨域的测试从第一天起就养成用无痕窗口的习惯。这不光是为了测 CORS本地开发时 Cookie、Service Worker、缓存这些东西都可能造成干扰无痕窗口能在很大程度上还原“新用户”视角。第二改 OSS CORS 配置前先看一眼当前的缓存时间。如果之前设的是 86400改完配置发现不生效不要怀疑 OSS先去查自己的浏览器。正确操作是先在无痕窗口里验证再把缓存时间调成 60 秒做收敛测试。第三curl 测试是钉死问题的最终手段。浏览器有一堆缓存和扩展干扰而 curl 不带任何历史状态。遇到拿不准的情况直接把浏览器 Network 面板里那条 OPTIONS 请求 Copy as cURL在终端里重发一遍对比响应头差异问题性质立刻清楚了。第四暴露头配置几乎是前端上传组件最容易踩的坑。很多上传库和预览组件表面上用了 ETag实际上库里可能只是好看但还是会去读。我在生产环境见过几次“上传成功但页面一直报错”最后发现就是 ETag 没暴露。建议直接把ETag和x-oss-meta-*一起配上永远不要在这上面节省。第五规则顺序这件事在团队协作时尤其容易出问题。今天 A 同学加了一条宽泛规则明天 B 同学加了一条严格规则后一条因为顺序靠后根本不生效。我建议把 Bucket 的 CORS 配置纳入上线检查清单别让它成为没人维护的灰色地带。