1. HSTS到底解决什么问题:先弄懂它为什么存在
我第一次真正认真研究HSTS,不是因为主动学习安全加固,而是被一个诡异的故障逼的。网站部署了HTTPS证书,浏览器也显示小锁图标,一切看起来正常。但过了一周,用户陆续反馈"网页打不开",我打开一看,浏览器直接提示"你无法访问该网站,因为网站使用的是HSTS。网络错误和攻击通常是暂时的,此页面可能无法正常工作"。
翻遍了服务器日志,查遍了证书配置,最后发现是自己之前加的一条响应头惹的祸。
1.1 SSL剥离攻击:为什么光有HTTPS还不够
先理清一个概念:HTTPS协议本身是安全的,问题出在用户第一次访问网站的那一刻。
假设你访问http://example.com,如果网站做了301跳转到https://example.com,在这个过程中,第一个HTTP请求是裸奔的。中间人拦截到这个请求后,可以偷偷把你的连接降级成HTTP,而服务器和浏览器都以为自己在用安全通道通信。这就是经典的SSL剥离攻击(SSL Stripping)。
简单做个类比:你去银行办理业务,银行保安把你领到VIP室,但你在路上被坏人换了个方向,带到了假柜台。你以为自己在和银行打交道,其实对面是骗子。
HSTS(HTTP Strict Transport Security,HTTP严格传输安全)的出现就是为了堵住这个入口。它通过服务器返回的响应头,告诉浏览器:这个网站只允许使用HTTPS访问,你在任何情况下都不要尝试HTTP明文连接。
1.2 HSTS的工作机制:一次握手,终身约束
HSTS的运作逻辑并不复杂。当浏览器第一次通过HTTPS访问一个网站时,服务器会在响应头里带上Strict-Transport-Security字段。浏览器记住这个信息之后,在指定时间内(由max-age参数控制),只要用户再访问这个域名,浏览器会自动把HTTP重写为HTTPS,且这个过程发生在任何网络请求发出之前。
关键点在于,这个"记住"发生在浏览器本地,不需要经过网络请求。所以即使中间人试图拦截,他看到的只是一次HTTPS连接请求,用HTTP协议根本无法和浏览器完成通信。
Strict-Transport-Security响应头的完整格式如下:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload三个参数的作用:
| 参数 | 作用 | 推荐值 |
|---|---|---|
max-age | 浏览器记住"只走HTTPS"的时长,单位是秒 | 至少31536000(即1年),不宜太短 |
includeSubDomains | 子域名是否同样强制HTTPS | 视情况添加,添加后所有子域名都被约束 |
preload | 允许域名提交到浏览器内置的HSTS预加载列表 | 确认无误后再加,加了很难撤销 |
顺便说一句,preload是为了解决"第一次访问"那个问题而生的。刚才提到HSTS要等浏览器访问过一次HTTPS才生效,但如果域名被收录到浏览器的预加载列表里,即使从没访问过,浏览器也天然就知道这个域名只能走HTTPS。提交流程在hstspreload.org官网进行,需要满足一定的审核条件。
1.3 HSTS生效的一个关键前提
这个点特别容易忽略:HSTS响应头必须通过HTTPS连接返回。换句话说,用户第一次用HTTP访问你的网站时,服务器返回的响应头即使带上了Strict-Transport-Security,浏览器也会直接忽略。
原因不难理解,HTTP是明文传输,中间人可以随意篡改响应头。如果浏览器信任明文通道里的HSTS指令,那攻击者直接伪造一条max-age=999999999的HSTS响应头,就能让用户彻底无法访问这个网站。
初次部署时,我建议先用浏览器开发者工具(F12)打开Network面板,确认HTTPS响应的响应头里真的出现了Strict-Transport-Security,再去考虑后续的推广和运维。
2. IIS上配置HSTS:三种方案,按场景选型
IIS(Internet Information Services)是Windows Server上常用的Web服务器,配置HSTS可以根据IIS版本和对灵活度的要求,选择不同的方案。
2.1 前置条件:先确保HTTPS已经跑通
在IIS上加HSTS之前,必须先完成两件事:
- 在服务器上安装有效的SSL证书,可以是企业级证书、Let's Encrypt免费证书,或云服务商提供的证书。
- 在IIS管理器里,为网站绑定HTTPS协议(443端口)和对应的证书。
证书这块我多说一句,HSTS强制浏览器只能走HTTPS,如果证书本身有问题(过期、域名不匹配、证书链不完整),用户就会被完全挡在门外,想回退到HTTP都不行。所以在开启HSTS之前,务必确认HTTPS访问一切正常。
验证方法也很简单:浏览器无痕窗口打开https://你的域名,能正常加载且地址栏有小锁标志,就算过关了。
2.2 方案一:web.config添加自定义响应头(最简单)
这是IIS上最直接的配置方式,适合只需添加静态响应头的场景。
打开站点根目录下的web.config文件,在<system.webServer>节点内加入<httpProtocol>配置:
<configuration> <system.webServer> <httpProtocol> <customHeaders> <remove name="Strict-Transport-Security" /> <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" /> </customHeaders> </httpProtocol> </system.webServer> </configuration>保存文件后,IIS会立即生效,不需要重启站点或服务器。<remove>指令是为了防止重复添加同名响应头,特别是当你在IIS管理器界面手动添加过之后再来改配置文件,这个防御动作有备无患。
验证方式:刷新HTTPS页面,F12在Network里找到主文档请求,查看Response Headers里是否出现了Strict-Transport-Security。
2.3 方案二:URL Rewrite模块动态添加(推荐)
如果站点已经有URL Rewrite规则,比如做了HTTP到HTTPS的301跳转,那么直接在现有规则集合里加一条HSTS响应头规则会更统一,方便后续集中管理。
前提是服务器已安装IIS URL Rewrite模块(下载和安装自行搜索,微软官方支持)。
在web.config的<rewrite>节点中加入:
<rewrite> <rules> <rule name="HTTP to HTTPS Redirect" stopProcessing="true"> <match url="(.*)" /> <conditions> <add input="{HTTPS}" pattern="off" ignoreCase="true" /> </conditions> <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" /> </rule> <rule name="Add HSTS Header" stopProcessing="true"> <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{HTTPS}" pattern="on" ignoreCase="true" /> </conditions> <action type="Rewrite" value="max-age=31536000; includeSubDomains" /> </rule> </rules> </rewrite>这个方案的好处是,HSTS响应头只在HTTPS请求时被写入,HTTP请求不会触发这条规则。使用serverVariable和Rewrite动作配合,比在<customHeaders>里写死更灵活——将来想调整max-age、增删includeSubDomains,只改一处即可。
注意:URL Rewrite的serverVariable写法是
RESPONSE_Strict_Transport_Security,不是HTTP_前缀,写错了规则不会生效,这个坑我自己踩过。
2.4 方案三:IIS 10版本上的原生支持
如果你用的是Windows Server 2016或更高版本,IIS 10开始原生支持HSTS配置,不需要辅助模块,直接在IIS管理器里操作。
操作路径:IIS管理器 → 选择站点 → 双击"HTTP响应头" → 右侧"添加" → 填写:
- 名称:
Strict-Transport-Security - 值:
max-age=31536000; includeSubDomains
这种方法最直观,适合不太想碰配置文件的运维同学。
实际项目中,我个人更推荐方案二(URL Rewrite),因为可以同时管理HTTP跳转和HSTS响应头,而且规则本身是声明式的,放到配置管理工具里也好维护。方案一适合快速验证,方案三则适合图形化操作偏好者。
2.5 配置后的自检清单
不管用哪个方案配完,都建议跑一遍自检:
curl -I https://你的域名重点看返回头里有没有Strict-Transport-Security字段,值是否正确。同时检查HTTP请求(curl -I http://你的域名)是否正常301跳转到HTTPS——注意,HTTP请求的响应头里不应该出现HSTS,出现也没用,浏览器不认。
3. Tomcat上配置HSTS:三条路线,各有利弊
Tomcat和IIS的架构思路完全不同,配置HSTS的姿势也更多样。
3.1 前置条件:HTTPS连接器就绪
Tomcat要支持HTTPS,需要先去conf/server.xml里配置SSL连接器。核心配置大致如下:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true"> <SSLHostConfig> <Certificate certificateKeystoreFile="conf/keystore.jks" certificateKeystorePassword="yourpassword" type="RSA" /> </SSLHostConfig> </Connector>证书可以用JKS格式(keytool命令生成),也可以用PKCS12格式(更推荐,兼容性更好)。无论哪种,先确认https://你的域名:8443能正常打开。
3.2 路线一:用Tomcat内置的HttpHeaderSecurityFilter(推荐)
Tomcat从8.0版本开始就提供了一个现成的过滤器叫HttpHeaderSecurityFilter,它不仅能加HSTS响应头,还顺手帮你处理了X-Content-Type-Options、X-Frame-Options这些常见安全头,性价比很高。
在应用的web.xml里注册过滤器:
<filter> <filter-name>httpHeaderSecurity</filter-name> <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class> <init-param> <param-name>hstsEnabled</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>hstsMaxAgeSeconds</param-name> <param-value>31536000</param-value> </init-param> <init-param> <param-name>hstsIncludeSubDomains</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>hstsPreload</param-name> <param-value>false</param-value> </init-param> </filter> <filter-mapping> <filter-name>httpHeaderSecurity</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>如果你不希望启用某些默认开启的安全头,可以查看Tomcat官方文档,通过对应的init-param关闭它们。比如antiClickJackingEnabled、blockContentTypeSniffingEnabled等等。
注意hstsPreload参数:当你准备提交到浏览器预加载列表,并且所有子域名都确定能走HTTPS时,再设为true。否则先保持false。
3.3 路线二:自写Filter实现(更灵活)
如果项目里没有用Tomcat的原生web.xml,而是Spring Boot内嵌Tomcat,或者希望在不同环境里动态控制HSTS策略,自己写一个Filter更好用。
import javax.servlet.*; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class HSTSFilter implements Filter { private long maxAgeSeconds = 31536000L; private boolean includeSubDomains = true; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; if (request.isSecure()) { StringBuilder hstsValue = new StringBuilder("max-age=").append(maxAgeSeconds); if (includeSubDomains) { hstsValue.append("; includeSubDomains"); } httpResponse.setHeader("Strict-Transport-Security", hstsValue.toString()); } chain.doFilter(request, response); } }注意request.isSecure()判断:只有HTTPS请求才写入HSTS响应头。如果忽略了这层判断,HTTP响应里也能看到这个头,虽然浏览器不会理会,但总归不规范。
在Spring Boot里,用@Configuration类注册这个Filter:
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SecurityConfig { @Bean public FilterRegistrationBean<HSTSFilter> hstsFilterRegistration() { FilterRegistrationBean<HSTSFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new HSTSFilter()); registration.addUrlPatterns("/*"); registration.setOrder(1); return registration; } }3.4 路线三:通过安全约束强制HTTPS
HSTS只是让浏览器"想"走HTTPS,但服务器本身也要能拒绝HTTP明文请求,才算闭环。Tomcat的web.xml中可以配置安全约束:
<security-constraint> <web-resource-collection> <web-resource-name>All Resources</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>加上这段之后,用户访问HTTP端口时,Tomcat会根据server.xml里的配置自动跳转到HTTPS端口。
需要特别说明的是,transport-guarantee是服务器端的强制跳转,HSTS是浏览器端的强制行为,两者配合才是完整方案:transport-guarantee负责把不听话的客户端拉回HTTPS,HSTS负责让浏览器以后不再发起HTTP请求。
3.5 Tomcat配置后的验证方法
配置完成后,通过以下命令验证效果:
curl -I https://你的域名:8443/正常返回里应包含:
Strict-Transport-Security: max-age=31536000; includeSubDomains同时再测试HTTP请求头,看是否会正确302到HTTPS。
4. 配置完反而访问不了:HSTS排错专项
这一节是我个人觉得最值得细看的,因为几乎所有搜"HSTS"相关关键词的人,一半是配置前学习,另一半是配置后救火。
4.1 最常见的三个故障原因
先看下"HSTS导致网站无法访问"的三种常见原因:
| 故障现象 | 根本原因 | 紧急处理 |
|---|---|---|
| 浏览器提示"HSTS错误",页面无法打开 | 证书失效、域名不匹配或证书链不完整 | 先修复证书,再考虑是否降低max-age |
| HTTP访问时没有正确跳转到HTTPS | 服务器端缺少301跳转或transport-guarantee配置 | 补全跳转规则 |
| 部分用户升级后无法访问 | 浏览器本地缓存的HSTS记录尚未过期 | 等max-age自然过期,或让用户清除浏览数据 |
HSTS最麻烦的地方在于:一旦浏览器在本地缓存了"只走HTTPS"的指令,即使你立刻把服务器配置改回来,用户端的浏览器依然会坚持到max-age过期为止。普通设置是31536000秒(一年),也就是说,配置错了但没察觉,未来整整一年里,这个域名在用户浏览器里都会被强制HTTPS。
这就是为什么我强调测试阶段要先用很小的max-age,比如300秒(5分钟),确认全部正常后,再逐步加大。
4.2 教你如何清除浏览器的HSTS缓存
用户端访问不了,首先要做的是清除浏览器里的HSTS记录。
Chrome和Edge浏览器,在地址栏输入:
chrome://net-internals/#hsts在"Delete domain security policies"区域,输入你的域名,点击"Delete"。清除之后,浏览器会忘记这个域名有过HSTS记录,下次访问时可以重新经历一次"HTTP跳HTTPS"的正常流程。
Firefox浏览器没有类似的隐藏页面,需要到"设置 → 隐私与安全 → Cookie和网站数据 → 清除数据",勾选"Cookie和网站数据"并确认清除。
提醒一下:清除浏览数据这个操作会连登录态一起清掉,用户操作成本不低。所以运维侧排查时,先确认服务器本身没问题,再让客户操作浏览器。
4.3 服务器端排查链路
我自己遇到"HSTS后无法访问"的场景,通常会按照以下顺序排查:
- 确认证书有效性:用在线SSL检测工具或本地openssl命令检查证书是否到期,域名是否包含
www和裸域。证书这步出错,HSTS模式下用户连"继续访问"的按钮都点不到。 - 确认HTTPS响应头:
curl -I https://域名看HSTS响应头是否真实存在。有时你只是加了配置但没生效,比如IIS的web.config节点位置写错,或Tomcat的Filter没加载。 - 检查HTTP是否跳转:
curl -I http://域名看是否有301或302,Location是否指向HTTPS地址。如果HTTP直接200返回了页面内容,用户侧就永远无法通过HSTS进入HTTPS。 - 用无痕窗口测试:普通窗口可能受HSTS缓存影响,无痕窗口是干净的,能更准确反映"新用户"视角下的访问体验。
分享一个我处理过的真实案例:客户反馈手机微信里打不开网站,电脑正常。排查后发现,客户的域名在hstspreload.org被提交到了预加载列表,而服务器只给主域名申请了证书,子域名m.domain.com没有覆盖到,导致移动端访问时被HSTS强制跳转HTTPS,但证书校验不过,直接白屏。费了好大劲,最快的处理方式不是等列表更新,而是先补齐子域名证书。
4.4 日志里怎么定位HSTS问题
IIS和Tomcat的日志里其实看不出"HSTS错误"这个敏感信息,但可以通过以下线索判断:
- 大量HTTP请求根本没有到达服务器(浏览器拦截了,没发出来)。
- HTTPS访问日志里出现大量4xx或5xx证书错误,说明TLS握手阶段就失败了。
- 如果网站本身就是纯HTTPS,还要看有没有客户端重复请求HTTP导致404。
多学一个排查工具没有坏处。PC上用chrome://net-internals/#hsts页面,还能在"Query domain"输入框里查出当前浏览器对该域名的HSTS状态,会显示是否static(预加载)或dynamic(动态缓存),这个功能排查时很好用。
5. 从280分到90分:HSTS的踩坑经验与部署建议
说完了配置和排错,最后聊点实际部署时才用得到的经验。
5.1 先短后长的灰度策略
用HSTS最忌讳一上来就max-age=31536000,万一中间出了岔子,整个域名的访问问题会持续一年。更稳妥的做法是分三步走:
- 第一阶段:
max-age=300,测试几天,观察有没有异常。 - 第二阶段:
max-age=86400(1天),运行一周。 - 第三阶段:
max-age=31536000; includeSubDomains,正式生效。
每次调整时,浏览器会在下一次HTTPS访问时刷新该域名的HSTS记录,所以逐步加长不会增加额外负担。
5.2 includeSubDomains一定要慎重
includeSubDomains这个词看着不起眼,但它会把你所有的子域名一起拉进强制HTTPS的范围。如果你的企业还有其他子域名正在跑HTTP服务(比如内部测试环境、旧版接口平台、静态资源服务器),开启之后它们会一起瘫痪。
所以这里有一个非常实用的建议:开启includeSubDomains之前,先梳理一遍DNS记录里所有的子域名,确认每个都能通过HTTPS正常访问。
如果实在无法保证所有子域名都支持HTTPS,就只配置:
Strict-Transport-Security: max-age=31536000不带includeSubDomains,安全性打了折,但不至于误伤。
5.3 提交到预加载列表之前想清楚
把域名提交到浏览器的预加载列表,意味着即使浏览器从没访问过你的网站,也天然知道"这个域名必须用HTTPS"。这样做彻底解决了首次访问时的安全问题,但代价是你几乎不可能从预加载列表中移除自己。
根据Chrome的规范,如果域名出现在预加载列表中,想要申请移除,需要等待至少两个版本周期,而且Chrome团队并不保证一定会处理。所以我的建议是:
- 先在正式环境跑1~2个月,确认HSTS稳定。
- 所有涉及的子域名都覆盖了有效证书。
- 确定未来一年内不会因为业务拆分导致子域名改用HTTP。
- 最后再去hstspreload.org提交。
5.4 HSTS与其他安全头的配合
HSTS只是安全响应头体系中的一个环节。在IIS或Tomcat上同时配置以下响应头,能组成更完善的安全防线:
| 响应头 | 作用 |
|---|---|
X-Content-Type-Options: nosniff | 禁止浏览器猜测文件类型,防止MIME混淆攻击 |
X-Frame-Options: SAMEORIGIN | 禁止页面被其他站点iframe嵌套,防点击劫持 |
Content-Security-Policy | 限制页面资源加载来源,缓解XSS |
Referrer-Policy | 控制跳转时Referer传递范围 |
这些头的配置方式和HSTS类似,在IIS里可以统一放在<customHeaders>中,在Tomcat里可以用HttpHeaderSecurityFilter一并处理,不需要额外开发。
5.5 我在实际运维中总结的几个习惯
配置HSTS这件事,次数多了之后,我养成了几个习惯,这里一并分享:
一是每次修改配置前备份web.config或server.xml。IIS那边一个空格写错,站点马上罢工,有备份能快速还原。Tomcat的server.xml修改后虽然需要重启才生效,但配置文件的备份同样不能省。
二是用本机curl做冒烟测试,不要只依赖浏览器。浏览器有缓存、有HSTS状态、有代理设置,干扰因素太多。curl请求能直观看清楚响应头,比F12更直接。
三是关注证书到期提醒。HSTS开启后,证书到期不再是"用户看到不安全警告"那么简单,而是"用户完全打不开网站"的严重故障。给SSL证书设置提前30天、7天、1天的提醒,哪怕是用最简单的日历提醒都行。
四是跟团队约定好"HSTS变更需要走变更流程"。这个头影响范围大、持续时间长,一旦上线出问题,回滚成本很高。
最后再补充一个重要提醒:HSTS是自己把路走窄的一种安全策略——它主动否决了HTTP这条退路。配置得当,它能帮你挡住SSL剥离攻击,配置失误,它会让你连自己网站的首页都打不开。希望这篇文章能帮你把前者变成现实,避开后者那些坑。