news 2026/10/17 1:16:55

Nginx 配置 HSTS 实战:告别 301 明文跳转,彻底强制 HTTPS

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nginx 配置 HSTS 实战:告别 301 明文跳转,彻底强制 HTTPS

如果你已经给网站配好了 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 nginx

reload和restart的区别在于,reload会平滑地重新加载配置,已经建立的连接不会被断开,只是新的连接使用新配置。这点对线上服务很重要,不要用restart。

修改完配置之后,立刻验证一下响应头是否带上了 HSTS:

curl -I https://example.com

正常情况下,响应头里会包含这样一行:

Strict-Transport-Security: max-age=31536000; includeSubDomains

3.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,再之后评估要不要申请预加载。每一步都不急,但每一步都不该省。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 1:16:24

决策树算法通俗指南:熵、基尼系数与调参实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:15:42

SCENRED场景缩减实战:GAMS把10000个随机场景降到300个

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:15:21

基于MP2642GR-Z与MK24FN256VDC12的两串电池主动均衡方案设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:15:13

Windows 深度学习环境搭建:Anaconda3 + TensorFlow + PyCharm 避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:14:38

储能电池双串被动均衡方案:MP2642GR-Z与PIC18LF45K40实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 1:13:40

双节锂电池主动均衡设计:MP2642+PIC18LF4680硬件闭环方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华