登录后的页面被共享缓存存了一份,缓存头里的私有与不可存该怎么写
登录后的页面被共享缓存存了一份,问题通常出在缓存头写得不完整。按 MDN 的定义,no-store 表示任何类型的缓存(私有或共享)都不应存储该响应;private 表示响应只能存于私有缓存,也就是浏览器本地缓存。两者的区别不是「缓存多久」,而是「允许谁存」。带登录态的页面只写过期时间而不写归属,共享缓存就完全可能把那一份响应复用给后面的其他用户。
两类缓存分别是谁
私有缓存位于使用者一侧,典型是浏览器本地缓存,天然按用户隔离,存了个性化内容通常不构成泄露。
共享缓存位于源站与客户端之间,例如反向代理、CDN 节点。MDN 的描述很清楚:它存一份响应并复用给多个用户。这正是风险的来源——共享缓存不理解登录态,它只按缓存键判断是不是同一个请求。
还有一类容易被忽略:应用内部的响应缓存与页面静态化产物。它们的性质介于两者之间,取决于生成时机是否包含用户身份。凡是「生成时知道当前用户、输出时不再区分用户」的缓存,都应按共享缓存对待。
不可存与仅私有各自含义
两者的适用边界要分清,写错方向比不写更麻烦。
| 指令 | 语义 | 适用对象 | 误用后果 |
|---|---|---|---|
| no-store | 任何缓存都不得存该响应 | 含凭据、交易、个人数据的页面 | 加了它,性能优化完全无从谈起 |
| private | 只允许私有缓存存 | 因人而异但敏感性不高的页面 | 若下游仍强缓存,等于没生效 |
| public + 过期时间 | 允许共享缓存存 | 与身份无关的公开内容 | 个性化页面写错会串给用户 |
选择顺序建议这样问:这个响应里有没有只属于当前用户的信息?如果有且不能容忍被他人读到,用 no-store。如果内容因人而异但敏感性有限、且缓存能带来明显收益,用 private。如果与身份无关,才可以进共享缓存。
一个常见误解是「private 会让 CDN 不缓存」。更准确的说法是:private 是要求共享缓存不要存它,但一些中间层的行为并不完全遵循该语义。因此对真正敏感的响应,no-store 才是确定性更强的写法。
会员分组页面为什么更容易出问题
站内支持用户管理和 VIP 分组,可以为不同用户组设置访问权限。这类分层能力一旦与缓存相遇,就会形成典型的串页问题:同一个地址,因登录用户所属用户组不同而返回不同内容,但缓存键里没有体现这个差异。
三类页面最容易被漏掉。一是登录后显示的会员专区入口,不同分组看到的内容不同;二是价格、下单与订单相关页面,权限决定可见范围;三是下载或附件类页面,权限校验在应用层,而响应一旦进了共享缓存,后续的访问就不再触发校验。
第三种尤其危险:它给人的错觉是「我程序里做了权限判断,所以安全」。但缓存会跳过程序,权限判断根本没被执行。
判断标准应当是「响应是否因人而异」,而不是「页面是否重要」。凡是渲染时读过用户身份、分组或权限的响应,都默认排除在共享缓存之外,再按需要单独放开与身份无关的子资源。
反向代理与加速节点上的副作用
链路上多一层缓存,就多一份风险,因为规则分散在两处:应用写响应头,中间层按自己的策略决定是否遵循。站点部署时如果同时存在反向代理与加速节点,这两层要一起核对,只调整应用侧常常不够。
要核对四件事。第一,中间层是否有强制缓存策略。有些配置会覆盖上游指令,此时应用侧写什么都没用。第二,命中缓存时响应头是否被改写。若缓存后头信息丢失,从客户端看不出这份内容来自缓存。第三,退出登录后缓存里的旧响应还在不在。登录态变化不会自动失效共享缓存中的对象。第四,静态化产物是否按用户区分目录。把登录页写成静态文件,是同类问题里最难被发现的一种。
三步自查
第一步,用两个不同身份的账号访问同一地址,比较响应是否真的不同。若第二次访问拿到了第一次的内容,说明命中了不该有的共享缓存。
第二步,看响应头里的归属声明与缓存标识,确认这条响应被哪一层存过。缺失声明的响应要补,带有缓存命中痕迹的登录态响应要立刻处理。
第三步,检查退出登录路径。可靠的实现应当让登录后的跳转地址不进入共享缓存,而不是依赖用户端清理。
常见问题
全站统一加 no-store 可以吗? 技术上可以,但会把公开内容的缓存收益一起牺牲,站点负载明显上升。应按响应类型区分,而不是全局一刀切。
只在登录接口加够吗? 不够。问题出在渲染了登录态的页面响应上,接口只是其中一部分。
CDN 上的 private 会不会生效? 不同实现的行为不完全一致,敏感响应用 no-store 更确定。上线前应在真实节点上验证一次,而不是依赖文档描述。
静态化怎么做才安全? 与身份无关的页面才静态化;会员相关内容改为按权限判断动态渲染,或者把差异收敛到不缓存的小块区域。
怎么判断有没有历史响应残留? 观察退出登录后是否仍能访问到旧内容,或核对缓存层的对象存在时间。发现残留时先关闭该路径的缓存,再排查生成逻辑。