<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Vary on Tarragon</title><link>https://tarrragon.github.io/blog/tags/vary/</link><description>Recent content in Vary on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 02 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/vary/index.xml" rel="self" type="application/rss+xml"/><item><title>12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/</guid><description>&lt;p>這篇整理快取收到一個請求時，怎麼決定要拿哪一份保存的副本回應它，或判定沒有可用的副本。這個判斷決定了一份副本會被哪些人拿到，所以也是個人化內容外洩到別人手上的成因所在。保存位置的分類見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取&lt;/a>。本篇談的外洩發生在共用快取上：同一份副本被發給不同使用者。另一種「看到別人的畫面」發生在同一台裝置上，前一個使用者的頁面在按上一頁時被瀏覽器的 back/forward cache 還原，見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁&lt;/a> 的〈上一頁與 back/forward cache〉。&lt;/p>
&lt;h2 id="快取鍵的組成">快取鍵的組成&lt;/h2>
&lt;p>快取用來挑選副本的資訊稱為&lt;strong>快取鍵&lt;/strong>（cache key）。規範定義的最小組成是請求方法加上目標網址：&lt;/p>
&lt;blockquote>
&lt;p>The &amp;ldquo;cache key&amp;rdquo; is the information a cache uses to choose a response and is composed from, at a minimum, the request method and target URI（RFC 9111 §2）&lt;/p>&lt;/blockquote>
&lt;p>規範也指出，許多常用的快取只保存 GET 的回應，所以只用網址當快取鍵。網址包含查詢字串，&lt;code>/products?id=1&lt;/code> 與 &lt;code>/products?id=1&amp;amp;utm_source=mail&lt;/code> 是兩個不同的快取鍵，各自保存一份副本。行銷追蹤參數這類不影響內容的查詢字串，會讓同一份內容被拆成很多份副本、降低命中率；CDN 通常提供忽略特定參數的設定，名稱與用法查各家文件。&lt;/p>
&lt;p>方法方面，規範定義了 GET、HEAD、POST 三種方法的快取語意，而 PUT、DELETE 這類方法的回應不能快取（RFC 9110 §9.2.3、§9.3）。POST 的回應只有在帶明確期限、而且帶一個值和請求網址相同的 &lt;code>Content-Location&lt;/code> 時才能保存（RFC 9110 §9.3.3）；規範也指出，絕大多數實作只支援 GET 與 HEAD（「the overwhelming majority of cache implementations only support GET and HEAD」，RFC 9110 §9.2.3）。&lt;/p>
&lt;h2 id="vary-標頭加進快取鍵的請求標頭">Vary 標頭加進快取鍵的請求標頭&lt;/h2>
&lt;p>同一個網址可能依請求標頭回不同內容：依 &lt;code>Accept-Encoding&lt;/code> 回 gzip 或 brotli 壓縮的版本，依 &lt;code>Accept-Language&lt;/code> 回不同語言。origin 用 &lt;code>Vary&lt;/code> 標頭告訴快取：這個回應的內容跟哪些請求標頭有關，挑副本時這些標頭也要相符。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Vary: Accept-Encoding&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>In other words, Vary expands the cache key required to match a new request to the stored cache entry.（RFC 9110 §12.5.5）&lt;/p>&lt;/blockquote>
&lt;p>比對規則有幾個細節（RFC 9111 §4.1）：&lt;/p>
&lt;ul>
&lt;li>快取比對新請求與副本當初那個請求在 &lt;code>Vary&lt;/code> 列出的標頭值時，只允許去掉空白、合併同名標頭這類不改變語意的正規化，其餘要逐字相符。&lt;/li>
&lt;li>請求沒有帶某個 &lt;code>Vary&lt;/code> 列出的標頭時，只能和同樣沒有帶這個標頭的副本相符。&lt;/li>
&lt;li>&lt;code>Vary: *&lt;/code> 永遠不相符，等於這個回應每次被重用前都要先向 origin 驗證。&lt;/li>
&lt;/ul>
&lt;p>&lt;code>Vary&lt;/code> 列出的標頭值越分散，副本就越多。實測時同一個網址回 &lt;code>Vary: Accept-Encoding&lt;/code>，nginx 1.27 依請求標頭的原始字串分開保存：&lt;code>gzip&lt;/code>、&lt;code>br&lt;/code>、&lt;code>gzip, deflate, br&lt;/code> 與沒帶這個標頭各存一份。Varnish 7.7 在預設設定下會先把 &lt;code>Accept-Encoding&lt;/code> 正規化、向 origin 一律要 gzip、再自己替客戶端解壓，所以四種請求共用同一份副本。這是實作的處理，不是規範的要求；所用的快取產品怎麼處理 &lt;code>Vary&lt;/code>，要查它的文件。&lt;/p>
&lt;h2 id="帶-authorization-的請求在共用快取的重用規則">帶 Authorization 的請求在共用快取的重用規則&lt;/h2>
&lt;p>請求帶 &lt;code>Authorization&lt;/code> 標頭，代表回應可能只屬於那個使用者。規範對共用快取的規則是：除非回應用指令明確允許，否則不能把這種回應拿去回應其他請求。&lt;/p>
&lt;blockquote>
&lt;p>A shared cache MUST NOT use a cached response to a request with an Authorization header field &amp;hellip; to satisfy any subsequent request unless the response contains a Cache-Control field with a response directive &amp;hellip; that allows it to be stored by a shared cache（RFC 9111 §3.5）&lt;/p></description><content:encoded><![CDATA[<p>這篇整理快取收到一個請求時，怎麼決定要拿哪一份保存的副本回應它，或判定沒有可用的副本。這個判斷決定了一份副本會被哪些人拿到，所以也是個人化內容外洩到別人手上的成因所在。保存位置的分類見 <a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a>。本篇談的外洩發生在共用快取上：同一份副本被發給不同使用者。另一種「看到別人的畫面」發生在同一台裝置上，前一個使用者的頁面在按上一頁時被瀏覽器的 back/forward cache 還原，見 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a> 的〈上一頁與 back/forward cache〉。</p>
<h2 id="快取鍵的組成">快取鍵的組成</h2>
<p>快取用來挑選副本的資訊稱為<strong>快取鍵</strong>（cache key）。規範定義的最小組成是請求方法加上目標網址：</p>
<blockquote>
<p>The &ldquo;cache key&rdquo; is the information a cache uses to choose a response and is composed from, at a minimum, the request method and target URI（RFC 9111 §2）</p></blockquote>
<p>規範也指出，許多常用的快取只保存 GET 的回應，所以只用網址當快取鍵。網址包含查詢字串，<code>/products?id=1</code> 與 <code>/products?id=1&amp;utm_source=mail</code> 是兩個不同的快取鍵，各自保存一份副本。行銷追蹤參數這類不影響內容的查詢字串，會讓同一份內容被拆成很多份副本、降低命中率；CDN 通常提供忽略特定參數的設定，名稱與用法查各家文件。</p>
<p>方法方面，規範定義了 GET、HEAD、POST 三種方法的快取語意，而 PUT、DELETE 這類方法的回應不能快取（RFC 9110 §9.2.3、§9.3）。POST 的回應只有在帶明確期限、而且帶一個值和請求網址相同的 <code>Content-Location</code> 時才能保存（RFC 9110 §9.3.3）；規範也指出，絕大多數實作只支援 GET 與 HEAD（「the overwhelming majority of cache implementations only support GET and HEAD」，RFC 9110 §9.2.3）。</p>
<h2 id="vary-標頭加進快取鍵的請求標頭">Vary 標頭加進快取鍵的請求標頭</h2>
<p>同一個網址可能依請求標頭回不同內容：依 <code>Accept-Encoding</code> 回 gzip 或 brotli 壓縮的版本，依 <code>Accept-Language</code> 回不同語言。origin 用 <code>Vary</code> 標頭告訴快取：這個回應的內容跟哪些請求標頭有關，挑副本時這些標頭也要相符。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Vary: Accept-Encoding</span></span></code></pre></div><blockquote>
<p>In other words, Vary expands the cache key required to match a new request to the stored cache entry.（RFC 9110 §12.5.5）</p></blockquote>
<p>比對規則有幾個細節（RFC 9111 §4.1）：</p>
<ul>
<li>快取比對新請求與副本當初那個請求在 <code>Vary</code> 列出的標頭值時，只允許去掉空白、合併同名標頭這類不改變語意的正規化，其餘要逐字相符。</li>
<li>請求沒有帶某個 <code>Vary</code> 列出的標頭時，只能和同樣沒有帶這個標頭的副本相符。</li>
<li><code>Vary: *</code> 永遠不相符，等於這個回應每次被重用前都要先向 origin 驗證。</li>
</ul>
<p><code>Vary</code> 列出的標頭值越分散，副本就越多。實測時同一個網址回 <code>Vary: Accept-Encoding</code>，nginx 1.27 依請求標頭的原始字串分開保存：<code>gzip</code>、<code>br</code>、<code>gzip, deflate, br</code> 與沒帶這個標頭各存一份。Varnish 7.7 在預設設定下會先把 <code>Accept-Encoding</code> 正規化、向 origin 一律要 gzip、再自己替客戶端解壓，所以四種請求共用同一份副本。這是實作的處理，不是規範的要求；所用的快取產品怎麼處理 <code>Vary</code>，要查它的文件。</p>
<h2 id="帶-authorization-的請求在共用快取的重用規則">帶 Authorization 的請求在共用快取的重用規則</h2>
<p>請求帶 <code>Authorization</code> 標頭，代表回應可能只屬於那個使用者。規範對共用快取的規則是：除非回應用指令明確允許，否則不能把這種回應拿去回應其他請求。</p>
<blockquote>
<p>A shared cache MUST NOT use a cached response to a request with an Authorization header field &hellip; to satisfy any subsequent request unless the response contains a Cache-Control field with a response directive &hellip; that allows it to be stored by a shared cache（RFC 9111 §3.5）</p></blockquote>
<p>明確允許的指令是 <code>public</code>、<code>s-maxage</code> 與 <code>must-revalidate</code>。所以帶 <code>Authorization</code> 的請求，回應只寫 <code>max-age=60</code> 時，共用快取不能拿它回應別人；寫成 <code>public, max-age=60</code> 時可以，而且可以拿去回應任何請求，包括沒有帶 <code>Authorization</code> 的匿名請求：共用快取不替 origin 檢查權限。只對登入者開放、但每個登入者內容都相同的回應，寫 <code>public</code> 等於對匿名使用者公開，仍要寫 <code>private</code>，或靠 CDN 自己在邊緣驗證身分。<code>Vary</code> 不必列 <code>Authorization</code>，因為 RFC 9111 §3.5 這條規則已經禁止跨使用者重用（RFC 9110 §12.5.5）。</p>
<p>實測兩個共用快取在 RFC 9111 §3.5 這條規則上的表現完全相反：</p>
<ul>
<li><strong>Varnish 7.7</strong>：只要請求帶 <code>Authorization</code>，內建設定就不查快取、直接轉給 origin，連回應寫了 <code>public</code> 或 <code>s-maxage</code> 也一樣。這比規範保守。</li>
<li><strong>nginx 1.27 的 proxy_cache</strong>（預設設定）：使用者甲帶 <code>Authorization: Bearer t1</code> 請求一個只寫 <code>max-age=60</code> 的路徑，回應被保存下來；接著使用者乙帶 <code>Bearer t2</code>、以及完全沒帶 <code>Authorization</code> 的請求，都拿到了使用者甲的那份回應。這違反 RFC 9111 §3.5 的規則。</li>
</ul>
<p>nginx 的行為代表：origin 依靠「請求帶了 <code>Authorization</code>，快取就不會共用」這條規則來保護個人資料，在預設設定的 nginx 前面失效。開發與測試時多半只用一個帳號打同一個路徑，拿到的永遠是自己的資料，這種共用因此不容易在上線前被看到。需要登入才能拿到的回應，要在 origin 端明寫 <code>private</code> 或 <code>no-store</code>，不依賴快取對請求裡 <code>Authorization</code> 標頭的判斷。改標頭只約束之後產生的回應；已經存進共用快取的那份副本仍照它當初的標頭運作，到期之前會繼續發給別人，要讓它立刻停止，得在副本所在的快取上清除，CDN 與反向代理的清除方式見 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈purge〉。</p>
<h2 id="cookie-與-set-cookie-在共用快取的處理">Cookie 與 Set-Cookie 在共用快取的處理</h2>
<p>以 Cookie 識別使用者的服務，規範沒有對請求的 <code>Cookie</code> 標頭訂任何快取規則；它只是一個普通的請求標頭，除非 <code>Vary: Cookie</code>，否則不進快取鍵。回應的 <code>Set-Cookie</code> 也不會阻止保存：</p>
<blockquote>
<p>Note that the Set-Cookie response header field [COOKIE] does not inhibit caching; a cacheable response with a Set-Cookie header field can be (and often is) used to satisfy subsequent requests to caches.（RFC 9111 §7.3）</p></blockquote>
<p>規範允許共用快取把一個帶 <code>Set-Cookie: session=使用者甲的值</code> 的回應保存下來，再原封不動地回應給使用者乙，原文也寫明這種事常常發生（「can be (and often is)」）；這時使用者乙的瀏覽器就收到了使用者甲的 session。規範在同一段建議 origin 自己用 <code>Cache-Control</code> 控制這類回應。</p>
<p>實作多半對帶 <code>Cookie</code> 的請求與帶 <code>Set-Cookie</code> 的回應加了保護，但做法不同，實測結果：</p>
<ul>
<li><strong>Varnish 7.7</strong>：請求帶 <code>Cookie</code> 時內建設定一律不查快取；回應帶 <code>Set-Cookie</code> 時內建設定不保存。</li>
<li><strong>nginx 1.27</strong>：回應帶 <code>Set-Cookie</code> 時預設不保存；請求的 <code>Cookie</code> 不影響快取鍵，回應有 <code>Vary: Cookie</code> 時才依 Cookie 值分開保存。</li>
</ul>
<p>這些保護是實作的預設，可以被設定關掉，CDN 的預設也各不相同。</p>
<h2 id="個人化回應的標頭選擇">個人化回應的標頭選擇</h2>
<p>回應的內容因使用者而不同時，標頭層面的做法如下（<code>private</code> 那一列成立的前提是回應沒有另外帶 <code>CDN-Cache-Control</code> 或 <code>Surrogate-Control</code>，或這些欄位裡也寫了 <code>private</code>、<code>no-store</code>（Varnish 的 <code>Surrogate-Control</code> 只認 <code>no-store</code>），見 <a href="/blog/backend/12-http-caching/cache-storage-locations/" data-link-title="12.1 HTTP 回應的保存位置：私有快取與共用快取" data-link-desc="HTTP 回應離開 origin 之後可能被保存的位置：私有快取與共用快取的規範定義、瀏覽器、CDN、反向代理與轉送代理各由誰經營、規範列出的保存條件、只對共用快取或只對私有快取有效的指令，以及把指令指定給 CDN 的 CDN-Cache-Control">12.1 HTTP 回應的保存位置：私有快取與共用快取</a> 的〈把指令指定給特定快取：CDN-Cache-Control〉）：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>效果</th>
          <th>適用情形</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Cache-Control: private</code></td>
          <td>共用快取不保存，瀏覽器可以保存</td>
          <td>個人化頁面、需登入的 API 回應，允許使用者自己的瀏覽器重複使用</td>
      </tr>
      <tr>
          <td><code>Cache-Control: no-store</code></td>
          <td>任何快取都不保存</td>
          <td>內容不能留在使用者裝置上（例如共用電腦上的敏感資料）</td>
      </tr>
      <tr>
          <td><code>Vary: Cookie</code></td>
          <td>共用快取依 Cookie 值分開保存</td>
          <td>很少適用：Cookie 值通常每個使用者都不同，副本數接近使用者數，命中率接近零</td>
      </tr>
  </tbody>
</table>
<p>標頭以外還有一種做法：頁面只有一小塊因使用者而不同（例如右上角的使用者名稱）時，把那一塊拆成另一個帶 <code>private</code> 的請求，頁面主體就能讓共用快取保存；前提是共用快取不會因為請求帶 Cookie 就跳過快取，Varnish 的內建設定會，要在 VCL 裡移除頁面主體請求的 Cookie。</p>
<p>MDN 的建議與表中 <code>private</code> 那一列一致：「you should specify Cache-Control: private instead of specifying a cookie for Vary.」個人化回應寫 <code>private</code>，是讓「這份回應能不能給別人」由 origin 決定，而不是交給共用快取的預設設定決定；〈帶 Authorization 的請求在共用快取的重用規則〉與〈Cookie 與 Set-Cookie 在共用快取的處理〉兩節的實測顯示，共用快取的預設在「會不會把回應發給別的使用者」這一點上，不同實作的結果相反。從外部確認某個網址有沒有把一個使用者的回應發給別人，做法在 <a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a> 的〈檢查實際送出的快取標頭〉。</p>
<h2 id="沒有列進快取鍵的輸入與快取投毒">沒有列進快取鍵的輸入與快取投毒</h2>
<p>快取比對請求時只看快取鍵裡的部分。請求裡其他會改變回應內容的部分（某些請求標頭、某些查詢參數），PortSwigger 的研究稱為 unkeyed input，也就是沒有列進快取鍵的輸入。origin 依一個沒有列進快取鍵的輸入產生了不同的回應，而快取把這份回應存下來，在它過期之前，所有快取鍵相同的請求都會拿到它，不論那些請求有沒有帶那個輸入。攻擊者刻意利用這一點，送出帶惡意值的請求、讓有害的回應被存下來發給其他使用者，稱為 web cache poisoning（快取投毒）；James Kettle 在 2018 年 8 月發表於 PortSwigger 的研究〈Practical Web Cache Poisoning〉整理了這類實際案例。</p>
<p>一個常見的形態是框架讀取 <code>X-Forwarded-Host</code> 這類代理轉送用的請求標頭，拿它組出頁面裡的絕對網址。請求帶上攻擊者的網域時，回應裡的網址就指向攻擊者的伺服器；這個標頭不在快取鍵裡，被存下來的頁面就發給之後的每個使用者。這和本篇〈Vary 標頭加進快取鍵的請求標頭〉一節講的是同一件事的兩種結果：origin 依某個請求標頭改變回應時，那個標頭列進 <code>Vary</code>、或由快取設定加進快取鍵，不同的值就各存一份；沒有列進去，它就是一個沒有列進快取鍵的輸入，一個值產生的回應會被發給帶其他值的請求。</p>
<p>PortSwigger 列的防範裡，和快取鍵直接相關的有下面幾條：</p>
<ul>
<li>網站用不到的請求標頭，在快取或 origin 前停用，不讓框架讀到。這類標頭多半是代理或框架預設支援的，網站本身不需要。</li>
<li>為了命中率想把某個輸入排除在快取鍵外時，改成在快取前把請求改寫成正規化的值，讓同一個快取鍵只對應一種回應。</li>
<li>不接受帶 body 的 GET 請求，因為 GET 請求的 body 不在快取鍵裡，卻可能被 origin 讀取。</li>
</ul>
<p>其他還有把快取限縮在純靜態的回應、修補看似無法利用的前端漏洞，見研究原文。</p>
<h2 id="快取規則與-web-cache-deception">快取規則與 web cache deception</h2>
<p>web cache deception 和快取投毒是兩種不同的問題，PortSwigger 的文件也明確區分：快取投毒操弄的是快取鍵，讓攻擊者的有害回應被存下來發給別人；web cache deception 利用的是快取規則，讓受害者帶個人資料的回應被存下來，再被攻擊者取走。</p>
<p>CDN 常依副檔名（<code>.css</code>、<code>.js</code>）或目錄（<code>/static</code>）設定快取規則，把符合的網址當成可以保存的靜態檔，而快取與 origin 解析網址路徑的方式可能不同。攻擊者引誘已登入的使用者點一個在快取看來是靜態檔、在 origin 看來是個人頁面的網址，origin 回出帶個人資料的頁面，快取依規則存下它，攻擊者再請求同一個網址就拿到那份副本。這種情形下快取鍵本身沒有問題，出問題的是快取規則覆寫了 origin 對這份回應能不能保存的判斷。PortSwigger 的防範建議：</p>
<ul>
<li>動態回應明寫 <code>Cache-Control: no-store, private</code>。</li>
<li>CDN 的快取規則不覆寫 origin 的 <code>Cache-Control</code>。</li>
<li>啟用 CDN 比對回應的 <code>Content-Type</code> 與網址副檔名是否一致的保護。</li>
<li>確認 origin 與快取解析網址路徑的方式一致。</li>
</ul>
<h2 id="瀏覽器快取的分區">瀏覽器快取的分區</h2>
<p>瀏覽器上另有一層規範允許的做法：在快取鍵裡加入額外資訊。規範舉的例子是加入發出請求的網站：</p>
<blockquote>
<p>user agent caches might include the referring site&rsquo;s identity, thereby &ldquo;double keying&rdquo; the cache to avoid some privacy risks（RFC 9111 §2）</p></blockquote>
<p>WHATWG Fetch 規範把 HTTP 快取依「頂層網站」分區（Fetch 的 network partition key）：同一個第三方資源，在網站甲與網站乙上被載入時，各存一份，目的是防止網站透過「某個資源是否已經在快取裡」推測使用者造訪過哪些網站。它對 origin 的影響是：跨網站共用的資源（例如公共 CDN 上的函式庫），不能預期因為使用者在別的網站載入過而命中快取。各瀏覽器分區的確切方式查各自的文件。</p>
]]></content:encoded></item><item><title>Vary（快取鍵變體標頭）</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/vary/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/vary/</guid><description>&lt;p>快取找保存的回應時，規範要求至少比對請求方法與網址；同一個網址會因為某個請求標頭回出不同內容時（例如依 &lt;code>Accept-Encoding&lt;/code> 回 gzip 或 br 壓縮的版本），origin 在回應上寫 &lt;code>Vary: &amp;lt;標頭名稱&amp;gt;&lt;/code>，快取就把那個請求標頭的值也加進比對條件，不同的值各存一份、各自重用。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-control/" data-link-title="Cache-Control（快取控制標頭）" data-link-desc="看到回應帶 Cache-Control、或要決定某個回應能被誰保存、保存多久、使用前要不要先問 origin 時，查這個標頭各指令的分工">Cache-Control&lt;/a> 以外，回應影響「誰拿到哪一份保存內容」的另一個標頭。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Vary 屬於快取鍵這一層：Cache-Control 決定一份回應能不能保存、能用多久，Vary 決定保存的這一份可以拿去回應哪些請求。它與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">Cache Hit Rate&lt;/a> 直接相關，列進去的標頭值越分散，同一個網址就被切成越多份，每一份被重用的機會越少。&lt;code>Vary: *&lt;/code> 讓比對永遠失敗，這份回應每次被重用前都要先向 origin 驗證。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>&lt;code>Vary: Accept-Encoding&lt;/code> 是最常見的用法。規範只允許快取在比對前做去掉空白這類不改變語意的正規化，所以 &lt;code>gzip&lt;/code>、&lt;code>br&lt;/code>、&lt;code>gzip, deflate, br&lt;/code> 三種寫法照字面會被當成三種值；實作可以自己再正規化（例如 Varnish 預設把它收斂成同一份），各產品的做法不同。&lt;/p>
&lt;p>&lt;code>Vary: Cookie&lt;/code> 是另一端：Cookie 的值幾乎每個使用者都不同，共用快取存下的份數接近使用者數，命中率接近零。個人化回應的建議做法是寫 &lt;code>Cache-Control: private&lt;/code> 讓共用快取不保存，而不是用 &lt;code>Vary: Cookie&lt;/code>。&lt;/p>
&lt;h2 id="判讀方式">判讀方式&lt;/h2>
&lt;p>看到一個回應帶 Vary，先問列出的標頭在實際流量裡有幾種值：值少而且有意義（壓縮方式、語系）時 Vary 讓不同版本安全地共用快取；值幾乎一人一種（Cookie、未經正規化的完整 &lt;code>User-Agent&lt;/code> 字串）時它實質上關掉了共用快取，要改用 &lt;code>private&lt;/code> 或在快取前先把標頭正規化成少數幾種值。&lt;/p>
&lt;p>&lt;code>Authorization&lt;/code> 不必列進 Vary：規範已經禁止共用快取把帶 &lt;code>Authorization&lt;/code> 請求的回應拿去回應別人，除非回應用 &lt;code>public&lt;/code> 這類指令明確允許。這是規範的要求，實作不一定遵守（實測預設設定的 nginx 1.27 會共用），需登入的回應仍由 origin 明寫 &lt;code>private&lt;/code> 或 &lt;code>no-store&lt;/code>。反過來，自訂的請求標頭（例如以標頭指定 API 版本）沒有列進 Vary 時，快取會把一個版本的回應發給要另一個版本的請求。&lt;/p>
&lt;p>規範的比對規則、Varnish 與 nginx 的實測差異，以及 Cookie、Authorization 在共用快取上的處理，見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>快取找保存的回應時，規範要求至少比對請求方法與網址；同一個網址會因為某個請求標頭回出不同內容時（例如依 <code>Accept-Encoding</code> 回 gzip 或 br 壓縮的版本），origin 在回應上寫 <code>Vary: &lt;標頭名稱&gt;</code>，快取就把那個請求標頭的值也加進比對條件，不同的值各存一份、各自重用。它是 <a href="/blog/backend/knowledge-cards/cache-control/" data-link-title="Cache-Control（快取控制標頭）" data-link-desc="看到回應帶 Cache-Control、或要決定某個回應能被誰保存、保存多久、使用前要不要先問 origin 時，查這個標頭各指令的分工">Cache-Control</a> 以外，回應影響「誰拿到哪一份保存內容」的另一個標頭。</p>
<h2 id="概念位置">概念位置</h2>
<p>Vary 屬於快取鍵這一層：Cache-Control 決定一份回應能不能保存、能用多久，Vary 決定保存的這一份可以拿去回應哪些請求。它與 <a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">Cache Hit Rate</a> 直接相關，列進去的標頭值越分散，同一個網址就被切成越多份，每一份被重用的機會越少。<code>Vary: *</code> 讓比對永遠失敗，這份回應每次被重用前都要先向 origin 驗證。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p><code>Vary: Accept-Encoding</code> 是最常見的用法。規範只允許快取在比對前做去掉空白這類不改變語意的正規化，所以 <code>gzip</code>、<code>br</code>、<code>gzip, deflate, br</code> 三種寫法照字面會被當成三種值；實作可以自己再正規化（例如 Varnish 預設把它收斂成同一份），各產品的做法不同。</p>
<p><code>Vary: Cookie</code> 是另一端：Cookie 的值幾乎每個使用者都不同，共用快取存下的份數接近使用者數，命中率接近零。個人化回應的建議做法是寫 <code>Cache-Control: private</code> 讓共用快取不保存，而不是用 <code>Vary: Cookie</code>。</p>
<h2 id="判讀方式">判讀方式</h2>
<p>看到一個回應帶 Vary，先問列出的標頭在實際流量裡有幾種值：值少而且有意義（壓縮方式、語系）時 Vary 讓不同版本安全地共用快取；值幾乎一人一種（Cookie、未經正規化的完整 <code>User-Agent</code> 字串）時它實質上關掉了共用快取，要改用 <code>private</code> 或在快取前先把標頭正規化成少數幾種值。</p>
<p><code>Authorization</code> 不必列進 Vary：規範已經禁止共用快取把帶 <code>Authorization</code> 請求的回應拿去回應別人，除非回應用 <code>public</code> 這類指令明確允許。這是規範的要求，實作不一定遵守（實測預設設定的 nginx 1.27 會共用），需登入的回應仍由 origin 明寫 <code>private</code> 或 <code>no-store</code>。反過來，自訂的請求標頭（例如以標頭指定 API 版本）沒有列進 Vary 時，快取會把一個版本的回應發給要另一個版本的請求。</p>
<p>規範的比對規則、Varnish 與 nginx 的實測差異，以及 Cookie、Authorization 在共用快取上的處理，見 <a href="/blog/backend/12-http-caching/cache-key/" data-link-title="12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization" data-link-desc="快取用什麼條件挑出一份副本回應請求：快取鍵的組成、Vary 如何把請求標頭加進快取鍵、帶 Authorization 的請求在共用快取的規則、Cookie 與 Set-Cookie 在規範與實作上的處理，個人化回應的標頭選擇，以及沒有列進快取鍵的輸入造成的快取投毒與快取規則造成的 web cache deception">12.4 快取鍵與副本共用：Vary、Cookie 與 Authorization</a>。</p>
]]></content:encoded></item></channel></rss>