跨域
想象一下,你曾经或者正登录着银行网站(bank.com),同时在另一个标签页打开了一个恶意网站(evil.com)。
如果没有跨域限制,evil.com 里的 JS 脚本可以轻而易举地:
- 读取你银行网站的 Cookie。
- 冒充你向银行发送「转账」请求。
- 读取你的私人账单数据。
这就是著名的 CSRF?(跨站请求伪造) 和 数据泄露 风险。为了防止这种「大乱斗」,浏览器强制执行了 同源策略?(Same-Origin Policy)。
虽然跨域有限制,但现代互联网需要资源共享(比如你的博客引用自己的云音乐平台的歌曲,通过脚本 读取音频渲染出跳动的音符)。于是有了 CORS?(Cross-Origin Resource Sharing,跨源资源共享)。
它本质上是服务器与浏览器之间的一次对话:
- 浏览器问: 「嘿,服务器,我这有个来自
abc.com的网页,想拿你的 MP3,准吗?」 - 服务器答(响应头): 「准了!我带上
Access-Control-Allow-Origin: https://abc.com。」
如果你是一个善良的免费平台,你也可以用
*表示所有人都可以访问:Access-Control-Allow-Origin: *
- 浏览器看: 「嗯,名单上有它,放行!」
CORS 是白名单制度。服务器必须明确说出允许谁访问。
如果服务器没配 Header,或者缓存了没头的响应,浏览器会一直拒绝!
浏览器双标行为
在 Web 早期,像 <img>、<video>、<audio>、<script> 标签都被设计为可以引用任何域名的资源。浏览器认为,既然你只是想「看」或者「听」,并不会把像素数据或音频二进制流通过 JS 拿走,那就没风险。这被称为无损读取。因此<audio src="xxx">(无属性):浏览器以「匿名」身份去拿。服务器返回什么它都播,它不在乎有没有 Access-Control-Allow-Origin。
如果你想在网页上实现音频可视化(跳动的频谱)、音效滤镜或者点击下载:浏览器要求你必须通过 AudioContext 提取音频数据。此时必须用 fetch("xxx") 才能操作二进制数据。fetch("xxx"):浏览器以「跨域」身份去拿。如果服务器没给「通行证」,即使数据下载完了,浏览器也会在最后关头把数据拦下并报错。
如果你的代码之前通过 audio 标签在浏览器里缓存了一个音频(不带 Access-Control-Allow-Origin),之后又改为用 fetch 获取这个资源,浏览器会从缓存里取出无 CORS 头的旧响应提供给 fetch,并在控制台提示该资源被拦截。
建议: 给 audio 标签加上 crossOrigin="anonymous"。这样 audio 和 fetch 都会要求服务器返回 CORS 头。
为了避免相同资源被频繁读取,不只是浏览器,整个网络都充满了各种各样的缓存。
缓存链路
| 缓存层级 | 存储位置 | 常见缓存时长 (TTL) | 对 CORS 的影响 | 缓存清除方式 |
|---|---|---|---|---|
| 浏览器强缓存 | 用户本地磁盘/内存 | 分钟级到年级(由 Cache-Control 决定) | 最致命。一旦存入不带 CORS 头的响应,后续请求不再询问服务器。 | 强制刷新(Ctrl+F5)、手动清除浏览器缓存、或修改资源 URL(加版本号/hash)。 |
| 浏览器协商缓存 | 用户本地 | 即时校验(ETag / Last-Modified) | 相对安全。每次都会向服务器确认文件是否变更,服务器在响应(包括 304)中有机会补发正确的 CORS 头。 | 无需特殊清除,每次请求均会与服务器协商。 |
| Nginx 反向代理缓存 | 服务器内存/磁盘 | 通常不主动缓存(除非配置了 proxy_cache) | 若开启缓存且未配置 Vary: Origin,会将缓存的错误响应(无 CORS 头)发送给所有用户。 | 清空 proxy_cache 目录、重载 Nginx,或配置 proxy_cache_bypass。 |
| CDN(如云存储/边缘节点) | 全球边缘节点 | 小时级到天级(通常 24h) | 扩散性强。一旦边缘节点缓存了缺少 CORS 头的响应,该地区所有用户均会触发 CORS 报错。 | 在 CDN 运营商后台手动刷新/预热缓存,或修改资源 URL 强制回源。 |
| ISP / 运营商网关 | 运营商机房 | 不透明(几小时到几天) | 极少数情况下(通常仅限 HTTP 明文流量),运营商会劫持并缓存静态资源,HTTPS 下基本不受影响。 | 基本无法主动清除,只能等待自然过期,或改用 HTTPS 规避。 |
| 后端框架内存缓存 | 应用服务器内存 | 秒级到小时级(由代码逻辑决定) | 若缓存了不含 CORS 头的响应对象并直接返回,后续请求将持续缺少 CORS 头,且对所有用户生效。 | 重启应用服务、调用缓存失效接口(如 Redis DEL、Spring @CacheEvict),或等待 TTL 自然过期。 |
浏览器缓存
缓存条目的 Response Headers 在开发者工具里不会展示 Access-Control-Allow-Origin,给排查带来一定困难。
浏览器的逻辑顺序
- 读取: JS 发起 fetch,浏览器发现磁盘缓存命中。
- 校验: 浏览器取出 from disk cache 的原始响应头,内部检查是否包含
Access-Control-Allow-Origin。 - 通过: 检查通过,数据交给 JS。出于安全考虑,浏览器在 Network 面板中会省略展示部分安全相关头(如 CORS 头)。
- 展示: 当你在开发者工具(Network 选项卡)点开该请求时,看到的是浏览器认为对当前调试有意义的头信息。
chrome://net-export/ 是 Chrome 最强大的网络日志工具,可将所有网络行为(包括读取缓存的细节)记录到一个 JSON 文件中。
操作步骤:
- 在地址栏输入
chrome://net-export/。 - 点击 "Start Logging to Disk",选择保存位置。
- 回到你的博客页面刷新,重现那个 from disk cache 的请求。
- 回到日志页面点击 "Stop Logging"。
- 使用 NetLog Viewer 打开该 JSON。
- 在日志中搜索
HTTP_CACHE_GET_HEADERS,即可看到缓存里所有原始的 Response Headers。
启发式缓存
HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,响应也会被存储和重用。这称为启发式缓存。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT
复用多长时间取决于实现,但规范建议存储后大约 10% 的时间。
本例中:约 0.1 年 = (Date − Last-Modified) × 10%。
浏览器分析
下面是一段 Chrome NetLog 的片段:
{"params":{"batch_id":0,"encryption_level":"ENCRYPTION_FORWARD_SECURE","packet_number":452,"sent_time_us":472946859030,"size":1250,"transmission_type":"NOT_RETRANSMISSION"},"phase":0,"source":{"id":19077,"start_time":"471676479","type":12},"time":"472946859","type":313},
{"phase":2,"source":{"id":27027,"start_time":"472946851","type":14},"time":"472946862","type":156},
{"params":{"cors_preflight_policy":"consider_preflight","is_revalidating":false,"request_headers":{"headers":["sec-ch-ua-platform: \"Windows\"","Accept-Encoding: identity;q=1, *;q=0","User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36","sec-ch-ua: \"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"145\", \"Chromium\";v=\"145\"","Range: bytes=0-","sec-ch-ua-mobile: ?0","Accept: */*"],"line":"GET /static/summary/blog_2026_2_28_.mp3 HTTP/1.1\r\n"},"url":"https://chat.jiangyang.fun/static/summary/blog_2026_2_28_.mp3"},"phase":1,"source":{"id":27030,"start_time":"472946868","type":1},"time":"472946868","type":576},
[事件 27030] 媒体资源加载日志
- 目标: AI 总结音频(blog_2026_2_28_.mp3)
- 浏览器行为: 通过 HTTP/1.1 获取资源
- 安全决策: 启用 CORS 策略检查(
consider_preflight) - 请求头特征: 带有
Range: bytes=0-(请求媒体流)、Chrome 145 的 User-Agent - 底层状态: 发送了编号为 452 的加密封包,大小 1.25KB,传输正常
另一条日志中的缓存 Key:
{"params":{"created":false,"key":"Range_1/0/_dk_https://jiangmiemie.com https://jiangmiemie.com https://chat.jiangyang.fun/static/summary/blog_2026_2_28_.mp3:2faabf977670a1:0"},"phase":1,"source":{"id":27032,"start_time":"472946870","type":14},"time":"472946870","type":156},
这个字符串是浏览器在磁盘上查找缓存的「身份证号」,由以下几部分组成:
- Range_1/0/: 分段缓存,表示该音频曾被分段请求并缓存。
- dk(Double Keying?): 双重键值。现代浏览器不再只按 URL 缓存,而是按「顶层站点 + 请求来源 + 资源 URL」组合缓存。
https://jiangmiemie.com: Top-level Site(顶层站点)。https://jiangmiemie.com: Frame Origin(请求来源)。https://chat.jiangyang.fun/static/...mp3: 资源真实 URL。
结论 A:缓存已经彻底「分区」了
由于 Key 里包含了 https://jiangmiemie.com,这个缓存条目是专门为你的博客页面生成的。即使你直接在地址栏访问该 MP3,浏览器会使用另一套 Key(不含来源域名)。跨域访问与直接访问对应两套独立的缓存条目;当前命中的是带 CORS 头的健康版本。
结论 B:created: false 意味着命中
"created": false 表示没有新建缓存条目,而是命中了已有条目,请求确实来自 disk cache。命中带 jiangmiemie.com 的 Key,说明该缓存项在写入时已通过跨域校验并记录了来源信息。
结论 C:双重验证通过
日志中的 2faabf977670a1 是该缓存条目的哈希指纹。只要该 ID 对应的资源能正常加载,说明其内部存储的 Access-Control-Allow-Origin 头是完整的。
未雨绸缪
若在商业项目中遇到类似问题:某资源未配置跨域,用户端首页大图裂开,CDN 已分发、用户浏览器也已缓存错误响应。如何紧急处理、避免 T0 事故?
思路:把「图片名称」和「真实地址」分离,例如配置里只存 "home_banner_url": "v1.png",真实 URL 由配置中心下发。
前端初始化时接入配置中心 SDK(例如 Nacos?),通过 WebSocket 或长轮询维持连接:
// configClient.js(伪代码)
import { ConfigClient } from 'nacos-sdk';
export const config = {
home_banner_url: "https://cdn.com/image.png"
};
client.subscribe({ dataId: 'app_config' }, (content) => {
const newConfig = JSON.parse(content);
config.home_banner_url = newConfig.home_banner_url;
store.dispatch({ type: 'UPDATE_CONFIG', payload: config });
});
组件不写死 src,而是从全局状态读取当前 URL:
// BannerComponent.jsx
import { useSelector } from 'react-redux';
const Banner = () => {
const bannerUrl = useSelector(state => state.config.home_banner_url);
return (
<div className="banner">
<img src={bannerUrl} alt="首页大图" />
</div>
);
};
场景: 早上 10:00,发现 image.png 因 CORS 缓存污染在全球范围裂开。
紧急修复(无需发版):
- 打开 Nacos 管理后台,将
home_banner_url从.../image.png改为.../image.png?v=0304_fix,点击发布。 - 正在运行的前端通过 WebSocket 收到新配置,React 状态更新,
img的src变为新 URL。 - 浏览器将新 URL 视为新资源,绕过旧缓存,向 CDN 请求最新图片。
- 用户无感刷新,裂图恢复。