CDN 与源站重复下发 HSTS:两个响应头,浏览器只认第一个-第三维度MokIII

现象:同一个响应头出现两次

站点刚接上 CDN 之后,我用 curl -I 检查响应头,发现 Strict-Transport-Security 出现了两行:

Strict-Transport-Security: max-age=15552000
Strict-Transport-Security: max-age=31536000

这不是"更安全",这是冲突。

一、HSTS 是什么,为什么重复会出问题

HSTS(HTTP Strict Transport Security)是一句"命令":告诉浏览器"以后访问我这个域名,只准用 HTTPS"。浏览器收到后,会在有效期内把该域名的 http 请求在本地直接改写成 https,不再发出明文请求。

它的效力由 max-age 决定。而 RFC 6797 明确规定:当响应中出现多个 HSTS 头时,浏览器只采用第一个,忽略其余。

问题就在这——哪个"第一个"取决于头部的排列顺序,而你可能毫不知情地:

  • 以为生效的是"A 侧那条长有效期",实际用的是"B 侧那条短的";
  • 或者写 includeSubDomains 的那条被忽略,子域其实没被覆盖。

越想通过"两边都发"来加强,越可能拿到最弱的那条。

二、怎么查

# -I 只看响应头
curl -sI https://www.example.com/ | grep -i 'strict-transport-security'

# 想看原始头部顺序,用 -D
curl -sD - -o /dev/null https://www.example.com/ | grep -i 'strict-transport'

计数很重要:出现 2 行就是重复了。

三、原则:只让一侧发

在"源站 + CDN"的结构里,这个头两条路径都会加:

  1. 源站 nginx 配置里写的 add_header Strict-Transport-Security ...;
  2. CDN 侧(很多厂商在控制台里默认打开"HTTPS 强制"并会自动下发 HSTS)。

正确做法是二选一:

  • 让 CDN 发(推荐用于接入 CDN 的站):源站把 HSTS 注释/删掉。这样只有一条,且由离用户更近的一侧下发;
  • 让源站发:那就去 CDN 控制台关掉它的 HSTS。

我选的是前者——源站那段 add_header 注释掉,备份好原文件。

四、配置示例

# 源站在已接 CDN 的情况下不再下发 HSTS(避免与 CDN 重复,RFC 6797 只取第一个)
#add_header Strict-Transport-Security "max-age=31536000";

注意几点:

  • 改之前备份 vhost(面板重新生成站点配置会覆盖你的手改,所以改动最好放在独立文件里,或者记清备份路径);
  • always 参数的作用是"无论状态码如何都加这个头",去掉时一并注意;
  • 改完 nginx -t && nginx -s reload。

五、验证与一个容易忽略的缓存问题

curl -sI https://www.example.com/ | grep -c -i 'strict-transport-security'   # 期望 1

如果还是 2,先别怀疑配置——CDN 和浏览器都可能缓存了响应头:

  1. CDN 侧刷新一下缓存(一般在控制台有"刷新缓存"按钮,或带随机参数请求);
  2. 浏览器自己的 HSTS 缓存是独立的(在 chrome://net-internals/#hsts 里能看和清),但它只影响你的浏览器行为,不影响服务端返回什么。

六、关于 preload:想清楚再上

HSTS preload 是把自己加进浏览器内置列表,让首次访问就强制 HTTPS。它几乎没有回头路——申请容易,移除要等浏览器版本更新,期间你的域名在用户浏览器里就是"只能用 https"。

建议:站点至少稳定运行半年、所有子域都确认支持 HTTPS 之后再考虑,而且先用短 max-age 试一段时间。

七、回滚

取消注释即可恢复由源站下发。但回滚时要同时确认 CDN 那一侧是否也开着——两侧都开就是你一开始遇到的问题。


一句话总结:HSTS 出现两次时,浏览器只认第一个,而你没法控制哪个在前。接入 CDN 后第一件事就是检查这个头,只保留一侧下发;includeSubDomains 和 preload 都要在确认影响范围之后再加。