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

firebase.json 的 headers 块管缓存 TTL、安全策略和 CORS。给出一份既快又安全的最小配置,和那些会让你付出代价的写法。

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-SecurityX-Content-Type-OptionsReferrer-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 只管静态文件。

相关阅读

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