Firebase Hosting 默认给静态文件配了一套 HTTP 头,平时够用,关键时刻不行。firebase.json 里的 headers 块负责覆盖这套默认值——管缓存、管安全、管 CORS。一次配对,以后就不再 debug 旧 CSS、不再丢 AdSense 位置、不再为字体跨域报错收尾。
问题背景
每个 HTTP 响应都带头,告诉浏览器怎么处理响应体——缓存多久、能不能进 iframe、哪些脚本能跑、其他域名能不能拉这个资源。Firebase Hosting 给了一组合理默认值,但猜不到你的具体需求。firebase.json 里的 headers 数组允许按路径覆盖。陷阱在于:一条过于贪心的规则能默默把整站搞坏。
判断标准
- 部署完 CSS 修复,用户看到的还是旧版好几个小时。
- Lighthouse 抱怨「Serve static assets with an efficient cache policy」。
- 站点被嵌进 iframe 是空白,或者合作方加载不了。
- 自托管字体在合作方站点上报 CORS 错误。
- AdSense 或 analytics 脚本被你的 Content Security Policy 拦掉。
快速结论
长寿命的指纹资源(/_astro/*.js、/assets/*.[hash].css)配 Cache-Control: public, max-age=31536000, immutable。HTML 配 Cache-Control: public, max-age=0, must-revalidate。安全头(Strict-Transport-Security、X-Content-Type-Options、Referrer-Policy)一次配好就别管了。CORS 只给真正需要的资源加。
真正能用的缓存头
让站点出问题的常见写法:一条规则对所有路径都套 max-age=31536000。带 hash 的 JS / CSS 缓存一年没问题,HTML 不行——用户会在浏览器缓存过期前一直看到旧页面。
{
"hosting": {
"headers": [
{
"source": "**/*.@(js|css|woff2|jpg|jpeg|png|webp|avif|svg)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "**/*.html",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }
]
},
{
"source": "/sitemap.xml",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=3600" }
]
}
]
}
}
immutable 告诉浏览器永远不要校验——只在文件名带 hash 时安全。HTML 用 max-age=0, must-revalidate,浏览器可以保留文件但每次都发 conditional 请求,新部署立刻可见。
安全头,一次配好
五个头能挡掉 95% 的常见攻击。写进 firebase.json 就不用再管。
{
"hosting": {
"headers": [
{
"source": "**",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}
}
第一次别加 Content-Security-Policy,配错会让所有脚本失效。先用 Content-Security-Policy-Report-Only 跑一段时间,看哪些会被挡,再正式上。
CORS 只给需要的资源
常见错误:为了「安全」给所有资源都加 Access-Control-Allow-Origin: *。这反而把你的 API 和 JSON 接口对所有站点放开。CORS 只在「会被另一个 origin 的 JavaScript 拉取的资源」上需要——比如给合作方加载的字体,或者另一个域名消费的 JSON 接口。
{
"source": "**/*.@(woff|woff2)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" }
]
}
JSON 接口给特定已知 origin 用,明确列出,不要用 *:
{
"source": "/api/public/**",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "https://partner.example.com" },
{ "key": "Vary", "value": "Origin" }
]
}
容易踩的坑
- 一条
**规则套max-age=31536000。HTML 缓存一年,用户除非手动清缓存看不到新部署。 - 加了
X-Frame-Options: DENY然后纳闷 AdSense Auto Ads 为什么挂了——AdSense iframe 是同源,默认值用SAMEORIGIN。 - 第一次就上严格
Content-Security-Policy而不用report-only。第一次部署就会把 analytics、字体、第三方脚本全挡掉。 - 忘了
headers规则是从上到下匹配——宽**规则放在具体规则上面,具体规则反而不会覆盖;具体规则放上面才生效。顺序很重要。 - 同时用
Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true。浏览器拒绝这个组合。要么去掉 credentials,要么列具体 origin。 - 改了
firebase.json又忘了firebase serve不一定遵守这些头——要在真实部署后验证。
FAQ
- 怎么验证某个头真的下发了?:
curl -sI https://yoursite.com/path | head -30看响应头。或者 DevTools Network tab 点资源看 Response Headers。 - 新的 Cache-Control 为什么没生效?: Firebase CDN 在部署后最多 5 分钟还会缓存旧头。等一下,或者用
?v=test强制刷新确认源头是否生效。 /index.html能缓存一小时吗?: 只在能接受用户部署后一小时内看到旧内容的前提下可以。活跃站点用 0 加 revalidate 更稳。- 这些头对 redirect 也生效吗?: 不生效。redirect 自己带响应头。
headers块只对返回内容(200 或 304)的 URL 生效,301/302 不受影响。 - 怎么给 Cloud Function 响应配头,而不是 Hosting 层?: 在函数里
res.set('Cache-Control', '...')配的头对该响应生效。Hosting headers 只管静态文件。