如果你已经给网站配好了 HTTPS 证书,并且做了 301 跳转,觉得这样就万事大吉了,那我建议你停一下,先看看一个问题:用户在浏览器里输入http://开头的地址时,浏览器发出的第一个请求,其实仍然是明文 HTTP 请求,只是服务器端把它重定向到了 HTTPS。这中间的那一次明文请求,就存在被劫持的可能。Nginx 开启 HSTS 之后,浏览器会在本地直接记住“这个域名必须走 HTTPS”,连明文请求都不会发出去,这个体验和安全级别是完全不同的。这篇文章我会从 HSTS 的原理讲起,然后给出 Nginx 配置的具体写法、验证方法和踩坑记录,适合已经部署了 HTTPS 但想进一步加固的开发者,也适合刚入门、想搞清楚跳转机制的新手。
1. HSTS 是什么,为什么 301 跳转远远不够
很多人一听 HSTS 就以为只是“换个响应头”,其实它改变了浏览器和服务器之间的信任方式。先把原理弄清楚,后面配置才不会踩坑。
1.1 301 跳转留下了什么隐患
先说一个最直观的场景:用户访问http://example.com,服务器返回 301,告诉浏览器“你去https://example.com”。看起来没毛病,但问题出在这个过程本身。
浏览器发出去的第一个请求是明文 HTTP 请求,这个请求如果出现在公共 Wi-Fi、运营商网络或者被植入广告的路由器环境下,中间人是可以看到或者说篡改这个请求的。更严重的一种情况是,攻击者可以在用户发出这个请求之前就拦截掉,直接返回一个伪造页面,用户根本到不了你的 301 重定向那一步。这就是业内常说的“第一次访问问题”(First Request Problem)。
另外还有一个体验层面的问题:每次用户在地址栏输入http://前缀,或者点击了一个旧链接,浏览器都得先发一次 HTTP 请求、等服务器返回 301、再发起 HTTPS 请求。多一次请求就多一次延迟,虽然体感不明显,但日复一日积累下来,也是一笔不必要的耗时。
1.2 HSTS 的交互闭环
HSTS(HTTP Strict Transport Security,HTTP 严格传输安全)解决的就是上面这个问题。它的核心机制是:服务器通过响应头告诉浏览器——在接下来的一段时间内,你对这个域名的所有访问请求,都直接使用 HTTPS,不要用 HTTP。
当浏览器收到带有 HSTS 头的 HTTPS 响应后,会在本地维护一份“HSTS 列表”,记录这个域名的安全策略。下次用户再输入http://example.com的时候,浏览器会在本地直接把这个地址改写成https://example.com,然后才发出请求。注意这个过程发生在浏览器内部,不会产生任何明文 HTTP 请求。
你可以这样理解:301 跳转是“你到了错误的站台,售票员告诉你隔壁才是对的”;HSTS 则相当于“你把目的地的车票直接买对了,压根不会走进错误的站台”。前者是补救,后者是预防。
1.3 max-age、includeSubDomains、preload 三个参数的含义
HSTS 响应头长这样:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload三个参数各管各的事:
max-age:单位是秒,告诉浏览器这个策略有效期多久。31536000是一年。超过这个时间,浏览器会重新询问服务器是否继续执行强制 HTTPS 策略。includeSubDomains:表示这个策略对子域名同样生效。比如你配了example.com,如果带上这个参数,www.example.com、api.example.com这些子域名也会被强制走 HTTPS,不带上就只对主域名生效。preload:表示允许浏览器厂商把当前域名加入“预加载列表”。这个列表会内置在浏览器发布包中,即使从未访问过该网站,浏览器也知道必须用 HTTPS 访问。提交预加载列表需要去专门的站点申请,审核通过后生效。
搞清楚这三个参数的含义之后,接下来就能进入正题了。
2. Nginx 开启 HSTS 的配置写法与细节
Nginx 开启 HSTS 其实就是加一行响应头的事,但“一行”放在哪里、怎么加,里面有不少讲究。
2.1 推荐配置模板
这里给你一份可以直接套用的配置模板。假设你的 Nginx 配置里,HTTPS 站点是配置在/etc/nginx/conf.d/下的独立文件里:
server { listen 443 ssl http2; server_name example.com www.example.com; # 证书配置这里省略,请按实际情况写 # ssl_certificate /path/to/fullchain.pem; # ssl_certificate_key /path/to/privkey.pem; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # 其他 location 配置省略 }这里有一个关键点:add_header指令我放在了server块内部。放在server块里,意味着该站点的所有响应都会带上这个头。很多人图方便把 HSTS 头放在http块里,也可以,但要小心,那样会对这台 Nginx 上的所有站点生效。如果某个站点还没准备好全站 HTTPS,就会出问题。所以我个人的建议是:放在站点自己的server块里,精确控制。
2.2 always 参数别漏,否则 404 响应头上没有 HSTS
add_header指令默认情况下只在响应状态码为 200、201、204、206、301、302、303、304、307、308 时才生效。也就是说,如果你的站点某个页面返回 404 或 500,响应头里就不会带上 HSTS。
这会造成什么后果?用户第一次访问一个不存在的链接,服务器返回 404,响应头里没有 HSTS,浏览器就不会记录这个域名的强制 HTTPS 策略。他接下来如果直接输入http://访问,就还是会先发明文请求。
解决办法就是加always参数:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;用上always之后,不管响应状态码是什么,这个头都会带在响应里,浏览器就能在任意响应中接收到 HSTS 策略并记录下来。这一步是很多人都忽略的坑,但作用非常大。
2.3 includeSubDomains 的开与关
includeSubDomains是一个需要谨慎决策的参数。开了它,所有子域名都会被强制 HTTPS。如果你的某些子域名不是 HTTPS 服务(比如还在用 HTTP 的内部管理后台、或者偶尔开的临时服务),一旦浏览器记住了含includeSubDomains的 HSTS 策略,这些子域名就直接访问不了了,用户会看到不安全警告,强制跳转不到任何页面。
在决定开不开之前,先做一次子域名盘点。列一个命令行检查:
for sub in www api blog oldtest do curl -sI --max-time 3 "http://$sub.example.com" | head -n 1 done看看哪些子域名还在响应 HTTP。只要有任何一个是 HTTP 明文服务,就暂时不要加includeSubDomains,等全部迁移到 HTTPS 之后再开。
2.4 HTTP 跳转和 HSTS 怎么配合
HSTS 不是用来替代 301 跳转的,两者要配合着用。你的 HTTP 配置里应该保留跳转规则:
server { listen 80; server_name example.com www.example.com; # HTTP 端口一律 301 跳转到 HTTPS return 301 https://example.com$request_uri; }HSTS 是给已经支持 HTTPS 的浏览器用的“强化策略”,301 是给第一次访问、还没有任何策略记录的浏览器用的“兜底方案”。两者配齐,才算完整。
另外要注意,不要把add_header Strict-Transport-Security写到 80 端口的server块里,因为 HSTS 头只有通过 HTTPS 传递才有意义。通过 HTTP 传递 HSTS 头是无效的,浏览器不会接受明文传输的安全策略。
3. 实操记录:从改配置到验证生效
这一节把配置和验证的完整流程走一遍,步骤是可以直接照做的。
3.1 配置前的四项检查
动手改配置之前,先做几项检查,避免改完之后出问题还得回滚。
第一,确认证书本身合法有效。用命令看:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates第二,确认当前页面里没有加载任何 HTTP 资源。Chrome 开发者工具或者浏览器的控制台里可以看到混合内容警告。如果资源有遗漏,先修完再上 HSTS,否则那些资源会被浏览器直接拦截,页面功能会受影响。
第三,确认子域名列表是完整已知的。特别是你企业内部、测试环境、或者很久之前搭过的小工具站点,很多都会被忽略。真到了浏览器记住 HSTS 策略之后才想起来某个子域名是 HTTP,那时候就晚了。
第四,确认 Nginx 本身语法没问题。执行nginx -t,如果有配置错误会直接提示。
3.2 修改配置并平滑重载
进入站点配置文件,找到 HTTPS 那个server块,加入 HSTS 响应头。加完之后测试并重载:
nginx -t systemctl reload nginxreload和restart的区别在于,reload会平滑地重新加载配置,已经建立的连接不会被断开,只是新的连接使用新配置。这点对线上服务很重要,不要用restart。
修改完配置之后,立刻验证一下响应头是否带上了 HSTS:
curl -I https://example.com正常情况下,响应头里会包含这样一行:
Strict-Transport-Security: max-age=31536000; includeSubDomains3.3 三种验证 HSTS 生效的方法
配置加上了不代表浏览器就一定记住了,还需要从多个角度验证。
第一种方式:用 curl 确认服务器响应头。这个刚才已经说过了,注意每次修改配置后都执行一次,确保always参数生效。
第二种方式:在浏览器里直接测试。打开 Chrome 的开发者工具,切到 Network 标签,然后在地址栏输入http://example.com,观察请求记录。如果 HSTS 已经生效,你会发现网络面板里直接出现的是https://example.com的请求,根本没有任何http://请求出现过。
第三种方式:利用 Chrome 内部页面查状态。在 Chrome 地址栏输入:
chrome://net-internals/#hsts在查询框里输入你的域名,点击 Query。如果输出里有HSTS相关的记录,说明浏览器已经把该域名纳入了强制 HTTPS 列表。
3.4 灰度推进策略
HSTS 一旦让浏览器记下策略,在max-age到期之前很难清除。所以稳妥的做法是先短后长,分阶段推进。
我第一次部署时用的是max-age=300(5分钟),确认策略能下发、跳转正常、子域名没出问题之后,再改成max-age=86400(1天),最后才改成max-age=31536000(1年)。每次调整之间隔至少一天,观察线上日志有没有异常。
如果你中途发现某个子域名访问不了了,不用慌。在服务端把 HSTS 头改成max-age=0,浏览器收到后就会把这个域名的 HSTS 策略清掉。这个操作要在 HTTPS 响应里做,因为浏览器不会接受通过 HTTP 传过来的 HSTS 变更。
4. 常见问题与踩坑记录
HSTS 的配置本身简单,但实际落地过程中遇到的问题五花八门。下面这些是我实操中真实遇到过的,整理出来给你避坑。
4.1 配了 HSTS 但浏览器就是不生效
这种情况最常见的原因有三个。
第一个是没有加always参数。如果你访问的是一个 404 页面,或者某个资源返回 301、302,没有always的时候响应头里就没有 HSTS,浏览器自然记不住策略。把这个参数补上就正常了。
第二个是第一次访问走了 HTTP。浏览器如果在 HTTP 响应里看到 HSTS 头,会直接无视掉,因为明文传输的安全策略本身就不安全。想要让 HSTS 生效,必须确认第一条响应是通过 HTTPS 返回的。
第三个是访问的是 IP 地址而不是域名。浏览器只对域名记录 HSTS 策略,不会对 IP 地址记录。如果你用https://123.45.67.89测试,那是测不出来的,必须用你配置的域名。
4.2 max-age 设太短等于白配
有人为了“安全起见”把max-age设成了 3600(一小时),结果发现用户访问一次之后,一个小时内有效,超过一小时又回到了发明文请求的状态。如果遇上不活跃的用户,可能永远记不住 HSTS 策略。
从实际运维角度看,max-age至少应该是一年,也就是 31536000 秒。很多大厂甚至设到了两年或者更长。这个值是不影响“修改策略”的,只要你改响应头,下次浏览器收到新值就会重新开始计数。设长一点,安全性更好,管理成本也更低。
4.3 preload 提交被拒的常见原因
想申请加入浏览器预加载列表的话,要去专门的站点提交域名,但是提交之后不能马上生效,而且审核有一定规范,我见过不少被拒的例子,常见原因如下:
- 站点没有开启 HTTPS,或者证书只覆盖部分子域名。
- 提交时填的域名没有带上
includeSubDomains,而预加载列表要求必须包含。 - 首页或者站点根路径返回的不是 HTTPS 有效页面,而是 404 或 500。
- 证书链不完整,某些浏览器会判定为无效证书。
预加载一旦通过审核,你的域名就会被写进浏览器的内置列表里,之后即使你想关闭 HSTS,通过max-age=0也没法让预加载列表里的策略失效。所以一定要在确定全站 HTTPS 已经稳定运行很长时间之后再申请预加载。
4.4 经过 CDN 后 HSTS 头丢了
如果你的网站走了 CDN,需要特别注意 CDN 会不会透传后端响应头。有些 CDN 默认配置下会过滤掉源站的某些响应头,HSTS 头就是其中之一。
排查方法很简单:在 CDN 后面直接请求你的源站域名,看源站返回的响应头里有没有 HSTS;然后再请求 CDN 的加速域名,看响应头里还有没有。如果源站有、CDN 没有,说明 CDN 在往下游转发时把 HSTS 头丢掉了。
解决办法是在 CDN 层也加上 HSTS 头,而不是只在源站加。CDN 控制台一般都有自定义响应头配置,把下面这一行加进去即可:
Strict-Transport-Security: max-age=31536000加完之后再验证一次下游响应。记住:HSTS 头必须每个经过的转发节点都带上,浏览器最终看到的才会包含这个策略。
4.5 证书到期与 HSTS 的联动风险
这一点很多人没意识到:HSTS 的风险点不在配置,而在证书。
一旦浏览器记住了 HSTS 策略,它是不会让你“退回”到 HTTP 去访问的。如果某一天你的 HTTPS 证书恰好到期了,而证书没有自动续期,用户访问时浏览器会显示证书错误页面。由于 HSTS 的作用,用户也没法绕过错误提示去访问 HTTP 版本。本来证书过期只是提示一个警告,现在连退路都被封死了。
所以我现在的习惯是:所有配了 HSTS 的站点,必须搭配证书自动续期工具,不管是 certbot 还是其他机制,都要确保证书不会到期。同时还要加一个监控,证书剩余有效期少于 30 天时发告警。
4.6 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| HSTS 响应头看不到 | 忘加always参数 | 加上always |
| 浏览器不跳转 HTTPS | 首次访问走了 HTTP | 确认 443 响应头包含 HSTS |
| 子域名访问被强制跳转 | 开了includeSubDomains但子域名未全 HTTPS | 先迁移子域名,或暂时不开 |
| preload 审核不通过 | 子域名不齐、未带 includeSubDomains | 补全子域名 HTTPS,重新提交 |
| 上游 CDN 不带 HSTS 头 | CDN 过滤了自定义头 | 在 CDN 层添加响应头 |
| 证书到期后没有退路 | HSTS 策略生效中 | 配置自动续期 + 到期监控 |
| 想清除已记录的 HSTS | 策略已生效 | 响应头改max-age=0 |
5. 把 HSTS 纳入日常运维的几个习惯
到了这一步,配置层面基本讲完了。但配置只是开始,真正要让 HSTS 稳定运行,还得把几个后续动作做成习惯。
5.1 定期用在线评分工具和安全扫描检查
我自己习惯每隔一段时间去测一次站点的安全标头。除了 HSTS,还会关注Content-Security-Policy、X-Content-Type-Options、X-Frame-Options等。这些响应头配合在一起,才能构成完整的响应头安全体系。HSTS 单独配了只是一个点,放在整体安全配置里才是面。
要把“检查响应头”做成一个固定动作。这个操作不用太频繁,每次改动 Nginx 配置之后跑一遍就行。我一般用一个简单的本地脚本:
curl -sI https://example.com | grep -i "Strict-Transport-Security"有没有输出、输出内容对不对,一目了然。
5.2 HSTS 和证书自动续期的配合
前面提到过证书自动续期的重要性,这里展开说一下链路。
证书续期工具会修改你的 Nginx 配置,然后重载服务。如果你的 HSTS 配置写在证书配置相近的位置,建议把这两块配置放在同一个文件里,这样维护起来才不会漏。
我见过更新的配置把 HSTS 头写在location块里的,结果证书续期工具重写配置文件之后,新生成的配置里没有location块的重写,HSTS 头就没了。这类问题很隐蔽,不会报错,但安全性瞬间掉回解放前。
所以定期巡检的时候,Nginx 配置里add_header Strict-Transport-Security是否存在,要作为一项检查内容。我都是直接把配置文件里 HSTS 相关的行用 grep 搜出来看的。
5.3 灰度、监控、回滚三步走
把 HSTS 当成一次变更来管理,而不是“加一行配置就完事”,这个心态转变很重要。
灰度:先在小范围环境验证,比如临时改一两个站点域名,确认没问题后再全量推进到所有站点。
监控:上线后观察 HTTPS 访问量有没有异常上涨、HTTP 跳转量有没有下降、有没有大范围的“用户无法访问”类工单。HTTPS 访问量上涨是正常的,因为强制走了 HTTPS,HTTP 的明文访问量会自然减少。
回滚:改了 HSTS 头之后,如果发现问题,把响应头改成:
Strict-Transport-Security: max-age=0这会让浏览器清除掉本地记录的 HSTS 策略。注意这个操作必须在 HTTPS 环境下完成,而且对于预加载列表里的域名无效。
5.4 下一阶段的优化方向
HSTS 配置稳定之后,可以往下推进 HTTPS 站点的其他优化:
- 开启 OCSP Stapling,减少浏览器验证证书链的时间。
- 把 TLS 版本和加密套件收紧,禁用已经不安全的旧版本协议。
- 做好 HTTP/2 的配置优化,让页面加载更快。
- 配合安全响应头的整体配置,提升站点在被各类安全检查工具扫描时的表现。
这些方向每一个单独拿出来都能写一长篇文章,但 HSTS 是整个 HTTPS 加固里面性价比最高、见效最快的一步。
从我个人的实际操作体会来说,给 Nginx 配好 HSTS 之后,最直观的感受是:在内网 Wi-Fi 环境下第一次访问站点时,地址栏里直接就是绿色锁标,中间不会再闪一下“被 301 转到 HTTPS”的过程。整个跳转链路干净利落。尤其是把includeSubDomains和preload全部配齐之后,站点在浏览器里的安全等级直接上升了一个档次。如果你目前只在 Nginx 里配了证书和 301 跳转,HSTS 就是你下一步最值得做的事。先把max-age=31536000加上,稳定运行后考虑includeSubDomains,再之后评估要不要申请预加载。每一步都不急,但每一步都不该省。