记录一次 Nuxt3 打包时使用 CDN 加载资源,导致部分设备从缓存中读取 CSS 资源跨域问题的排查与解决。
问题背景
最近公司海外业务CDN域名进行了切换,增加了国内火山的CDN加速。原本以为这是一个简单的基础设施升级,没想到却引发了一个让人头疼的跨域问题。
问题现象
某个周末,突然接到反馈:Android设备上访问网站时,CSS文件无法加载,导致页面样式完全错乱。
具体表现:
- Android Webview 报错提示跨域问题
- CSS 资源被浏览器拦截,无法正常加载
- 其他终端(PC、iOS)当时访问正常
应急处理
由于是周末且问题影响用户体验,当时采取了最直接的临时方案:
- 将CDN地址切回之前没有国内加速的旧地址
- 问题立即得到解决
问题深入分析
复现困难
后续尝试复现这个问题时却没有成功,这让排查变得更加困难。但在灰度环境中,问题出现的频率明显增加,甚至扩展到了iOS设备。
我们初期怀疑是否是某个场景让CDN缓存到了错误的资源版本,但经过多次测试和清除CDN缓存,问题依然存在。
根因定位
通过对比不同资源的加载情况,发现了关键问题:
Nuxt3 打包后生成的 CSS Link 标签缺少 crossorigin
属性,而其他 JS 等资源都有完整的 crossorigin
属性。
例如:
|
官方确认
通过搜索相关 Issue,发现这确实是一个已知问题:Entry CSS integrity issues on CDN
好消息是,Nuxt 官方在后续版本中已经修复了这个问题。
解决方案
升级 Nuxt3 版本:更新到3.14
及以上包含此修复的版本,需要注意的是升级后,设备中已有的缓存可能仍然存在问题,需要手动清除缓存后方能正常。
核心原因是什么?
这个问题的核心在于浏览器对跨域资源的缓存机制以及 crossorigin
属性的关键作用——它决定了浏览器如何处理跨域资源的请求和缓存,特别是是否启用 CORS(跨域资源共享)模式。
以下是我的一些分析:
问题发生的原因(未加 crossorigin
)
首次加载(www.example.com/a):
- 浏览器看到
<link href="https://cdn.example.net/style.css" rel="stylesheet">
。 - 因为没有
crossorigin
属性,浏览器不会使用 CORS 模式请求该资源。它发送一个简单的跨域请求。 - 简单请求不需要预检(Preflight),浏览器不会发送
Origin
请求头。 - CDN 服务器返回样式文件。由于请求是“简单”的且不涉及用户凭证(cookies、HTTP认证等),即使 CDN 没有返回任何 CORS 响应头(如
Access-Control-Allow-Origin
),浏览器通常也允许加载和使用这个 CSS 文件(因为 CSS 本身通常被认为是“安全”的跨域资源)。此时浏览器将资源缓存了下来。
- 浏览器看到
后续加载(www.example.com/b):
- 浏览器再次遇到相同的
<link>
标签引用https://cdn.example.net/style.css
。 - 浏览器检查缓存,发现有匹配项。
- 关键点: 当浏览器从缓存中读取一个跨域资源时,它需要知道当初获取这个资源时使用的模式(CORS 还是非 CORS),以便决定当前页面是否可以安全地使用它。
- 由于第一次加载时没有
crossorigin
属性,浏览器将该资源的缓存标记为以非 CORS 模式(”opaque” 或 “no-cors”) 获取的。 - 当尝试在
www.example.com/b
页面上使用这个缓存的资源时,浏览器会执行一个安全检查:它要求当前使用该资源的上下文必须与当初获取它的模式兼容。 - 不幸的是,浏览器(出于安全策略)认为一个以 非 CORS 模式 获取的资源,不能安全地在一个可能期望或需要 CORS 模式的上下文中使用(即使当前请求本身也是没有
crossorigin
的简单请求)。这触发了跨域错误。错误本质上是:“我缓存里的这个资源是用不安全(非CORS)的方式拿到的,我现在不能放心地把它给你用,即使你也没明确要求安全(CORS)的方式”。
- 浏览器再次遇到相同的
解决方案生效的原因(添加 crossorigin
)
首次加载(www.example.com/a - 添加后):
- 浏览器看到
<link href="https://cdn.example.net/style.css" rel="stylesheet" crossorigin="anonymous">
。 crossorigin="anonymous"
属性明确告诉浏览器:使用 CORS 模式 来请求这个资源,并且不携带用户凭据(如 cookies、HTTP 认证)。- 浏览器发送请求时,会添加
Origin: https://www.example.com
请求头。 - CDN 服务器必须在响应中包含适当的 CORS 响应头,例如
Access-Control-Allow-Origin: https://www.example.com
或Access-Control-Allow-Origin: *
,以明确允许该来源访问资源。 - 浏览器收到响应,验证
Access-Control-Allow-Origin
头匹配当前来源(或允许通配符*
),确认 CORS 成功。此时浏览器将资源缓存,并明确标记这个资源是以 CORS 模式获取的,并且来源是经过服务器显式授权的。
- 浏览器看到
后续加载(www.example.com/b):
- 浏览器再次遇到相同的
<link crossorigin="anonymous" ...>
标签。 - 检查缓存,找到匹配项,并看到该资源是以 CORS 模式获取且来源已授权的。
- 浏览器知道这个缓存的资源是“安全”的,因为它当初是通过 CORS 协议获取的,服务器明确允许了
www.example.com
使用它。 - 浏览器允许使用缓存的资源,不会触发跨域错误。
- 浏览器再次遇到相同的
crossorigin
属性的区别总结
特性 | 没有 crossorigin 属性 |
有 crossorigin 属性 (通常 anonymous ) |
---|---|---|
请求模式 | 非 CORS 模式 (no-cors / opaque) | CORS 模式 (cors) |
Origin 请求头 |
不发送 | 发送 (Origin: <当前页面源> ) |
所需响应头 | 无特定要求 | 必须包含有效的 Access-Control-Allow-Origin |
资源缓存标记 | 标记为 非 CORS (opaque) 模式获取 | 标记为 CORS 模式获取且来源已授权 |
跨域错误触发点 | 可能发生在后续从缓存加载并使用时 | 发生在首次请求时(如果服务器 CORS 头缺失/无效) |
安全性保证 | 低。浏览器对资源来源和使用方式控制较弱。 | 高。通过 CORS 协议显式授权来源,资源使用更安全可靠。 |
适用场景 | 对跨域资源来源和安全性要求不高,且确定该资源永远不会导致跨域缓存问题。 | 推荐做法。确保跨域资源加载透明、安全,兼容缓存,避免后续页面访问时出现意外跨域错误。 |
结合该网友的文章:条件型 CORS 响应下因缺失 Vary Origin 导致的缓存错乱问题,
我们可以推测在缺少crossorigin
的情况下,CDN资源响应头可能缺少 Access-Control-Allow-Origin
和 Vary: Origin
,这会导致浏览器在处理缓存时出现问题。
结论
- 未加
crossorigin
: 导致资源以 非 CORS (opaque) 模式加载和缓存。浏览器认为这种模式获取的资源“不够安全”,在后续页面从缓存中重用它时,即使请求本身也是简单的,浏览器也会保守地阻止使用,抛出跨域错误。 - 添加
crossorigin="anonymous"
: 强制使用 CORS 模式。浏览器发送Origin
头,服务器必须用Access-Control-Allow-Origin
头显式授权来源。资源被标记为 CORS 模式且来源已授权 后缓存。后续页面加载时,浏览器确认缓存的资源是“安全”的(因为服务器明确授权过),因此允许使用,避免了跨域错误。
最后可以得到一个大概的猜测:在公司海外CDN叠加国内火山CDN后,可能因为未携带crossorigin
属性,导致了资源以非CORS模式加载,同时S3存储的资源响应头缺少 Access-Control-Allow-Origin
和 Vary: Origin
,然后被火山CDN缓存了,同时也被一部分用户的浏览器缓存了,导致了后续访问时出现跨域错误。
最佳实践: 对于任何来自不同域(尤其是 CDN)的静态资源(CSS, JS, Fonts, Images 等),强烈建议添加 crossorigin="anonymous"
属性。这确保了:
- 资源通过 CORS 协议显式授权加载。
- 资源被正确地标记并缓存,避免后续页面访问时因缓存模式不匹配而导致的跨域错误。
- 提高了资源加载的安全性和可预测性。
- 通过明确的 CORS 策略,减少资源加载的不确定性。