Skip to main content

同源策略、跨域与缓存链

· 11 min read
Allen
normal software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

跨域

想象一下,你曾经或者正登录着银行网站(bank.com),同时在另一个标签页打开了一个恶意网站(evil.com)

如果没有跨域限制,evil.com 里的 JS 脚本可以轻而易举地:

  • 读取你银行网站的 Cookie。
  • 冒充你向银行发送「转账」请求。
  • 读取你的私人账单数据。

这就是著名的 CSRF?(跨站请求伪造)数据泄露 风险。为了防止这种「大乱斗」,浏览器强制执行了 同源策略?(Same-Origin Policy)

虽然跨域有限制,但现代互联网需要资源共享(比如你的博客引用自己的云音乐平台的歌曲,通过脚本读取音频渲染出跳动的音符)。于是有了 CORS?(Cross-Origin Resource Sharing,跨源资源共享)

它本质上是服务器与浏览器之间的一次对话

  1. 浏览器问: 「嘿,服务器,我这有个来自 abc.com 的网页,想拿你的 MP3,准吗?」
  2. 服务器答(响应头): 「准了!我带上 Access-Control-Allow-Origin: https://abc.com。」

如果你是一个善良的免费平台,你也可以用*表示所有人都可以访问:Access-Control-Allow-Origin: *

  1. 浏览器看: 「嗯,名单上有它,放行!」

CORS 是白名单制度。服务器必须明确说出允许谁访问。

如果服务器没配 Header,或者缓存了没头的响应,浏览器会一直拒绝!

tip

浏览器双标行为

在 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,给排查带来一定困难。

info

浏览器的逻辑顺序

  • 读取: 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
tip

启发式缓存

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/: 分段缓存,表示该音频曾被分段请求并缓存。
  • dkDouble 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 缓存污染在全球范围裂开。

紧急修复(无需发版):

  1. 打开 Nacos 管理后台,将 home_banner_url.../image.png 改为 .../image.png?v=0304_fix,点击发布。
  2. 正在运行的前端通过 WebSocket 收到新配置,React 状态更新,imgsrc 变为新 URL。
  3. 浏览器将新 URL 视为新资源,绕过旧缓存,向 CDN 请求最新图片。
  4. 用户无感刷新,裂图恢复。

参考链接