<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Web on 吾爱主机</title><link>https://blog.waihost.com/categories/web/</link><description>Recent content in Web on 吾爱主机</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 25 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://blog.waihost.com/categories/web/index.xml" rel="self" type="application/rss+xml"/><item><title>TLS 1.3 握手与证书链校验：原理、流程与工程排查</title><link>https://blog.waihost.com/posts/tls-1-3-handshake-certificate-chain/</link><pubDate>Sat, 25 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/tls-1-3-handshake-certificate-chain/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/tls-1-3-handshake-certificate-chain.svg" alt="Featured image of post TLS 1.3 握手与证书链校验：原理、流程与工程排查" /&gt;&lt;p&gt;线上偶发 “证书不受信任”“握手失败”“只能 HTTP 能开、HTTPS 间歇 525”，根因往往不在业务代码，而在 &lt;strong&gt;TLS 握手协商&lt;/strong&gt; 与 &lt;strong&gt;X.509 证书路径校验&lt;/strong&gt;。浏览器地址栏的小锁背后，是一次版本/密码套件协商、密钥交换、身份证明和密钥确认；服务端 PEM 链顺序错一环，客户端就会在完全不同的报错路径上失败。&lt;/p&gt;
&lt;p&gt;本文基于 &lt;strong&gt;RFC 8446（TLS 1.3）&lt;/strong&gt;、&lt;strong&gt;RFC 5280（PKIX 证书与路径校验）&lt;/strong&gt;，并对照 MDN 的 TLS/HTTPS 说明、OpenSSL &lt;code&gt;s_client&lt;/code&gt; 与 Nginx &lt;code&gt;ngx_http_ssl_module&lt;/code&gt; 官方文档，把机制、可复现排查命令与工程配置落到同一条链路上。&lt;/p&gt;
&lt;h2 id="问题背景tls-到底在保护什么"&gt;&lt;a href="#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%aftls-%e5%88%b0%e5%ba%95%e5%9c%a8%e4%bf%9d%e6%8a%a4%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;问题背景：TLS 到底在保护什么
&lt;/h2&gt;&lt;p&gt;MDN 对 Transport Layer Security（TLS）的定位很直接：让客户端在不可信网络上与服务器 &lt;strong&gt;安全通信&lt;/strong&gt;。在 Web 上，用 TLS 保护 HTTP 的结果就是 &lt;strong&gt;HTTPS&lt;/strong&gt;。TLS 从三方面加固连接：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;目标&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;机密性（Encryption）&lt;/td&gt;
					&lt;td&gt;传输中数据被加密，窃听者难以直接读内容&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;完整性（Integrity）&lt;/td&gt;
					&lt;td&gt;中间人无法在不被发现的情况下篡改数据&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;身份认证（Authentication）&lt;/td&gt;
					&lt;td&gt;一端能向另一端证明“我是谁”&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在公开 Web 上，&lt;strong&gt;服务端向客户端认证&lt;/strong&gt; 是常态；&lt;strong&gt;客户端向服务端出示证书&lt;/strong&gt; 相对少见（多见于 mTLS / 专用 API）。HTTPS 也是对抗 &lt;strong&gt;中间人（MITM）&lt;/strong&gt; 的关键防线：攻击者插在浏览器与站点之间时，若无法通过证书路径与握手完整性校验，就难以“静默”读写业务流量。&lt;/p&gt;
&lt;p&gt;因此工程排错要分清两层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;握手层&lt;/strong&gt;：版本、密码套件、密钥交换、Finished 校验是否完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信任层&lt;/strong&gt;：证书链能否锚定到本地信任锚、名字是否匹配、是否在有效期内、是否被吊销策略拒绝。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="核心原理tls-13-握手的三个阶段"&gt;&lt;a href="#%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86tls-13-%e6%8f%a1%e6%89%8b%e7%9a%84%e4%b8%89%e4%b8%aa%e9%98%b6%e6%ae%b5" class="header-anchor"&gt;&lt;/a&gt;核心原理：TLS 1.3 握手的三个阶段
&lt;/h2&gt;&lt;p&gt;RFC 8446 说明：安全通道的密码参数由 &lt;strong&gt;握手协议&lt;/strong&gt; 产生。握手用于协商协议版本、选择密码算法、可选地互相认证，并建立共享密钥材料；完成后双方用该密钥保护应用层流量。握手失败或其它协议错误会终止连接（可选先发 alert）。&lt;/p&gt;
&lt;p&gt;TLS 1.3 支持三种基本密钥交换模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;(EC)DHE&lt;/strong&gt;（有限域或椭圆曲线上的 Diffie-Hellman）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;仅 PSK&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PSK 与 (EC)DHE 组合&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="完整握手消息流1-rtt"&gt;&lt;a href="#%e5%ae%8c%e6%95%b4%e6%8f%a1%e6%89%8b%e6%b6%88%e6%81%af%e6%b5%811-rtt" class="header-anchor"&gt;&lt;/a&gt;完整握手消息流（1-RTT）
&lt;/h3&gt;&lt;p&gt;规范 Figure 1 给出了基本完整握手（&lt;code&gt;*&lt;/code&gt; 表示可选/场景相关；&lt;code&gt;{}&lt;/code&gt; 表示用握手流量密钥保护；&lt;code&gt;[]&lt;/code&gt; 表示用应用流量密钥保护）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Client Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Key ^ ClientHello
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Exch | + key_share*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | + signature_algorithms*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | + psk_key_exchange_modes*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v + pre_shared_key* --------&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerHello ^ Key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; + key_share* | Exch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; + pre_shared_key* v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {EncryptedExtensions} ^ Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {CertificateRequest*} v Params
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {Certificate*} ^
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {CertificateVerify*} | Auth
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {Finished} v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;-------- [Application Data*]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ^ {Certificate*}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Auth | {CertificateVerify*}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v {Finished} --------&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; [Application Data] &amp;lt;-------&amp;gt; [Application Data]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可按三个阶段理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Key Exchange（密钥交换）&lt;/strong&gt;&lt;br&gt;
客户端发送 &lt;code&gt;ClientHello&lt;/code&gt;（含随机数、可接受版本、对称密码/HKDF 哈希对列表，以及 &lt;code&gt;key_share&lt;/code&gt; / &lt;code&gt;pre_shared_key&lt;/code&gt; 等扩展）。服务端回 &lt;code&gt;ServerHello&lt;/code&gt; 选定连接参数；若用 (EC)DHE，服务端 &lt;code&gt;key_share&lt;/code&gt; &lt;strong&gt;必须&lt;/strong&gt; 落在客户端给出的某一组上。ClientHello 与 ServerHello 共同决定共享密钥。&lt;strong&gt;此阶段之后的消息均加密。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Server Parameters（服务端参数）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EncryptedExtensions&lt;/code&gt;：对那些不决定密码参数、也不绑定单张证书的 ClientHello 扩展作应答。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CertificateRequest&lt;/code&gt;：若需要基于证书的客户端认证则出现；否则省略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Authentication（认证）&lt;/strong&gt;&lt;br&gt;
证书类认证使用同一套消息：&lt;code&gt;Certificate&lt;/code&gt;、&lt;code&gt;CertificateVerify&lt;/code&gt;、&lt;code&gt;Finished&lt;/code&gt;（PSK 认证则作为密钥交换的副作用发生）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Certificate&lt;/strong&gt;：端点证书及链上支撑证书。约定使用证书认证时服务端 &lt;strong&gt;必须&lt;/strong&gt; 发送；客户端仅当收到 &lt;code&gt;CertificateRequest&lt;/code&gt; 时发送。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CertificateVerify&lt;/strong&gt;：证明持有与证书对应的私钥，并对握手到该点的完整性提供签名保护；服务器基于证书认证时 &lt;strong&gt;必须&lt;/strong&gt; 发送，且紧挨在 Certificate 之后、Finished 之前。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finished&lt;/strong&gt;：认证块的最后一条消息，用派生自 Base Key 的 MAC 对握手 transcript 做确认；收方必须校验，错误则 &lt;code&gt;decrypt_error&lt;/code&gt; 终止。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;双方都发送并成功校验对端 Finished 后，才可在连接上收发应用数据（0-RTT 与服务端首飞后抢发等例外见规范；后者在完成握手前 &lt;strong&gt;无法&lt;/strong&gt; 确认对端身份与活性）。&lt;/p&gt;
&lt;h3 id="相对-tls-12-的关键变化工程相关"&gt;&lt;a href="#%e7%9b%b8%e5%af%b9-tls-12-%e7%9a%84%e5%85%b3%e9%94%ae%e5%8f%98%e5%8c%96%e5%b7%a5%e7%a8%8b%e7%9b%b8%e5%85%b3" class="header-anchor"&gt;&lt;/a&gt;相对 TLS 1.2 的关键变化（工程相关）
&lt;/h3&gt;&lt;p&gt;RFC 8446 §1.2 列出若干重大差异，与排障直接相关的包括：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;变化&lt;/th&gt;
					&lt;th&gt;工程含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;对称算法只保留 &lt;strong&gt;AEAD&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;旧 CBC/非 AEAD 套件不再进入 1.3 协商&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;去掉静态 RSA / 静态 DH 套件&lt;/td&gt;
					&lt;td&gt;基于公钥的密钥交换提供 &lt;strong&gt;前向保密（forward secrecy）&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ServerHello 之后握手消息加密&lt;/td&gt;
					&lt;td&gt;证书等不再明文暴露在握手中后期&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;密钥派生改用 &lt;strong&gt;HKDF&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;密钥分离与分析模型更清晰&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;版本协商改为扩展中的版本列表&lt;/td&gt;
					&lt;td&gt;兼容历史上错误实现版本协商的服务端&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;会话恢复 / 旧 PSK 套件统一为新 PSK 交换&lt;/td&gt;
					&lt;td&gt;恢复与 0-RTT 都围绕 PSK 建模&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可选 &lt;strong&gt;0-RTT early data&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;省一轮 RTT，但牺牲部分安全属性&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="0-rtt快但默认不适合写操作"&gt;&lt;a href="#0-rtt%e5%bf%ab%e4%bd%86%e9%bb%98%e8%ae%a4%e4%b8%8d%e9%80%82%e5%90%88%e5%86%99%e6%93%8d%e4%bd%9c" class="header-anchor"&gt;&lt;/a&gt;0-RTT：快，但默认不适合“写操作”
&lt;/h3&gt;&lt;p&gt;当客户端与服务端已共享 PSK（外部配置或先前握手得到）时，客户端可在 &lt;strong&gt;首飞&lt;/strong&gt; 发送 early data。规范强调：0-RTT 在连接建立时节省往返，&lt;strong&gt;代价是某些安全属性&lt;/strong&gt;。Nginx 文档进一步写明：early data 内的请求 &lt;strong&gt;可能遭受重放（replay）&lt;/strong&gt;；应用层可用 &lt;code&gt;$ssl_early_data&lt;/code&gt; 识别，例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Early-Data&lt;/span&gt; &lt;span class="nv"&gt;$ssl_early_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;工程默认建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保持 &lt;code&gt;ssl_early_data off;&lt;/code&gt;（Nginx 默认即为 &lt;code&gt;off&lt;/code&gt;），除非上层明确幂等且能处理重放。&lt;/li&gt;
&lt;li&gt;切勿把“首次提交订单/支付/改密”暴露给 0-RTT 路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="证书链校验从信任锚走到叶子"&gt;&lt;a href="#%e8%af%81%e4%b9%a6%e9%93%be%e6%a0%a1%e9%aa%8c%e4%bb%8e%e4%bf%a1%e4%bb%bb%e9%94%9a%e8%b5%b0%e5%88%b0%e5%8f%b6%e5%ad%90" class="header-anchor"&gt;&lt;/a&gt;证书链校验：从信任锚走到叶子
&lt;/h2&gt;&lt;p&gt;握手里的 &lt;code&gt;Certificate&lt;/code&gt; 消息只是把 &lt;strong&gt;证书序列&lt;/strong&gt; 交给对端；真正“信不信”取决于客户端的 &lt;strong&gt;认证路径校验&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;RFC 5280 §6 指出：Internet PKI 的路径处理验证 &lt;strong&gt;主体可分辨名和/或主体备用名（SAN）&lt;/strong&gt; 与主体公钥之间的绑定；该绑定还受路径上证书约束与依赖方输入限制。基本路径校验中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;有效路径从信任锚（trust anchor）签发的证书开始&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;信任锚选择是策略问题（层级 PKI 顶端 CA、组织自建根、系统信任库中的根等），但路径校验过程本身与“选了哪颗根”无关。&lt;/li&gt;
&lt;li&gt;校验相对于 &lt;strong&gt;当前日期时间&lt;/strong&gt;（实现也可支持对过去某时刻的验证，但不能对有效期之外的时间做校验）。&lt;/li&gt;
&lt;li&gt;客户端 &lt;strong&gt;必须拒绝&lt;/strong&gt; 含有不支持 &lt;strong&gt;critical&lt;/strong&gt; 扩展的证书。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="叶子身份san-才是名字的主战场"&gt;&lt;a href="#%e5%8f%b6%e5%ad%90%e8%ba%ab%e4%bb%bdsan-%e6%89%8d%e6%98%af%e5%90%8d%e5%ad%97%e7%9a%84%e4%b8%bb%e6%88%98%e5%9c%ba" class="header-anchor"&gt;&lt;/a&gt;叶子身份：SAN 才是名字的主战场
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;subjectAltName&lt;/code&gt;（SAN）把身份绑定到证书主体：可含邮箱、&lt;strong&gt;DNS 名&lt;/strong&gt;、IP、URI 等。规范要求：只要要把这类身份绑进证书，就 &lt;strong&gt;必须&lt;/strong&gt; 使用 SAN（或 issuer 对应扩展）；DNS 名也可额外用 subject 的 domainComponent 表示，但现代浏览器与库几乎都以 &lt;strong&gt;SAN DNS&lt;/strong&gt; 做主机名匹配。&lt;/p&gt;
&lt;p&gt;线上“证书有效但浏览器仍报警”的常见原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;证书只写在 CN、未正确填 SAN。&lt;/li&gt;
&lt;li&gt;SAN 有 &lt;code&gt;example.com&lt;/code&gt; 却没有 &lt;code&gt;www.example.com&lt;/code&gt;（或反过来）。&lt;/li&gt;
&lt;li&gt;多站点共用 IP，TLS 客户端未发 &lt;strong&gt;SNI&lt;/strong&gt;，服务端回了默认虚拟主机证书。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="ca-与路径长度basicconstraints"&gt;&lt;a href="#ca-%e4%b8%8e%e8%b7%af%e5%be%84%e9%95%bf%e5%ba%a6basicconstraints" class="header-anchor"&gt;&lt;/a&gt;CA 与路径长度：basicConstraints
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;basicConstraints&lt;/code&gt; 标明主体是否为 CA，以及可包含该证书的路径最大深度。若 v3 证书无此扩展，或存在但 &lt;strong&gt;cA 未断言&lt;/strong&gt;，则该公钥 &lt;strong&gt;不得&lt;/strong&gt; 用于验证证书签名——这也是“把服务器叶子证书误当成中间 CA 拼进链”会立刻失败的原因之一。&lt;/p&gt;
&lt;h3 id="服务端交付的链-vs-客户端验证出的链"&gt;&lt;a href="#%e6%9c%8d%e5%8a%a1%e7%ab%af%e4%ba%a4%e4%bb%98%e7%9a%84%e9%93%be-vs-%e5%ae%a2%e6%88%b7%e7%ab%af%e9%aa%8c%e8%af%81%e5%87%ba%e7%9a%84%e9%93%be" class="header-anchor"&gt;&lt;/a&gt;服务端交付的链 vs 客户端验证出的链
&lt;/h3&gt;&lt;p&gt;OpenSSL &lt;code&gt;s_client -showcerts&lt;/code&gt; 显示的是 &lt;strong&gt;服务端发送的证书列表（发送顺序）&lt;/strong&gt;，文档明确：&lt;strong&gt;它不是已验证链&lt;/strong&gt;。测试工具默认在证书校验错误后仍可能继续握手；生产客户端不应如此。需要“校验失败就中止”时使用 &lt;code&gt;-verify_return_error&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="实践用-openssl-复现握手与链问题"&gt;&lt;a href="#%e5%ae%9e%e8%b7%b5%e7%94%a8-openssl-%e5%a4%8d%e7%8e%b0%e6%8f%a1%e6%89%8b%e4%b8%8e%e9%93%be%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;实践：用 OpenSSL 复现握手与链问题
&lt;/h2&gt;&lt;h3 id="1看协议版本套件与服务端发送的证书"&gt;&lt;a href="#1%e7%9c%8b%e5%8d%8f%e8%ae%ae%e7%89%88%e6%9c%ac%e5%a5%97%e4%bb%b6%e4%b8%8e%e6%9c%8d%e5%8a%a1%e7%ab%af%e5%8f%91%e9%80%81%e7%9a%84%e8%af%81%e4%b9%a6" class="header-anchor"&gt;&lt;/a&gt;1）看协议版本、套件与服务端发送的证书
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 强制 TLS 1.3，展示服务端 certificate_list，SNI 指定主机名&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect example.com:443 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -servername example.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -tls1_3 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -showcerts &amp;lt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关注输出中的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Protocol&lt;/code&gt; / &lt;code&gt;Cipher&lt;/code&gt;（是否真是 TLSv1.3 与 AEAD 套件）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Certificate chain&lt;/code&gt; 段：叶子是否在前、中间证书是否齐全&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Verify return code&lt;/code&gt;（在默认“测试继续”模式下仍会打印校验结果）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OpenSSL 说明：若未提供 &lt;code&gt;-servername&lt;/code&gt;，会尽量用 &lt;code&gt;-connect&lt;/code&gt; 里的 DNS 名填充 SNI；从 1.1.1 起默认行为如此。排查“默认证书错了”时，&lt;strong&gt;刻意省略 SNI&lt;/strong&gt; 与 &lt;strong&gt;显式指定 SNI&lt;/strong&gt; 对比往往立刻见分晓：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 对比：不发 SNI（排查默认 vhost 证书）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect 203.0.113.10:443 -noservername -showcerts &amp;lt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2让校验错误真正失败贴近生产客户端"&gt;&lt;a href="#2%e8%ae%a9%e6%a0%a1%e9%aa%8c%e9%94%99%e8%af%af%e7%9c%9f%e6%ad%a3%e5%a4%b1%e8%b4%a5%e8%b4%b4%e8%bf%91%e7%94%9f%e4%ba%a7%e5%ae%a2%e6%88%b7%e7%ab%af" class="header-anchor"&gt;&lt;/a&gt;2）让校验错误真正失败（贴近生产客户端）
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect example.com:443 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -servername example.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -verify_return_error &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -CAfile /etc/ssl/certs/ca-certificates.crt &amp;lt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;-verify_return_error&lt;/code&gt; 会在服务端证书校验失败时返回错误并通常中止握手，而不是“打印 warning 仍连上”。&lt;/p&gt;
&lt;h3 id="3从-pem-文件本地拆解链"&gt;&lt;a href="#3%e4%bb%8e-pem-%e6%96%87%e4%bb%b6%e6%9c%ac%e5%9c%b0%e6%8b%86%e8%a7%a3%e9%93%be" class="header-anchor"&gt;&lt;/a&gt;3）从 PEM 文件本地拆解链
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 分拆 fullchain.pem 中的每张证书主题/颁发者/SAN/有效期&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl crl2pkcs7 -nocrl -certfile /etc/nginx/ssl/fullchain.pem &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; openssl pkcs7 -print_certs -noout -text &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; egrep &lt;span class="s1"&gt;&amp;#39;Subject:|Issuer:|DNS:|Not Before|Not After|CA:&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;核对顺序：叶子 → 中间 →（一般 &lt;strong&gt;不要&lt;/strong&gt; 把公共根塞进服务端发送链，根应由客户端信任库提供）。&lt;/p&gt;
&lt;h3 id="4nginx-侧链顺序与协议底线"&gt;&lt;a href="#4nginx-%e4%be%a7%e9%93%be%e9%a1%ba%e5%ba%8f%e4%b8%8e%e5%8d%8f%e8%ae%ae%e5%ba%95%e7%ba%bf" class="header-anchor"&gt;&lt;/a&gt;4）Nginx 侧：链顺序与协议底线
&lt;/h3&gt;&lt;p&gt;官方 &lt;code&gt;ssl_certificate&lt;/code&gt;：&lt;strong&gt;同一 PEM 内先主证书（叶子），再中间证书&lt;/strong&gt;；密钥用 &lt;code&gt;ssl_certificate_key&lt;/code&gt;。&lt;br&gt;
&lt;code&gt;ssl_protocols&lt;/code&gt; 默认已是 &lt;code&gt;TLSv1.2 TLSv1.3&lt;/code&gt;——新站点通常不必再打开 TLS 1.0/1.1。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 叶子在前，中间在后（官方顺序）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/ssl/api.fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/ssl/api.key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 与当前默认一致；显式写出便于审计
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# TLS 1.3 套件由库侧管理；此指令主要影响 ≤1.2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;HIGH:!aNULL:!MD5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_prefer_server_ciphers&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 多 worker 共享会话参数，降低重复握手成本
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_session_cache&lt;/span&gt; &lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_session_timeout&lt;/span&gt; &lt;span class="s"&gt;1d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# OCSP stapling：需让 Nginx 知道签发者证书
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_stapling&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_stapling_verify&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_trusted_certificate&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/ssl/api.chain-only.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;resolver&lt;/span&gt; &lt;span class="n"&gt;1.1.1.1&lt;/span&gt; &lt;span class="n"&gt;8.8.8.8&lt;/span&gt; &lt;span class="s"&gt;valid=300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 默认关闭 0-RTT；开启前先确认业务幂等
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_early_data&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 仅在 mTLS 场景打开
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ssl_verify_client on;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ssl_client_certificate /etc/nginx/ssl/client-ca.pem;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ssl_verify_depth 2;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;说明（均来自 Nginx 官方指令文档）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ssl_session_cache&lt;/code&gt; 默认 &lt;code&gt;none&lt;/code&gt;；生产建议 &lt;code&gt;shared:...&lt;/code&gt; 并配合合适超时。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_stapling&lt;/code&gt; 默认 &lt;code&gt;off&lt;/code&gt;；开启时签发者证书须可知（可在 fullchain 或 &lt;code&gt;ssl_trusted_certificate&lt;/code&gt; 中提供）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_verify_client&lt;/code&gt; 默认 &lt;code&gt;off&lt;/code&gt;；&lt;code&gt;ssl_verify_depth&lt;/code&gt; 默认 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_early_data&lt;/code&gt; 默认 &lt;code&gt;off&lt;/code&gt;；开启后请求可能被重放。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="常见坑与排查清单"&gt;&lt;a href="#%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;常见坑与排查清单
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;现象&lt;/th&gt;
					&lt;th&gt;更可能的原因&lt;/th&gt;
					&lt;th&gt;建议动作&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;浏览器报 NET::ERR_CERT_COMMON_NAME_INVALID&lt;/td&gt;
					&lt;td&gt;SAN 不匹配访问主机名&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;openssl x509 -in leaf.pem -noout -ext subjectAltName&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;手机信任、某台服务器不信任&lt;/td&gt;
					&lt;td&gt;服务端少发中间证书 / 系统信任库过旧&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;-showcerts&lt;/code&gt; 数链长；补全 intermediate&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;同 IP 多站，偶发证书串站&lt;/td&gt;
					&lt;td&gt;SNI 未传到正确 vhost&lt;/td&gt;
					&lt;td&gt;对比 &lt;code&gt;-servername&lt;/code&gt; 有无&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;仅部分客户端握手失败&lt;/td&gt;
					&lt;td&gt;仍协商到 TLS 1.0/1.1 或过旧套件&lt;/td&gt;
					&lt;td&gt;收紧 &lt;code&gt;ssl_protocols&lt;/code&gt;；抓包看 ClientHello&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;s_client&lt;/code&gt; 显示 Verify error 但仍连上&lt;/td&gt;
					&lt;td&gt;工具默认不因校验失败中止&lt;/td&gt;
					&lt;td&gt;加 &lt;code&gt;-verify_return_error&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;开启 early data 后偶发重复写&lt;/td&gt;
					&lt;td&gt;0-RTT 可重放&lt;/td&gt;
					&lt;td&gt;关闭 early data，或业务读 &lt;code&gt;$ssl_early_data&lt;/code&gt; 拒绝非幂等&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;stapling 无效 / 日志 OCSP error&lt;/td&gt;
					&lt;td&gt;缺签发者证书或 resolver&lt;/td&gt;
					&lt;td&gt;配 &lt;code&gt;ssl_trusted_certificate&lt;/code&gt; + &lt;code&gt;resolver&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;mTLS 客户端被拒&lt;/td&gt;
					&lt;td&gt;深度/信任锚/optional 语义不符&lt;/td&gt;
					&lt;td&gt;查 &lt;code&gt;ssl_verify_client&lt;/code&gt; / &lt;code&gt;ssl_verify_depth&lt;/code&gt; / 客户端链&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;补充运维习惯：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;证书轮换演练&lt;/strong&gt;：新 fullchain 应用后，立刻用外网 VPS 跑一遍 &lt;code&gt;s_client -verify_return_error&lt;/code&gt;，不要只在本机 &lt;code&gt;curl -k&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP→HTTPS&lt;/strong&gt;：MDN 建议对明文入口 &lt;code&gt;301&lt;/code&gt; 到 HTTPS，并配合 &lt;strong&gt;HSTS&lt;/strong&gt;（&lt;code&gt;Strict-Transport-Security&lt;/code&gt;）降低 SSL stripping 窗口；同时杜绝 &lt;strong&gt;mixed content&lt;/strong&gt;（HTTPS 页加载 HTTP 子资源）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密钥权限与进程身份&lt;/strong&gt;：私钥仅 Nginx worker 可读；配置重载失败时注意旧进程仍握旧 fd。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;TLS 1.3 握手可粗分为 &lt;strong&gt;密钥交换 → 服务端参数 → 认证&lt;/strong&gt;；ServerHello 之后的握手消息受握手流量密钥保护，&lt;code&gt;CertificateVerify&lt;/code&gt; 证明私钥持有，&lt;code&gt;Finished&lt;/code&gt; 做握手完整性确认。&lt;/li&gt;
&lt;li&gt;1.3 强制 AEAD、去掉静态 RSA 密钥交换、引入统一 PSK/0-RTT 模型——配置与排障要以 &lt;strong&gt;前向保密 + 加密握手 + 谨慎 early data&lt;/strong&gt; 为默认心智。&lt;/li&gt;
&lt;li&gt;证书信任是 &lt;strong&gt;从信任锚出发的路径校验&lt;/strong&gt;（RFC 5280），不是“PEM 里有几段文字”；SAN、basicConstraints、中间证书顺序与 SNI 是线上最高频的失败点。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;openssl s_client&lt;/code&gt; 区分“服务端发送了什么”和“客户端验证是否通过”，用 Nginx 官方顺序拼 fullchain，并按业务风险决定是否打开会话缓存、stapling 与 0-RTT。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把“能连上”升级为“能证明连对了人、用对了算法、链完整且可复现验证”，HTTPS 才真正从开关变成可运维的安全能力。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc8446" target="_blank" rel="noopener"
 &gt;RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3&lt;/a&gt;（&lt;code&gt;.txt&lt;/code&gt;：&lt;code&gt;https://www.rfc-editor.org/rfc/rfc8446.txt&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc5280" target="_blank" rel="noopener"
 &gt;RFC 5280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile&lt;/a&gt;（&lt;code&gt;.txt&lt;/code&gt;：&lt;code&gt;https://www.rfc-editor.org/rfc/rfc5280.txt&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security" target="_blank" rel="noopener"
 &gt;MDN: Transport Layer Security (TLS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Glossary/HTTPS" target="_blank" rel="noopener"
 &gt;MDN: HTTPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.openssl.org/master/man1/openssl-s_client/" target="_blank" rel="noopener"
 &gt;OpenSSL: openssl-s_client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_ssl_module.html" target="_blank" rel="noopener"
 &gt;Nginx: Module ngx_http_ssl_module&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>HTTP 条件请求与缓存协商：ETag、Last-Modified 与 Cache-Control 工程实践</title><link>https://blog.waihost.com/posts/http-conditional-requests-cache-negotiation/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/http-conditional-requests-cache-negotiation/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/http-conditional-requests-cache-negotiation.svg" alt="Featured image of post HTTP 条件请求与缓存协商：ETag、Last-Modified 与 Cache-Control 工程实践" /&gt;&lt;p&gt;很多系统的“慢”并不在业务逻辑，而在重复传输不变的响应体。浏览器、CDN、反向代理都可能持有一份旧副本；真正关键的是：&lt;strong&gt;这份副本还能不能直接用？如果不能直接用，能否只问服务器“变了没有”？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;HTTP 把这件事拆成两层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;新鲜度（freshness）&lt;/strong&gt;：缓存还在不在有效期内，决定要不要立刻回源；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证（validation）&lt;/strong&gt;：过期或必须再确认时，用条件请求做协商，成功则返回 &lt;code&gt;304 Not Modified&lt;/code&gt;，避免再次传输实体。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文基于 HTTP 语义规范（RFC 9110）与 HTTP 缓存规范（RFC 9111），结合 Nginx 能力，讲清楚 &lt;code&gt;Cache-Control&lt;/code&gt;、&lt;code&gt;ETag&lt;/code&gt; / &lt;code&gt;Last-Modified&lt;/code&gt;、&lt;code&gt;If-None-Match&lt;/code&gt; / &lt;code&gt;If-Modified-Since&lt;/code&gt; 的协作关系，以及可落地的配置与排错方法。&lt;/p&gt;
&lt;h2 id="一先分清存下来--可以直接用"&gt;&lt;a href="#%e4%b8%80%e5%85%88%e5%88%86%e6%b8%85%e5%ad%98%e4%b8%8b%e6%9d%a5--%e5%8f%af%e4%bb%a5%e7%9b%b4%e6%8e%a5%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;一、先分清：存下来 ≠ 可以直接用
&lt;/h2&gt;&lt;p&gt;RFC 9111 把缓存决策大致分成：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;阶段&lt;/th&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;关键概念&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;存储（store）&lt;/td&gt;
					&lt;td&gt;响应能不能进缓存&lt;/td&gt;
					&lt;td&gt;方法、状态码、&lt;code&gt;no-store&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt;、&lt;code&gt;Authorization&lt;/code&gt; 等&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;新鲜度（fresh）&lt;/td&gt;
					&lt;td&gt;已存副本是否仍 fresh&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;max-age&lt;/code&gt; / &lt;code&gt;s-maxage&lt;/code&gt; / &lt;code&gt;Expires&lt;/code&gt; / Age&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;验证（validate）&lt;/td&gt;
					&lt;td&gt;stale 后能否复用&lt;/td&gt;
					&lt;td&gt;条件请求 + 校验器（validator）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;复用（reuse）&lt;/td&gt;
					&lt;td&gt;最终回什么&lt;/td&gt;
					&lt;td&gt;200 全量、304 无 body、或回源新响应&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常见误解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;no-cache&lt;/code&gt; 不是“禁止缓存”&lt;/strong&gt;。它表示：可以用存储副本，但&lt;strong&gt;必须先成功验证&lt;/strong&gt;后才能拿去满足其他请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;no-store&lt;/code&gt; 才是“不要存”&lt;/strong&gt;。缓存不得存储该请求/响应的任何部分，也不能拿它去满足别的请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;private&lt;/code&gt; 不是“浏览器也不能缓存”&lt;/strong&gt;。它限制的是&lt;strong&gt;共享缓存&lt;/strong&gt;（CDN、公司代理）：响应面向单个用户；私有缓存（浏览器）仍可在规则允许下存储。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把语义说清楚，后面的工程配置才不会互相打架。&lt;/p&gt;
&lt;h2 id="二新鲜度cache-control-的核心指令"&gt;&lt;a href="#%e4%ba%8c%e6%96%b0%e9%b2%9c%e5%ba%a6cache-control-%e7%9a%84%e6%a0%b8%e5%bf%83%e6%8c%87%e4%bb%a4" class="header-anchor"&gt;&lt;/a&gt;二、新鲜度：Cache-Control 的核心指令
&lt;/h2&gt;&lt;h3 id="1-响应侧最常用指令"&gt;&lt;a href="#1-%e5%93%8d%e5%ba%94%e4%be%a7%e6%9c%80%e5%b8%b8%e7%94%a8%e6%8c%87%e4%bb%a4" class="header-anchor"&gt;&lt;/a&gt;1. 响应侧最常用指令
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指令&lt;/th&gt;
					&lt;th&gt;含义（工程视角）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;max-age=N&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;响应在 age &amp;gt; N 秒后视为 stale&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;s-maxage=N&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;只对共享缓存&lt;/strong&gt;覆盖 &lt;code&gt;max-age&lt;/code&gt;/&lt;code&gt;Expires&lt;/code&gt;；并带有必须再验证后才能复用 stale 的语义&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;明确允许缓存（例如带 &lt;code&gt;Authorization&lt;/code&gt; 的响应在共享缓存中的场景）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;禁止共享缓存存储（或限定字段）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;使用前必须验证&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;no-store&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;不要存储&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;must-revalidate&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;一旦 stale，&lt;strong&gt;不得&lt;/strong&gt;在未验证成功前复用；断连时更应报错而不是偷偷给 stale&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个可记的分层：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# 静态资源：可公开、长期缓存（内容指纹化后）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# HTML 入口：可缓存但每次用前确认
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: no-cache
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# 登录态 API：不要被中间层存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: private, no-store
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# 对一致性要求高的共享缓存内容
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=60, must-revalidate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-age-与看起来刚生成"&gt;&lt;a href="#2-age-%e4%b8%8e%e7%9c%8b%e8%b5%b7%e6%9d%a5%e5%88%9a%e7%94%9f%e6%88%90" class="header-anchor"&gt;&lt;/a&gt;2. Age 与“看起来刚生成”
&lt;/h3&gt;&lt;p&gt;响应里的 &lt;code&gt;Age&lt;/code&gt; 表示估计已在缓存中停留的秒数。共享缓存命中时，客户端看到的“年龄”往往不是 0。排查“为什么我设了 &lt;code&gt;max-age=60&lt;/code&gt;，却总在提前回源”时，要同时看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源站给出的 &lt;code&gt;Cache-Control&lt;/code&gt; / &lt;code&gt;Expires&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;中间层是否改写了头&lt;/li&gt;
&lt;li&gt;响应是否已有较大 &lt;code&gt;Age&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-启发式新鲜度别依赖没写过期时间也碰巧能缓存"&gt;&lt;a href="#3-%e5%90%af%e5%8f%91%e5%bc%8f%e6%96%b0%e9%b2%9c%e5%ba%a6%e5%88%ab%e4%be%9d%e8%b5%96%e6%b2%a1%e5%86%99%e8%bf%87%e6%9c%9f%e6%97%b6%e9%97%b4%e4%b9%9f%e7%a2%b0%e5%b7%a7%e8%83%bd%e7%bc%93%e5%ad%98" class="header-anchor"&gt;&lt;/a&gt;3. 启发式新鲜度：别依赖“没写过期时间也碰巧能缓存”
&lt;/h3&gt;&lt;p&gt;没有显式过期信息时，部分缓存会用启发式算法估算 fresh 时间。这在不同实现间差异大，生产环境应&lt;strong&gt;显式给出 freshness&lt;/strong&gt;，不要赌默认行为。&lt;/p&gt;
&lt;h2 id="三校验器etag-与-last-modified"&gt;&lt;a href="#%e4%b8%89%e6%a0%a1%e9%aa%8c%e5%99%a8etag-%e4%b8%8e-last-modified" class="header-anchor"&gt;&lt;/a&gt;三、校验器：ETag 与 Last-Modified
&lt;/h2&gt;&lt;p&gt;验证阶段依赖&lt;strong&gt;表示元数据（representation metadata）&lt;/strong&gt;，最常见两类：&lt;/p&gt;
&lt;h3 id="1-last-modified"&gt;&lt;a href="#1-last-modified" class="header-anchor"&gt;&lt;/a&gt;1. &lt;code&gt;Last-Modified&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;源站认为所选表示&lt;strong&gt;最后修改&lt;/strong&gt;的时间（HTTP-date）。例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;优点：实现简单，文件系统/对象存储天然具备 mtime。&lt;br&gt;
局限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;秒级精度，一秒内多次变更可能撞车；&lt;/li&gt;
&lt;li&gt;时钟回拨、多副本 mtime 不一致会误判；&lt;/li&gt;
&lt;li&gt;内容没变但元数据变了时，语义可能不够准。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-etag实体标签"&gt;&lt;a href="#2-etag%e5%ae%9e%e4%bd%93%e6%a0%87%e7%ad%be" class="header-anchor"&gt;&lt;/a&gt;2. &lt;code&gt;ETag&lt;/code&gt;（实体标签）
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;ETag&lt;/code&gt; 是源站为某个表示分配的不透明标签：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;xyzzy&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: W/&amp;#34;xyzzy&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;强校验器（strong）&lt;/strong&gt;：表示数据（以及会影响 200 内容的关键元数据）变化时标签应变化。适合字节级精确比较。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;弱校验器（weak，&lt;code&gt;W/&lt;/code&gt; 前缀）&lt;/strong&gt;：语义等价即可，即使字节不完全相同。生成成本更低，但比较能力更弱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RFC 9110 明确：在不便存修改时间、一秒精度不够、或修改时间维护不一致时，&lt;strong&gt;ETag 通常比 Last-Modified 更可靠&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;工程上常见生成策略：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;策略&lt;/th&gt;
					&lt;th&gt;适用&lt;/th&gt;
					&lt;th&gt;注意&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;内容哈希（SHA-256 截断）&lt;/td&gt;
					&lt;td&gt;静态文件、构建产物&lt;/td&gt;
					&lt;td&gt;强 ETag，成本随体积上升&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;版本号 / 修订号&lt;/td&gt;
					&lt;td&gt;业务资源、CMS&lt;/td&gt;
					&lt;td&gt;发布流程必须保证“内容变则版本变”&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;inode + size + mtime&lt;/td&gt;
					&lt;td&gt;传统文件服务器&lt;/td&gt;
					&lt;td&gt;迁移/复制后可能变，跨机一致性差&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;弱 ETag（模板渲染摘要）&lt;/td&gt;
					&lt;td&gt;HTML 片段语义不变&lt;/td&gt;
					&lt;td&gt;不要拿去当字节级一致性保证&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="四条件请求如何问变了没有"&gt;&lt;a href="#%e5%9b%9b%e6%9d%a1%e4%bb%b6%e8%af%b7%e6%b1%82%e5%a6%82%e4%bd%95%e9%97%ae%e5%8f%98%e4%ba%86%e6%b2%a1%e6%9c%89" class="header-anchor"&gt;&lt;/a&gt;四、条件请求：如何问“变了没有”
&lt;/h2&gt;&lt;h3 id="1-if-none-match优先基于-etag"&gt;&lt;a href="#1-if-none-match%e4%bc%98%e5%85%88%e5%9f%ba%e4%ba%8e-etag" class="header-anchor"&gt;&lt;/a&gt;1. &lt;code&gt;If-None-Match&lt;/code&gt;（优先，基于 ETag）
&lt;/h3&gt;&lt;p&gt;客户端/缓存带上已存表示的 ETag：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/app.js&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;If-None-Match&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;&amp;#34;33a64df551425fcc55e4d42a148795d9f25f89d4&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;源站比较当前 ETag：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;匹配&lt;/strong&gt;（表示未变）→ &lt;code&gt;304 Not Modified&lt;/code&gt;，通常无 body；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不匹配&lt;/strong&gt; → &lt;code&gt;200 OK&lt;/code&gt; + 新表示（含新的 ETag）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对 &lt;code&gt;If-None-Match&lt;/code&gt;，规范要求使用&lt;strong&gt;弱比较函数&lt;/strong&gt;：即使只有弱 ETag，也可用于缓存验证。&lt;/p&gt;
&lt;h3 id="2-if-modified-since基于时间"&gt;&lt;a href="#2-if-modified-since%e5%9f%ba%e4%ba%8e%e6%97%b6%e9%97%b4" class="header-anchor"&gt;&lt;/a&gt;2. &lt;code&gt;If-Modified-Since&lt;/code&gt;（基于时间）
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/style.css&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;If-Modified-Since&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;Sat, 29 Oct 1994 19:43:31 GMT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;若所选表示的修改时间不比该日期更新，则不必再传实体。&lt;/p&gt;
&lt;h3 id="3-两者同时出现时谁说了算"&gt;&lt;a href="#3-%e4%b8%a4%e8%80%85%e5%90%8c%e6%97%b6%e5%87%ba%e7%8e%b0%e6%97%b6%e8%b0%81%e8%af%b4%e4%ba%86%e7%ae%97" class="header-anchor"&gt;&lt;/a&gt;3. 两者同时出现时谁说了算？
&lt;/h3&gt;&lt;p&gt;RFC 9110 规定：若请求里带了 &lt;code&gt;If-None-Match&lt;/code&gt;，接收方&lt;strong&gt;必须忽略&lt;/strong&gt; &lt;code&gt;If-Modified-Since&lt;/code&gt;——因为实体标签被认为是更准确的替代条件。两者同时出现多是兼容历史客户端；服务端实现应按规范优先级处理。&lt;/p&gt;
&lt;h3 id="4-304-not-modified-到底省了什么"&gt;&lt;a href="#4-304-not-modified-%e5%88%b0%e5%ba%95%e7%9c%81%e4%ba%86%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;4. &lt;code&gt;304 Not Modified&lt;/code&gt; 到底省了什么
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;304&lt;/code&gt; 表示：若没有条件约束，这次本应是 &lt;code&gt;200&lt;/code&gt;；但条件为假（未修改），客户端应继续使用本地已存表示。&lt;/p&gt;
&lt;p&gt;源站仍应带上在同等 &lt;code&gt;200&lt;/code&gt; 中会发送的关键头（如 &lt;code&gt;ETag&lt;/code&gt;、&lt;code&gt;Date&lt;/code&gt;、&lt;code&gt;Content-Location&lt;/code&gt; 等），以便缓存&lt;strong&gt;刷新元数据&lt;/strong&gt;（freshen），而不只是“空响应”。&lt;/p&gt;
&lt;p&gt;流量层面：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;首次：200 + 完整 body（例如 800KB）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;之后未变更：304 + 几乎无 body（通常几百字节头）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对静态资源、移动端弱网、跨境回源，收益非常明显。&lt;/p&gt;
&lt;h2 id="五一张图串起完整路径"&gt;&lt;a href="#%e4%ba%94%e4%b8%80%e5%bc%a0%e5%9b%be%e4%b8%b2%e8%b5%b7%e5%ae%8c%e6%95%b4%e8%b7%af%e5%be%84" class="header-anchor"&gt;&lt;/a&gt;五、一张图串起完整路径
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Client/CDN 本地有副本？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | 无 -----&amp;gt; 普通 GET -----&amp;gt; 200 + body + Cache-Control + ETag/LM
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | 有且 fresh -----&amp;gt; 直接复用（不回源）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | 有但 stale / 或 no-cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 条件请求：If-None-Match / If-Modified-Since
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +--&amp;gt; 未变：304（刷新头，复用旧 body）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +--&amp;gt; 已变：200（新 body + 新校验器）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;记住两句话：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;fresh 解决“要不要问”&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;validator 解决“问了以后要不要传 body”&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只有 &lt;code&gt;max-age&lt;/code&gt; 没有校验器，过期后往往只能全量回源；只有 ETag 没有合理 freshness，会变成“每次都协商”，仍然浪费 RTT。&lt;/p&gt;
&lt;h2 id="六工程实践按资源类型给策略"&gt;&lt;a href="#%e5%85%ad%e5%b7%a5%e7%a8%8b%e5%ae%9e%e8%b7%b5%e6%8c%89%e8%b5%84%e6%ba%90%e7%b1%bb%e5%9e%8b%e7%bb%99%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;六、工程实践：按资源类型给策略
&lt;/h2&gt;&lt;h3 id="1-带内容哈希的静态资源最推荐"&gt;&lt;a href="#1-%e5%b8%a6%e5%86%85%e5%ae%b9%e5%93%88%e5%b8%8c%e7%9a%84%e9%9d%99%e6%80%81%e8%b5%84%e6%ba%90%e6%9c%80%e6%8e%a8%e8%8d%90" class="header-anchor"&gt;&lt;/a&gt;1. 带内容哈希的静态资源（最推荐）
&lt;/h3&gt;&lt;p&gt;构建后文件名含 hash，例如 &lt;code&gt;app.3f2a1c.js&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/static/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 文件名已指纹化，可大胆长缓存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;365d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;public,&lt;/span&gt; &lt;span class="s"&gt;max-age=31536000,&lt;/span&gt; &lt;span class="s"&gt;immutable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;URL 变 = 内容变&lt;/strong&gt;，可用超长 &lt;code&gt;max-age&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;immutable&lt;/code&gt; 提示浏览器在 freshness 内不必做启发式再验证（支持情况因客户端而异，但不妨碍给出）；&lt;/li&gt;
&lt;li&gt;旧 URL 自然失效，无需纠结主动 purge 每一个文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-html--入口文档"&gt;&lt;a href="#2-html--%e5%85%a5%e5%8f%a3%e6%96%87%e6%a1%a3" class="header-anchor"&gt;&lt;/a&gt;2. HTML / 入口文档
&lt;/h3&gt;&lt;p&gt;入口常引用带 hash 的静态资源，自身却要尽快看到新版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: no-cache
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;html-rev-42&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;或短 &lt;code&gt;max-age&lt;/code&gt; + 强制再验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: max-age=0, must-revalidate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;html-rev-42&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样浏览器/边缘仍可存副本，但使用前会走条件请求；HTML 未变则 304，变了才下新文档。&lt;/p&gt;
&lt;h3 id="3-个性化或含凭证的-api"&gt;&lt;a href="#3-%e4%b8%aa%e6%80%a7%e5%8c%96%e6%88%96%e5%90%ab%e5%87%ad%e8%af%81%e7%9a%84-api" class="header-anchor"&gt;&lt;/a&gt;3. 个性化或含凭证的 API
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: private, no-store
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt; 避免 CDN 把用户 A 的响应给用户 B；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-store&lt;/code&gt; 进一步降低敏感信息落盘风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;code&gt;no-store&lt;/code&gt; &lt;strong&gt;不是&lt;/strong&gt;完备隐私方案（恶意或失控缓存可能不遵守），敏感数据仍要靠鉴权、最小化返回字段和传输安全。&lt;/p&gt;
&lt;h3 id="4-可被-cdn-缓存的公共-api--配置"&gt;&lt;a href="#4-%e5%8f%af%e8%a2%ab-cdn-%e7%bc%93%e5%ad%98%e7%9a%84%e5%85%ac%e5%85%b1-api--%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;4. 可被 CDN 缓存的公共 API / 配置
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: public, s-maxage=30, max-age=10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;cfg-20260715-1&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;浏览器短缓存（&lt;code&gt;max-age&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;CDN 稍长（&lt;code&gt;s-maxage&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;过期后用 ETag 协商，降低源站带宽。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nginx 反代缓存可配合：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;proxy_cache_path&lt;/span&gt; &lt;span class="s"&gt;/var/cache/nginx&lt;/span&gt; &lt;span class="s"&gt;levels=1:2&lt;/span&gt; &lt;span class="s"&gt;keys_zone=api_cache:50m&lt;/span&gt; &lt;span class="s"&gt;inactive=10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/api/public/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache&lt;/span&gt; &lt;span class="s"&gt;api_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache_valid&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 过期后用条件请求回源校验
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache_revalidate&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 更新期间允许短暂提供 stale，降低源站抖动影响
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache_use_stale&lt;/span&gt; &lt;span class="s"&gt;updating&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt; &lt;span class="s"&gt;timeout&lt;/span&gt; &lt;span class="s"&gt;http_500&lt;/span&gt; &lt;span class="s"&gt;http_502&lt;/span&gt; &lt;span class="s"&gt;http_503&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://upstream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;proxy_cache_revalidate on&lt;/code&gt; 的作用，正是在缓存过期后启用 &lt;code&gt;If-Modified-Since&lt;/code&gt; / &lt;code&gt;If-None-Match&lt;/code&gt; 条件回源——这与 RFC 里的 validation 模型一致。&lt;/p&gt;
&lt;h2 id="七服务端如何正确实现条件-get"&gt;&lt;a href="#%e4%b8%83%e6%9c%8d%e5%8a%a1%e7%ab%af%e5%a6%82%e4%bd%95%e6%ad%a3%e7%a1%ae%e5%ae%9e%e7%8e%b0%e6%9d%a1%e4%bb%b6-get" class="header-anchor"&gt;&lt;/a&gt;七、服务端如何正确实现条件 GET
&lt;/h2&gt;&lt;p&gt;以伪代码说明最小正确逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strong_etag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# 例如 &amp;#39;&amp;#34;%s&amp;#34;&amp;#39; % sha256(body)[:16]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;last_mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified_http_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 1) 优先处理 If-None-Match&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;If-None-Match&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;etag_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# 按弱比较规则实现&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_304&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_200&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 2) 否则再看 If-Modified-Since&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;If-Modified-Since&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ims&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modified_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ims&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_304&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_200&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实现清单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同一表示的 ETag 必须稳定&lt;/strong&gt;：相同内容不要每次随机；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容变更必须换 ETag&lt;/strong&gt;：漏更新会造成“改了却一直 304”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Vary&lt;/code&gt; 要配对&lt;/strong&gt;：若响应随 &lt;code&gt;Accept-Encoding&lt;/code&gt; / &lt;code&gt;Authorization&lt;/code&gt; / 自定义头变化，必须正确 &lt;code&gt;Vary&lt;/code&gt;，否则缓存会串内容；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩前后一致性&lt;/strong&gt;：对 gzip/br 内容，ETag 策略要与表示选择一致，避免“协商错表示”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要手动拼错弱标记&lt;/strong&gt;：&lt;code&gt;W/&lt;/code&gt; 大小写敏感，格式为 &lt;code&gt;W/&amp;quot;tag&amp;quot;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="八常见坑与排查清单"&gt;&lt;a href="#%e5%85%ab%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;八、常见坑与排查清单
&lt;/h2&gt;&lt;h3 id="坑-1no-cache-和-no-store-用反"&gt;&lt;a href="#%e5%9d%91-1no-cache-%e5%92%8c-no-store-%e7%94%a8%e5%8f%8d" class="header-anchor"&gt;&lt;/a&gt;坑 1：&lt;code&gt;no-cache&lt;/code&gt; 和 &lt;code&gt;no-store&lt;/code&gt; 用反
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;想“每次确认后再用本地副本” → &lt;code&gt;no-cache&lt;/code&gt;（或 &lt;code&gt;max-age=0, must-revalidate&lt;/code&gt;）+ ETag；&lt;/li&gt;
&lt;li&gt;想“别存敏感响应” → &lt;code&gt;no-store&lt;/code&gt;（通常再加 &lt;code&gt;private&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="坑-2只配了长缓存没有内容指纹"&gt;&lt;a href="#%e5%9d%91-2%e5%8f%aa%e9%85%8d%e4%ba%86%e9%95%bf%e7%bc%93%e5%ad%98%e6%b2%a1%e6%9c%89%e5%86%85%e5%ae%b9%e6%8c%87%e7%ba%b9" class="header-anchor"&gt;&lt;/a&gt;坑 2：只配了长缓存，没有内容指纹
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;Cache-Control: max-age=31536000&lt;/code&gt; 配在&lt;strong&gt;固定 URL&lt;/strong&gt; 的 &lt;code&gt;app.js&lt;/code&gt; 上，发版后用户会长时间用旧脚本。正确做法是 &lt;strong&gt;hash 文件名&lt;/strong&gt; 或主动缩短 freshness 并保证能 purge。&lt;/p&gt;
&lt;h3 id="坑-3etag-在多实例间不一致"&gt;&lt;a href="#%e5%9d%91-3etag-%e5%9c%a8%e5%a4%9a%e5%ae%9e%e4%be%8b%e9%97%b4%e4%b8%8d%e4%b8%80%e8%87%b4" class="header-anchor"&gt;&lt;/a&gt;坑 3：ETag 在多实例间不一致
&lt;/h3&gt;&lt;p&gt;滚动发布时实例 A/B 对同一文件算出不同 ETag（例如混入了进程启动时间），客户端会在 200/304 间抖动，缓存命中率暴跌。校验器生成必须&lt;strong&gt;内容决定论&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="坑-4中间层剥掉了校验器或改写了-cache-control"&gt;&lt;a href="#%e5%9d%91-4%e4%b8%ad%e9%97%b4%e5%b1%82%e5%89%a5%e6%8e%89%e4%ba%86%e6%a0%a1%e9%aa%8c%e5%99%a8%e6%88%96%e6%94%b9%e5%86%99%e4%ba%86-cache-control" class="header-anchor"&gt;&lt;/a&gt;坑 4：中间层剥掉了校验器或改写了 Cache-Control
&lt;/h3&gt;&lt;p&gt;排查命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 看源站&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sI https://origin.example.com/static/app.js &lt;span class="p"&gt;|&lt;/span&gt; sed -n &lt;span class="s1"&gt;&amp;#39;1,20p&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 看经 CDN/Nginx 后&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sI https://www.example.com/static/app.js &lt;span class="p"&gt;|&lt;/span&gt; sed -n &lt;span class="s1"&gt;&amp;#39;1,20p&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 模拟条件请求&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ETAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;abc123&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sI -H &lt;span class="s2"&gt;&amp;#34;If-None-Match: &lt;/span&gt;&lt;span class="nv"&gt;$ETAG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; https://www.example.com/static/app.js &lt;span class="p"&gt;|&lt;/span&gt; sed -n &lt;span class="s1"&gt;&amp;#39;1,15p&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对比：&lt;code&gt;ETag&lt;/code&gt;、&lt;code&gt;Cache-Control&lt;/code&gt;、&lt;code&gt;Age&lt;/code&gt;、&lt;code&gt;CF-Cache-Status&lt;/code&gt;/&lt;code&gt;X-Cache&lt;/code&gt; 等是否符合预期。第二次应看到 &lt;code&gt;304&lt;/code&gt;（在 ETag 仍匹配时）。&lt;/p&gt;
&lt;h3 id="坑-5api-被共享缓存误缓存"&gt;&lt;a href="#%e5%9d%91-5api-%e8%a2%ab%e5%85%b1%e4%ba%ab%e7%bc%93%e5%ad%98%e8%af%af%e7%bc%93%e5%ad%98" class="header-anchor"&gt;&lt;/a&gt;坑 5：API 被共享缓存误缓存
&lt;/h3&gt;&lt;p&gt;带 cookie/authorization 的响应若缺少 &lt;code&gt;private&lt;/code&gt;/&lt;code&gt;no-store&lt;/code&gt;，再叠加错误的 &lt;code&gt;public&lt;/code&gt;，可能把用户数据缓存到边缘。对用户相关响应默认保守，对真正公共的 GET 再显式放开。&lt;/p&gt;
&lt;h3 id="坑-6304-后客户端仍像没更新样式"&gt;&lt;a href="#%e5%9d%91-6304-%e5%90%8e%e5%ae%a2%e6%88%b7%e7%ab%af%e4%bb%8d%e5%83%8f%e6%b2%a1%e6%9b%b4%e6%96%b0%e6%a0%b7%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;坑 6：304 后客户端仍像“没更新样式”
&lt;/h3&gt;&lt;p&gt;常见是 HTML 长缓存，或 Service Worker / 应用内二次缓存。协议层 304 只保证&lt;strong&gt;这一跳 HTTP 缓存协商&lt;/strong&gt;；还要检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML 的 Cache-Control；&lt;/li&gt;
&lt;li&gt;SW 的 &lt;code&gt;cache.addAll&lt;/code&gt; 策略；&lt;/li&gt;
&lt;li&gt;应用本地 localStorage 里的资源清单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="九选型小结"&gt;&lt;a href="#%e4%b9%9d%e9%80%89%e5%9e%8b%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;九、选型小结
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;目标&lt;/th&gt;
					&lt;th&gt;推荐组合&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;最大化静态资源性能&lt;/td&gt;
					&lt;td&gt;内容哈希 URL + 长 &lt;code&gt;max-age&lt;/code&gt; + &lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;入口文档尽快更新&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt; 或短 max-age + ETag + 304&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;公共只读 API 降源站负载&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;s-maxage&lt;/code&gt; + ETag + CDN &lt;code&gt;proxy_cache_revalidate&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;用户私有数据&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;private, no-store&lt;/code&gt;，默认不进共享缓存&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;过期后仍要强一致&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;must-revalidate&lt;/code&gt; / &lt;code&gt;proxy-revalidate&lt;/code&gt; / &lt;code&gt;s-maxage&lt;/code&gt; 语义&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用 freshness 控制回源频率&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 validator 控制回源体积&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 private/no-store 控制安全边界&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 URL 指纹解决“长缓存与发布”的矛盾&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;HTTP 缓存不是“加一个 Redis”或“在 Nginx 里 &lt;code&gt;proxy_cache on&lt;/code&gt;”这么简单，它是一套有严格语义的协议机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cache-Control&lt;/code&gt; 回答&lt;strong&gt;能不能存、能存多久、过期后能不能直接用&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt; / &lt;code&gt;Last-Modified&lt;/code&gt; 回答&lt;strong&gt;如何证明表示是否变化&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;If-None-Match&lt;/code&gt; / &lt;code&gt;If-Modified-Since&lt;/code&gt; 与 &lt;code&gt;304&lt;/code&gt; 回答&lt;strong&gt;如何在确认未变时省略 body&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这三层设计对齐后，静态站点、前后端分离入口、CDN 边缘缓存和公共 API 都能在&lt;strong&gt;带宽、延迟、一致性&lt;/strong&gt;之间取得可解释的平衡。下次再看到“用户一直是旧页面”或“源站带宽无故升高”，先把响应头和一次条件请求抓出来——答案通常就在 &lt;code&gt;Cache-Control&lt;/code&gt; 与校验器的协作里。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;IETF RFC 9111：&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9111" target="_blank" rel="noopener"
 &gt;HTTP Caching&lt;/a&gt;（新鲜度、验证、&lt;code&gt;Cache-Control&lt;/code&gt; 指令语义）&lt;/li&gt;
&lt;li&gt;IETF RFC 9110：&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9110" target="_blank" rel="noopener"
 &gt;HTTP Semantics&lt;/a&gt;（&lt;code&gt;ETag&lt;/code&gt;、&lt;code&gt;Last-Modified&lt;/code&gt;、&lt;code&gt;If-None-Match&lt;/code&gt;、&lt;code&gt;If-Modified-Since&lt;/code&gt;、&lt;code&gt;304 Not Modified&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching" target="_blank" rel="noopener"
 &gt;HTTP caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag" target="_blank" rel="noopener"
 &gt;ETag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match" target="_blank" rel="noopener"
 &gt;If-None-Match&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control" target="_blank" rel="noopener"
 &gt;Cache-Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Nginx 文档：&lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_headers_module.html" target="_blank" rel="noopener"
 &gt;ngx_http_headers_module&lt;/a&gt;（&lt;code&gt;expires&lt;/code&gt; / &lt;code&gt;add_header&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Nginx 文档：&lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html" target="_blank" rel="noopener"
 &gt;ngx_http_proxy_module&lt;/a&gt;（&lt;code&gt;proxy_cache_revalidate&lt;/code&gt;、&lt;code&gt;proxy_cache_use_stale&lt;/code&gt;、&lt;code&gt;proxy_cache_valid&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Node.js 26.5.0 Current 发布：Web Streams、TLS 可观测性与权限模型修复实践指南</title><link>https://blog.waihost.com/posts/nodejs-26-5-current-upgrade-guide/</link><pubDate>Fri, 10 Jul 2026 09:02:00 +0800</pubDate><guid>https://blog.waihost.com/posts/nodejs-26-5-current-upgrade-guide/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/nodejs-26-5-current-upgrade-guide.svg" alt="Featured image of post Node.js 26.5.0 Current 发布：Web Streams、TLS 可观测性与权限模型修复实践指南" /&gt;&lt;p&gt;Node.js 26.5.0 已于 2026-07-08 发布，属于 Current 版本线的一次常规功能与维护更新。它不是安全发布，但包含了几类对后端服务、网关、边缘函数和内部平台都值得关注的变化：&lt;code&gt;Blob&lt;/code&gt; 新增面向流式读取的能力，TLS 协商信息更容易被观测，权限模型在 &lt;code&gt;NODE_OPTIONS&lt;/code&gt; 传播场景下修复了行为一致性问题，同时依赖组件继续更新到较新的版本。&lt;/p&gt;
&lt;p&gt;如果你的生产环境仍以 LTS 为主，不需要因为 Current 版本线的每次发布立即升级线上服务；但如果团队正在验证 Node.js 26、维护基础镜像、做运行时平台适配，26.5.0 很适合作为一次“小步升级 + 回归验证”的窗口。&lt;/p&gt;
&lt;h2 id="这次发布的定位"&gt;&lt;a href="#%e8%bf%99%e6%ac%a1%e5%8f%91%e5%b8%83%e7%9a%84%e5%ae%9a%e4%bd%8d" class="header-anchor"&gt;&lt;/a&gt;这次发布的定位
&lt;/h2&gt;&lt;p&gt;从官方 release index 可以看到，Node.js 26.5.0 的发布日期为 2026-07-08，随附组件版本包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;npm：&lt;code&gt;11.17.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;V8：&lt;code&gt;14.6.202.34&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;libuv：&lt;code&gt;1.52.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;zlib：&lt;code&gt;1.3.2.1-motley&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OpenSSL：&lt;code&gt;3.5.7&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ABI modules：&lt;code&gt;147&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;security: false&lt;/code&gt;，即这不是一次安全专版发布&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着它更适合被理解为 Current 线的功能增强和维护版本，而不是“必须立刻全量升级”的安全补丁。不过，Current 线通常会提前暴露未来 LTS 可能影响应用的运行时变化，平台团队应该尽早把它纳入 CI 验证。&lt;/p&gt;
&lt;h2 id="关键变化一blobtextstream-让文本读取更贴近流式处理"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e5%8f%98%e5%8c%96%e4%b8%80blobtextstream-%e8%ae%a9%e6%96%87%e6%9c%ac%e8%af%bb%e5%8f%96%e6%9b%b4%e8%b4%b4%e8%bf%91%e6%b5%81%e5%bc%8f%e5%a4%84%e7%90%86" class="header-anchor"&gt;&lt;/a&gt;关键变化一：&lt;code&gt;Blob.textStream()&lt;/code&gt; 让文本读取更贴近流式处理
&lt;/h2&gt;&lt;p&gt;官方发布说明将 &lt;code&gt;blob.textStream()&lt;/code&gt; 列为 notable change。过去我们处理 &lt;code&gt;Blob&lt;/code&gt; 文本内容时，常见方式是一次性调用 &lt;code&gt;blob.text()&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这种写法简单，但它会把文本内容整体读入内存。对于普通接口请求体、配置片段或小文件来说问题不大；但在日志分析、对象存储网关、上传文件预处理等场景中，一次性读取会放大内存峰值，也不利于和 Web Streams 管线组合。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Blob.textStream()&lt;/code&gt; 的价值在于：它把“文本内容”暴露为可流式消费的接口，应用可以更自然地接入 &lt;code&gt;ReadableStream&lt;/code&gt;、&lt;code&gt;TextDecoderStream&lt;/code&gt;、分块处理、背压控制等模式。实践中建议关注三类用法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 伪代码：面向流式消费的处理方式
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 分块统计、过滤、写入下游，而不是一次性放进内存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;processChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;升级验证时不要只跑“能否启动”的 smoke test，应补充大文件和高并发场景，观察 RSS、GC 暂停、吞吐量是否更稳定。&lt;/p&gt;
&lt;h2 id="关键变化二tls-协商组信息更容易排查"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e5%8f%98%e5%8c%96%e4%ba%8ctls-%e5%8d%8f%e5%95%86%e7%bb%84%e4%bf%a1%e6%81%af%e6%9b%b4%e5%ae%b9%e6%98%93%e6%8e%92%e6%9f%a5" class="header-anchor"&gt;&lt;/a&gt;关键变化二：TLS 协商组信息更容易排查
&lt;/h2&gt;&lt;p&gt;26.5.0 还提到 “report negotiated TLS groups”。这类变化不一定会改变业务逻辑，但对运维和安全排障很有价值。&lt;/p&gt;
&lt;p&gt;在实际生产中，TLS 问题经常表现为“某些客户端能连、某些客户端失败”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端运行时、OpenSSL、系统证书库版本不同；&lt;/li&gt;
&lt;li&gt;负载均衡、反向代理、服务端 Node.js 的 TLS 配置不一致；&lt;/li&gt;
&lt;li&gt;安全基线升级后，曲线/密钥交换组被禁用或协商失败；&lt;/li&gt;
&lt;li&gt;FIPS、国密或企业代理环境引入额外限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果运行时能报告协商到的 TLS groups，平台团队就能把“握手失败/降级/兼容性差”的问题从黑盒日志变成可观测事实。建议在升级验证阶段做一次完整 TLS 回归：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node -p &lt;span class="s2"&gt;&amp;#34;process.versions&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect example.com:443 -tls1_3 &amp;lt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;同时检查服务端访问日志、网关日志和 APM 标签，确认 TLS 版本、加密套件、协商组等信息是否能被统一记录。对于金融、企业内网、老旧 Android/WebView 客户端，这一步尤其重要。&lt;/p&gt;
&lt;h2 id="关键变化三权限模型与-node_options-的一致性修复"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e5%8f%98%e5%8c%96%e4%b8%89%e6%9d%83%e9%99%90%e6%a8%a1%e5%9e%8b%e4%b8%8e-node_options-%e7%9a%84%e4%b8%80%e8%87%b4%e6%80%a7%e4%bf%ae%e5%a4%8d" class="header-anchor"&gt;&lt;/a&gt;关键变化三：权限模型与 &lt;code&gt;NODE_OPTIONS&lt;/code&gt; 的一致性修复
&lt;/h2&gt;&lt;p&gt;Node.js 的 Permission Model 仍处于演进阶段，但已经是很多团队评估“最小权限运行 JavaScript 服务”的重要方向。26.5.0 的提交列表包含 “fix permission model propagation via NODE_OPTIONS”。&lt;/p&gt;
&lt;p&gt;这类修复值得平台团队认真看待，因为 &lt;code&gt;NODE_OPTIONS&lt;/code&gt; 在容器镜像、PaaS 平台、CI/CD、serverless 运行时里非常常见。例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--experimental-permission --allow-fs-read=/app/config&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node server.js
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果权限相关选项在子进程、worker、工具链包装脚本中传播不一致，可能会出现两种风险：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开发/测试环境以为权限限制已生效，但生产包装脚本绕过了限制；&lt;/li&gt;
&lt;li&gt;本该允许的读取、网络或子进程行为被误拦截，导致线上难以复现的故障。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，升级到 26.5.0 后建议新增一组“权限模型回归用例”，覆盖：主进程、&lt;code&gt;child_process&lt;/code&gt;、worker、测试框架、构建脚本以及容器入口脚本。不要只验证应用入口文件。&lt;/p&gt;
&lt;h2 id="关键变化四依赖组件更新带来的隐性影响"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e5%8f%98%e5%8c%96%e5%9b%9b%e4%be%9d%e8%b5%96%e7%bb%84%e4%bb%b6%e6%9b%b4%e6%96%b0%e5%b8%a6%e6%9d%a5%e7%9a%84%e9%9a%90%e6%80%a7%e5%bd%b1%e5%93%8d" class="header-anchor"&gt;&lt;/a&gt;关键变化四：依赖组件更新带来的隐性影响
&lt;/h2&gt;&lt;p&gt;本次版本还包含若干依赖更新，例如 release notes 中列出的 Undici、nghttp3、SQLite 等更新；release index 也显示 OpenSSL、libuv、zlib 等底层组件处在较新的版本组合上。对大多数业务而言，这些更新不会直接要求改代码，但它们可能影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP 客户端行为：连接复用、代理、重定向、超时和 header 处理；&lt;/li&gt;
&lt;li&gt;HTTP/2 / HTTP/3 相关边界行为；&lt;/li&gt;
&lt;li&gt;内置 SQLite 场景的兼容性和性能；&lt;/li&gt;
&lt;li&gt;原生插件编译、ABI 兼容和镜像体积；&lt;/li&gt;
&lt;li&gt;TLS/加密能力与企业安全基线的匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果项目依赖 &lt;code&gt;node-gyp&lt;/code&gt;、原生 addon、Electron、Playwright、Puppeteer 或自定义 OpenSSL 行为，建议在升级前后分别输出环境信息：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node -p &lt;span class="s2"&gt;&amp;#34;process.version&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node -p &lt;span class="s2"&gt;&amp;#34;process.versions&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm ls --depth&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并在 CI 中保留构建日志，方便定位 ABI 或编译链变化。&lt;/p&gt;
&lt;h2 id="推荐升级流程"&gt;&lt;a href="#%e6%8e%a8%e8%8d%90%e5%8d%87%e7%ba%a7%e6%b5%81%e7%a8%8b" class="header-anchor"&gt;&lt;/a&gt;推荐升级流程
&lt;/h2&gt;&lt;p&gt;对于生产团队，建议把 Node.js 26.5.0 当作 Current 线验证版本，而不是直接替换 LTS：&lt;/p&gt;
&lt;h3 id="1-先确认版本线策略"&gt;&lt;a href="#1-%e5%85%88%e7%a1%ae%e8%ae%a4%e7%89%88%e6%9c%ac%e7%ba%bf%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;1. 先确认版本线策略
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;线上核心服务：优先使用当前 LTS；&lt;/li&gt;
&lt;li&gt;内部平台、SDK、CLI、基础镜像：可以提前验证 Current；&lt;/li&gt;
&lt;li&gt;新特性探索：用隔离环境验证，不直接影响生产流量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-建立最小回归清单"&gt;&lt;a href="#2-%e5%bb%ba%e7%ab%8b%e6%9c%80%e5%b0%8f%e5%9b%9e%e5%bd%92%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;2. 建立最小回归清单
&lt;/h3&gt;&lt;p&gt;至少覆盖以下命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node -v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm -v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node -p &lt;span class="s2"&gt;&amp;#34;process.versions&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm ci
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm run build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Web 服务还应补充接口 smoke test、TLS 连接测试、代理场景测试和大文件上传/下载测试。&lt;/p&gt;
&lt;h3 id="3-对-web-streams-和-blob-场景做专项验证"&gt;&lt;a href="#3-%e5%af%b9-web-streams-%e5%92%8c-blob-%e5%9c%ba%e6%99%af%e5%81%9a%e4%b8%93%e9%a1%b9%e9%aa%8c%e8%af%81" class="header-anchor"&gt;&lt;/a&gt;3. 对 Web Streams 和 Blob 场景做专项验证
&lt;/h3&gt;&lt;p&gt;如果项目处理上传文件、对象存储、日志、CSV/JSONL 或边缘函数请求体，可以新增流式处理测试，避免把大对象一次性读入内存。&lt;/p&gt;
&lt;h3 id="4-对权限模型做入口链路测试"&gt;&lt;a href="#4-%e5%af%b9%e6%9d%83%e9%99%90%e6%a8%a1%e5%9e%8b%e5%81%9a%e5%85%a5%e5%8f%a3%e9%93%be%e8%b7%af%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;4. 对权限模型做“入口链路”测试
&lt;/h3&gt;&lt;p&gt;如果你正在使用或评估 &lt;code&gt;--experimental-permission&lt;/code&gt;，不要只测 &lt;code&gt;node app.js&lt;/code&gt;。要覆盖 npm script、PM2/systemd、Docker ENTRYPOINT、测试框架和子进程。&lt;/p&gt;
&lt;h3 id="5-保留快速回滚方案"&gt;&lt;a href="#5-%e4%bf%9d%e7%95%99%e5%bf%ab%e9%80%9f%e5%9b%9e%e6%bb%9a%e6%96%b9%e6%a1%88" class="header-anchor"&gt;&lt;/a&gt;5. 保留快速回滚方案
&lt;/h3&gt;&lt;p&gt;升级运行时比升级普通依赖更底层。建议镜像 tag 明确区分：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node:26.4.0-bookworm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node:26.5.0-bookworm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在灰度阶段保留旧镜像和回滚脚本，避免运行时行为变化影响全量流量。&lt;/p&gt;
&lt;h2 id="注意事项"&gt;&lt;a href="#%e6%b3%a8%e6%84%8f%e4%ba%8b%e9%a1%b9" class="header-anchor"&gt;&lt;/a&gt;注意事项
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;26.5.0 是 Current，不等同于 LTS；生产环境是否采用要看团队的运行时策略。&lt;/li&gt;
&lt;li&gt;这不是安全发布，不应把它包装成“紧急安全升级”。&lt;/li&gt;
&lt;li&gt;权限模型仍要关注实验性接口和未来变更，不建议仅凭一次修复就假设所有沙箱场景都已稳定。&lt;/li&gt;
&lt;li&gt;原生 addon 和构建链要在目标架构上验证，尤其是 Alpine、ARM64、CI runner 与生产镜像不一致时。&lt;/li&gt;
&lt;li&gt;如果线上仍在 Node.js 22/24 LTS，优先处理对应 LTS 安全更新，再安排 Node.js 26 的兼容性验证。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;Node.js 26.5.0 的重点不在“惊天动地的新功能”，而在几个对工程实践很实用的方向：更流式的 &lt;code&gt;Blob&lt;/code&gt; 文本读取、更好的 TLS 可观测性、权限模型传播修复，以及底层依赖维护更新。对平台团队来说，最合理的姿势是：把它纳入 CI 和预发环境，围绕 Web Streams、TLS、权限模型、原生 addon 建立回归清单；对业务团队来说，则应继续遵循 LTS 优先策略，等验证充分后再决定是否升级运行时基线。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;官方发布说明：&lt;a class="link" href="https://nodejs.org/en/blog/release/v26.5.0" target="_blank" rel="noopener"
 &gt;https://nodejs.org/en/blog/release/v26.5.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;官方 release index：&lt;a class="link" href="https://nodejs.org/download/release/index.json" target="_blank" rel="noopener"
 &gt;https://nodejs.org/download/release/index.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub Release：&lt;a class="link" href="https://github.com/nodejs/node/releases/tag/v26.5.0" target="_blank" rel="noopener"
 &gt;https://github.com/nodejs/node/releases/tag/v26.5.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Node.js Permission Model 文档：&lt;a class="link" href="https://nodejs.org/api/permissions.html" target="_blank" rel="noopener"
 &gt;https://nodejs.org/api/permissions.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Node.js Web Streams 文档：&lt;a class="link" href="https://nodejs.org/api/webstreams.html" target="_blank" rel="noopener"
 &gt;https://nodejs.org/api/webstreams.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>