Firebase Hosting 头配置:缓存、安全、CORS

firebase.json 的 headers 块管缓存 TTL、安全策略和 CORS。直接抄一份既快又安全的最小配置,跳过那些会默默搞坏整站的写法。

Firebase Hosting 通过 Google CDN 分发静态文件,如果你什么都不配,默认 Cache-Controlmax-age=3600(一小时)。这个默认值平时够用,关键时刻不行:你推了个 CSS 修复,CDN 和每个浏览器把旧文件再扣一小时,用户就开始抱怨了。firebase.json 里的 headers 块负责覆盖这套默认值——管缓存、管安全、管 CORS。一次配对,以后就不再 debug 旧资源、不再丢 AdSense 位置、不再为字体跨域报错收尾。

速览

  • 带指纹的资源(/_astro/*.js*.[hash].css)用 public, max-age=31536000, immutable 缓存一年;HTML 用 public, max-age=0, must-revalidate,每次部署立刻可见。
  • Firebase 的默认值:静态文件 max-age=3600,Cloud Function 或 Cloud Run 返回的内容是 private(CDN 完全不缓存)。默认不合适时自己配头。
  • 五个安全头一次性配好(Strict-Transport-SecurityX-Content-Type-OptionsX-Frame-OptionsReferrer-PolicyPermissions-Policy),然后就别管了。Content-Security-Policy 放到第二轮,先用 report-only。
  • CORS 只给「会被另一个 origin 的 JavaScript 拉取」的具体资源加,绝不全站套 *
  • 头规则按 source glob 从上到下取第一条匹配。具体规则放在宽规则上面。

Firebase 头的行为(文档里藏得深的部分)

每个 HTTP 响应都带头,告诉浏览器缓存多久、能不能进 iframe、哪些脚本能跑、其他域名能不能拉资源。Firebase Hosting 给了默认值,但读不出你的意图。firebase.json 里的 headers 数组按路径覆盖默认值。有三个事实决定你的配置能不能成(依据 Firebase Configure Hosting behaviorManage cache behavior 官方文档核对,截至 2026 年 6 月):

  • 默认缓存就是一小时。 没配 Cache-Control 的静态内容拿到 max-age=3600。所以「旧内容扣一小时」是默认行为,不是配错了。
  • 第一条匹配的规则生效。 和 redirect、rewrite 一样,Hosting 取从上到下第一条 source glob 匹配的头规则。数组要按「先具体后宽泛」排。
  • 头规则在 rewrite 之前执行。 头是针对请求的 URL 路径匹配的,在任何 rewrite 之前,所以它瞄准的是用户请求的 URL,而不是 rewrite 后的目标。

推荐用 source glob 写匹配模式;需要的话 regex(RE2)也支持。

指向这里的症状

  • 部署完 CSS 修复,用户最多一小时还看到旧版。
  • Lighthouse 报「Serve static assets with an efficient cache policy」。
  • 站点嵌进 iframe 是空白,或者合作方嵌不进去。
  • 自托管字体在合作方域名上报 CORS 错误,控制台里能看到。
  • 加了 Content Security Policy 之后 AdSense 或 analytics 脚本被拦掉。

真正能用的缓存头

让站点出问题的写法是一条规则对所有路径都套 max-age=31536000。带指纹的 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 请求,用户总是落到最新部署。

把 CDN 和浏览器分开调

max-age 管浏览器,s-maxage 管共享 CDN 缓存,并在 CDN 上覆盖 max-age。如果你想让浏览器频繁校验、但让 Google 边缘节点多扣一会儿,就两个都设:

{ "key": "Cache-Control", "value": "public, max-age=0, s-maxage=600, must-revalidate" }

这个写法对 Cloud Function 或 Cloud Run 的动态响应最有用——它们默认根本不进 CDN 缓存(默认是 private)。你得显式加 public 再加 s-maxage 才能让它缓存。

资源类型推荐 Cache-Control原因
带 hash 的 JS/CSS/图片(*.[hash].*public, max-age=31536000, immutable每次构建文件名都变,可永久缓存
HTML 页面public, max-age=0, must-revalidate每次部署都要立刻可见
sitemap.xmlrobots.txtpublic, max-age=3600爬虫能容忍一小时的滞后
Cloud Function JSONpublic, max-age=0, s-maxage=600边缘缓存 10 分钟;浏览器每次重查
Firebase 默认(不配头)max-age=3600一小时,什么都不设时生效

安全头,一次配好

五个头能挡掉大多数常见攻击。放在 ** source 下,配好就别管。

{
  "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,在 DevTools 里盯一周违规报告,再换成强制生效的头。每个 directive 可以查 MDN CSP 参考

CORS 只给需要的资源

常见错误是为了「安全」给每个资源都加 Access-Control-Allow-Origin: *,这反而把你的 JSON 接口和 API 对全网放开。CORS 只在「会被另一个 origin 的 JavaScript 拉取的资源」上需要:通常是给合作方加载的字体,或者另一个域名消费的 JSON 接口。

{
  "source": "**/*.@(woff|woff2)",
  "headers": [
    { "key": "Access-Control-Allow-Origin", "value": "*" }
  ]
}

JSON 接口给一个已知 origin 用,就明确列出来,别用 *,并加 Vary: Origin 防止缓存把错误的 CORS 响应发给别人:

{
  "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、字体、第三方脚本全挡掉。
  • 把宽 ** 规则放在具体规则上面。第一条匹配生效,宽规则先命中,具体规则永远跑不到。要「先具体后宽泛」排。
  • 同时用 Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true。浏览器拒绝这个组合。要么去掉 credentials,要么列具体 origin。
  • 指望 firebase serve 遵守你的头。它不一定模拟得出来。要在真实部署后验证。

验证部署

firebase deploy --only hosting 之后,确认头真的下发了:

curl -sI https://yoursite.com/_astro/app.abcd1234.js | grep -i cache-control
# 预期:cache-control: public, max-age=31536000, immutable

curl -sI https://yoursite.com/index.html | grep -i cache-control
# 预期:cache-control: public, max-age=0, must-revalidate

如果还是旧值,CDN 在部署后会短暂保留上一版头。在 URL 后面加一个一次性查询串(?v=test)绕过缓存,确认源头已经在发新头。

FAQ

  • 怎么验证某个头真的下发了?: 跑 curl -sI https://yoursite.com/path 读响应头,或者打开 DevTools,在 Network tab 点资源,看 Response Headers。
  • 新的 Cache-Control 为什么没生效?: Firebase 重新部署会清 CDN,但边缘节点可能短暂还在发上一版头。等几分钟,或者用 ?v=test 命中 URL,确认源头已经在发新头。
  • 什么都不配时默认的 Cache-Control 是什么?: 静态文件拿到 max-age=3600(一小时)。Cloud Function 或 Cloud Run 返回的内容是 private,CDN 完全不缓存。
  • /index.html 能缓存一小时吗?: 只在能接受用户部署后一小时内看到旧内容的前提下可以。活跃站点用 max-age=0, must-revalidate 更稳。
  • 这些头对 redirect 也生效吗?: 不生效。301/302 自己带响应头。headers 块只对返回内容(200 或 304)的 URL 生效,不管 redirect 响应。
  • 怎么给 Cloud Function 响应配头,而不是 Hosting 层?: 在函数里调 res.set('Cache-Control', '...')。注意只有 __session 这个 cookie 会传到后端代码,而且它会成为 CDN 缓存键的一部分。

相关阅读

标签: #独立开发 #Firebase #部署 / 托管 #headers #cache