返回博客列表

Cache-Control里的public、private、no-cache、no-store怎么选?缓存配置与排查指南

CdnChart 技术团队发布于 2026-10-0813 分钟阅读
Cache-Control里的public、private、no-cache、no-store怎么选?缓存配置与排查指南

修改了网页,用户却仍然看到旧内容;登录后显示了其他用户的信息;接口已经返回no-cache,CDN控制台里却还能看到缓存记录——这些问题经常被归结为“缓存没有清理”,实际原因往往是没有理解Cache-Control各项指令控制的到底是什么。

public、private、no-cache和no-store并不是四档从“缓存最多”到“缓存最少”的开关,它们主要回答三个不同问题:

  1. 响应能不能被存储?

  2. 可以存到浏览器,还是也能存到CDN、反向代理等共享缓存?

  3. 已存储的响应再次使用前,是否必须向源站验证?

最容易记住的判断是:

  • public:允许浏览器和共享缓存存储;

  • private:只允许用户自己的私有缓存存储,不应进入CDN等共享缓存;

  • no-cache:可以存储,但每次复用前必须向源站验证;

  • no-store:任何缓存都不应存储这次请求或响应。

其中最容易误解的是no-cache。它不是“不缓存”,真正表示禁止存储的是no-store。

先分清浏览器缓存和CDN缓存

一个经过CDN的网站通常至少存在两层HTTP缓存:

源站
  │
  │ 源站响应与Cache-Control
  ▼
CDN边缘节点:共享缓存
  │
  │ CDN返回的响应
  ▼
用户浏览器:私有缓存

浏览器缓存只服务于当前用户,通常被称为私有缓存。CDN、企业代理和反向代理可能同时服务许多用户,属于共享缓存。

这一区别非常重要。某个登录页面允许当前用户的浏览器短暂存储,不代表它可以进入CDN缓存;否则,CDN可能把第一个用户的个性化响应发送给其他用户。

根据《RFC 9111:HTTP Caching》,private禁止共享缓存存储响应,但不禁止私有缓存存储;public则可以明确允许共享缓存存储原本可能不具备共享缓存资格的响应。

四个指令的核心区别

指令

浏览器可以存储

CDN可以存储

复用前是否验证

常见用途

public

可以

可以

由max-age等指令决定

公共图片、CSS、JS、公开页面

private

可以

不应存储

由max-age或no-cache决定

用户中心、个性化页面

no-cache

可以

可以,除非同时有private

每次复用前必须验证

经常变化但可使用ETag验证的内容

no-store

不应存储

不应存储

无缓存可复用

高敏感响应、一次性数据

需要注意,public和private主要限制“由谁存储”,no-cache和no-store主要限制“怎样存储及复用”。因此它们可以组合,例如:

Cache-Control: private, no-cache

表示浏览器可以保存响应,但每次使用前都要向源站验证;CDN等共享缓存不应保存。

public:允许CDN缓存,但不代表已经设置缓存时间

下面这条响应头明确允许浏览器和共享缓存存储内容:

Cache-Control: public, max-age=3600

它表示响应可以被存储,并在接下来3600秒内被视为新鲜内容。在这段时间内,缓存通常可以直接复用,不必访问源站。

public适合所有用户看到相同结果的资源,例如:

  • 不含用户信息的图片、字体、CSS和JavaScript;

  • 公开下载文件;

  • 所有人内容一致的文章页面;

  • 不根据Cookie、认证状态或地区返回不同结果的公开接口。

但只写下面这条通常不够明确:

Cache-Control: public

public只是明确授予缓存资格,并没有指定内容可以新鲜多久。缓存可能根据状态码、Last-Modified等信息进行启发式缓存,不同浏览器和CDN的处理也可能不同。生产环境通常应同时给出明确的max-age或s-maxage。

对于文件名带内容哈希、更新时URL一定变化的静态资源,可以使用:

Cache-Control: public, max-age=31536000, immutable

例如:

/app.83f7c21a.js
/styles.192b8d44.css

一年缓存适用于“内容一变,URL就跟着变”的资源。如果一直使用/app.js这个固定地址,却设置一年浏览器缓存,发布新版本后就很容易出现用户长期拿到旧文件的问题。

public不能用于不受控制的个性化响应

假设下面这个地址会根据登录Cookie返回不同内容:

https://www.example.com/api/profile

如果响应被配置为:

Cache-Control: public, max-age=300

而CDN缓存键又没有正确区分用户,用户A的资料就可能被缓存后返回给用户B。

不要把“CDN缓存键包含Cookie”当作默认事实。不同CDN和缓存规则对Cookie、查询参数、请求头的处理并不相同,而且把完整Cookie加入缓存键还可能造成缓存碎片和命中率骤降。

对于用户个性化内容,更稳妥的做法通常是使用private或no-store,而不是试图把每一个用户的响应放入公共CDN缓存。

private:禁止共享缓存,不等于浏览器不缓存

典型配置是:

Cache-Control: private, max-age=300

它表示响应只能存储在私有缓存中,例如当前用户的浏览器缓存,并可在300秒内直接复用。CDN、共享代理等不应存储该响应。

适合使用private的内容包括:

  • 登录后的用户中心页面;

  • 根据Cookie展示不同状态的HTML;

  • 用户个人偏好或非敏感配置;

  • 仅对当前用户有意义、但允许浏览器短暂复用的数据。

如果内容必须每次确认是否更新,可以使用:

Cache-Control: private, no-cache

这并不会完全禁止浏览器保存响应。浏览器仍可以保存内容,但复用前必须向源站验证。

按照MDN的Cache-Control说明,private尤其适合登录后或由Cookie维持会话的个性化响应;如果遗漏该限制,响应有可能进入共享缓存并造成个人信息泄露。

不过,private不是数据安全机制。它依赖缓存遵守HTTP规范,也不会对响应内容进行加密。敏感信息仍然需要HTTPS、可靠的认证授权、会话失效和服务端访问控制。

no-cache:可以存储,但使用前必须验证

下面这条配置经常被误认为“完全关闭缓存”:

Cache-Control: no-cache

它真正表达的是:缓存可以存储这个响应,但不能未经源站验证就直接拿来满足后续请求。

验证过程通常依赖以下响应头:

ETag: "page-a81f3"

或者:

Last-Modified: Wed, 23 Sep 2026 08:00:00 GMT

浏览器或CDN下次请求时可以发送条件请求:

If-None-Match: "page-a81f3"

如果内容没有变化,源站返回:

HTTP/1.1 304 Not Modified

缓存便可以继续使用原来的响应体,不必重新下载完整内容。如果已经发生变化,源站返回新的200响应和内容。

这就是no-cache的价值:保证使用前经过验证,同时保留304响应带来的传输效率。RFC 9111明确规定,未携带字段参数的no-cache响应在成功完成验证前,不得用于满足其他请求。

适合no-cache的场景包括:

  • URL固定但内容可能随时更新的HTML;

  • 配置文件、版本清单或应用入口文件;

  • 希望用户每次访问都检查更新,但内容未变化时避免重新传输;

  • 更新频繁、同时已正确设置ETag或Last-Modified的公开资源。

例如SPA应用可以把入口HTML设置为:

Cache-Control: no-cache
ETag: "index-20260924"

HTML每次访问都会验证,HTML引用的带哈希JS和CSS则可以长期缓存。

没有验证器时,no-cache可能无法节省多少流量

如果响应没有ETag或Last-Modified,缓存仍需向源站发起请求,但源站可能只能重新返回完整的200响应。

因此,设置no-cache后应同步检查验证器:

curl -sS -D - -o /dev/null https://www.example.com/index.html

重点查看:

Cache-Control
ETag
Last-Modified

假设第一次响应包含:

ETag: "index-20260924"

可以手动验证条件请求:

curl -sS -D - -o /dev/null \
  -H 'If-None-Match: "index-20260924"' \
  https://www.example.com/index.html

内容未变化时,正常结果通常是304 Not Modified。如果仍返回200,需要检查应用、Web服务器或CDN是否正确处理并转发If-None-Match。

no-store:不允许缓存存储,但不是完整的隐私方案

需要禁止浏览器、CDN和其他HTTP缓存保存响应时,应使用:

Cache-Control: no-store

适合的场景包括:

  • 包含高度敏感个人信息的响应;

  • 支付、账户安全或一次性操作结果;

  • 动态生成的密钥、令牌或敏感下载地址;

  • 不应在共享设备本地缓存中留下副本的页面;

  • 明确要求每次都从服务端重新获取的数据。

no-store比no-cache限制更强。前者不应存储,后者可以存储但必须验证。

不过,RFC 9111对no-store的说明同时提醒,它并不是充分的隐私保护措施。恶意或不合规的缓存可能不遵守指令,网络链路也可能面临其他风险。敏感内容仍必须使用HTTPS,并配合服务端认证、授权、会话失效和必要的数据脱敏。

还有一个容易遗漏的问题:把源站响应从public改成no-store,不能保证CDN中已经存在的旧对象立即消失。旧缓存可能在新请求到达源站之前继续被使用。

发生这种情况时,需要同时执行:

  1. 在源站改为正确的Cache-Control;

  2. 清理CDN中已经存储的对应URL;

  3. 必要时更换资源URL或版本号;

  4. 检查浏览器是否还保留旧版本;

  5. 确认后续响应已经带上新策略。

不同内容应该怎样配置

带哈希的CSS、JS和图片

资源一旦变化就生成新URL:

Cache-Control: public, max-age=31536000, immutable

这种方式适合充分利用浏览器和CDN缓存。发布新版本时不要覆盖旧URL,而是让HTML引用新文件名。

URL固定、需要及时更新的HTML

如果每次访问都要确认版本:

Cache-Control: no-cache
ETag: "page-version"

如果允许浏览器短暂缓存、CDN缓存时间稍长:

Cache-Control: public, max-age=60, s-maxage=300

这里的max-age=60主要控制浏览器新鲜时间,s-maxage=300控制共享缓存。是否完全按照这个策略执行,还要检查CDN规则。

登录后的个性化页面

允许浏览器保存,但禁止CDN保存:

Cache-Control: private, no-cache

如果页面包含较敏感信息,或不希望保存在用户设备的HTTP缓存中:

Cache-Control: no-store

不要仅因为响应带有Set-Cookie就默认认为CDN一定不会缓存。部分CDN默认会绕过,另一些缓存规则可能覆盖这种行为,必须结合当前厂商配置验证。

公开API

所有用户在相同请求条件下都会得到相同结果,并允许短暂缓存时:

Cache-Control: public, max-age=30, s-maxage=300

如果响应会根据Accept-Language或Origin变化,还要正确设计缓存键,并根据实际情况返回Vary:

Vary: Accept-Language

不能只加public,却忽略决定响应内容的请求头、查询参数和Cookie。

用户资料和认证接口

普通用户资料通常可以使用:

Cache-Control: private, no-cache

令牌、支付结果、密码重置信息等敏感响应通常应使用:

Cache-Control: no-store

无论使用哪一种,都不能把缓存指令当成权限校验。未授权用户是否能够取得数据,必须由服务端认证和授权逻辑决定。

public、private能和no-cache、no-store一起写吗?

有些组合是有明确意义的。

public, no-cache

Cache-Control: public, no-cache

允许浏览器和共享缓存存储,但每次复用前都要验证。适合公开且需要保持最新、同时支持ETag验证的内容。

private, no-cache

Cache-Control: private, no-cache

只允许私有缓存保存,并要求每次复用前验证。适合用户专属但不要求完全禁止本地存储的响应。

private, no-store

Cache-Control: private, no-store

no-store已经禁止所有缓存存储,因此private通常没有增加实质约束。为了让策略简洁清楚,一般直接使用:

Cache-Control: no-store

no-cache, no-store

这也是常见的兼容性写法,但在现代HTTP语义中,既然已经禁止存储,复用前验证就没有多少实际意义。除非需要兼容特定旧系统或框架,通常不必机械叠加。

为什么源站返回no-cache,CDN仍可能缓存

不能只看源站的响应头就断定CDN一定如何处理。CDN控制台中的缓存规则、边缘TTL、最小TTL和强制缓存配置,可能覆盖源站策略。

例如,Cloudflare官方说明中区分了是否启用Origin Cache Control:启用时,no-cache可以存储但每次都重新验证;某些未启用的配置下则会直接不缓存。缓存规则还可以覆盖源站的Cache-Control。

Amazon CloudFront的官方文档也特别警告:如果缓存策略的Minimum TTL大于0,即使源站返回no-cache、no-store或private,CloudFront仍可能至少缓存到Minimum TTL指定的时间。

因此,遇到“响应头明明不允许缓存,CDN却还在命中”时,应按下面顺序检查:

应用生成的Cache-Control
        ↓
Nginx、Apache或网关是否覆盖/追加响应头
        ↓
CDN缓存规则是否覆盖源站TTL
        ↓
CDN缓存键是否包含必要参数
        ↓
旧缓存是否已经清理
        ↓
浏览器是否仍保留旧副本

不同CDN的命中响应头并不相同。可以先通过CDNChart的CDN检测工具辅助确认当前域名是否经过CDN以及可能使用的服务商,再进入对应厂商控制台检查规则。

用curl检查实际返回的缓存策略

检查公开访问路径时,可以执行:

curl -sS -D - -o /dev/null https://www.example.com/app.js

重点查看:

Cache-Control
Age
ETag
Last-Modified
Expires
Vary
Via
X-Cache
CF-Cache-Status
Set-Cookie

其中:

  • Cache-Control表示响应提供的缓存指令;

  • Age通常表示响应在共享缓存中已经存放的秒数;

  • ETag和Last-Modified用于条件验证;

  • Vary说明哪些请求头可能影响缓存版本;

  • X-Cache、CF-Cache-Status等是厂商相关线索,并非所有CDN都会提供;

  • Set-Cookie可能影响缓存资格,但具体行为取决于厂商和规则。

连续请求两次可以观察变化:

curl -sS -D - -o /dev/null https://www.example.com/app.js
curl -sS -D - -o /dev/null https://www.example.com/app.js

如果第二次出现HIT或Age增加,通常说明共享缓存正在复用对象。如果始终是MISS、BYPASS或没有Age,可能是响应不可缓存,也可能只是该CDN使用了不同的调试字段。

不要仅依赖curl -I。它发送的是HEAD请求,部分应用或CDN对HEAD与GET的处理不同。上面的-D - -o /dev/null会执行正常GET请求,只是不把响应体输出到终端。

也可以使用CDNChart的网站测速工具观察不同地区公开访问时的状态码和性能表现。不过,公开测速只能辅助发现缓存效果差异,不能代替CDN控制台日志和源站响应头检查。有关平台测试口径,可以参考测评方法与数据说明。

修改缓存配置后怎样确认真正生效

缓存策略变更完成后,不要只刷新一次浏览器。建议至少验证以下内容:

  • 源站直连响应是否已经返回新Cache-Control;

  • 通过CDN访问时,响应头是否被边缘规则改写;

  • 已存在的旧对象是否完成清理;

  • 静态资源第二次请求是否按预期命中;

  • HTML是否按照设置重新验证或过期;

  • 登录和未登录状态是否返回不同且正确的内容;

  • 两个不同账号是否可能取得相同的个性化缓存对象;

  • 查询参数、Cookie和关键请求头是否正确进入缓存键;

  • 修改内容后,用户是否可以在预期时间内看到新版本。

浏览器开发者工具中的“Disable cache”通常只在开发者工具打开时影响当前浏览器请求,不能代表普通用户的访问状态。强制刷新也可能携带额外的请求缓存指令。因此,排查时应同时测试正常访问、无痕窗口、CDN节点和源站直连结果。

常见问题

no-cache是不是完全不缓存?

不是。no-cache允许缓存存储响应,但要求每次复用前向源站验证。如果需要任何HTTP缓存都不保存,应使用no-store。

private设置后,浏览器还会缓存吗?

可能会。private禁止的是CDN、代理等共享缓存,浏览器等私有缓存仍可保存。实际保存多久还要结合max-age、Expires或no-cache判断。

public不写max-age可以吗?

语法上可以,但缓存新鲜时间可能依赖其他响应头或启发式规则,行为不够明确。生产环境通常应为可缓存内容提供明确的max-age或s-maxage。

max-age=0和no-cache完全一样吗?

两者在很多实际场景中都会导致响应立即过期并触发验证,但语义并不完全相同。max-age=0表示响应立即变旧,no-cache明确要求每次复用前验证。再叠加CDN自身规则后,两者的实际表现还可能不同。

已经设置no-store,为什么用户仍看到旧内容?

旧内容可能在修改响应头之前就已经存入CDN或浏览器。新的no-store响应无法保证旧缓存立即被删除,需要清理CDN缓存、检查浏览器副本,必要时更换资源URL。

public响应带Set-Cookie会不会被CDN缓存?

不一定。许多CDN默认不会缓存带Set-Cookie的响应,但缓存规则可能改变这一行为。不要依赖未经验证的默认设置,更不要对包含用户身份信息的响应强制公共缓存。

使用no-store后,退出登录就一定不能看到旧页面吗?

不能这样保证。HTTP缓存只是浏览器状态的一部分,浏览器还可能使用历史记录或页面快照机制。退出登录时仍需在服务端使会话或令牌失效,敏感接口必须重新校验权限,不能依赖缓存头完成访问控制。

CDN显示HIT,但响应里写着no-cache,是否一定配置错误?

不一定。no-cache允许存储,只是不允许未经验证直接复用。CDN可能已经向源站完成验证后继续使用缓存对象。不过,也需要检查厂商的缓存状态定义、边缘规则和Age变化,确认它是否真的按要求执行了验证。

  • Cache-Control public
  • Cache-Control private
  • no-cache和no-store区别
  • CDN缓存设置
  • 浏览器缓存控制
  • HTTP缓存配置