深入理解 ASP.NET Core 中的UseHsts()

一、什么是 HSTS

HSTS(HTTP Strict Transport Security,HTTP 严格传输安全)是一种由 RFC 6797 定义的 Web 安全策略机制。它的核心思想很简单:网站通过一个响应头告诉浏览器"今后只能用 HTTPS 访问我" ,浏览器记住这个指令后,在指定有效期内会强制将所有对该域名的请求转换为 HTTPS,即使用户手动输入 http:// 或点击了 HTTP 链接。

ASP.NET Core 中,app.UseHsts() 就是用来注册这个中间件的,它会在 HTTPS 响应中自动添加 Strict-Transport-Security 响应头。

二、为什么需要 HSTS:它解决的核心问题

要理解 HSTS 的价值,必须先理解它要防御的攻击场景。

2.1 中间人攻击与 SSL 剥离(SSL Stripping)

考虑一个典型流程:用户在浏览器地址栏输入 example.com(没有写协议)。浏览器默认会先发起一个 HTTP 请求:

复制代码
GET http://example.com/  →  301 Redirect to https://example.com/

问题在于第一次的 HTTP 请求是明文的。如果用户处在不可信网络(公共 WiFi、被劫持的路由器)中,攻击者可以:

  1. 拦截这个明文 HTTP 请求;
  2. 自己以 HTTPS 与真实服务器通信,但与用户始终保持 HTTP 连接;
  3. 用户全程"以为"自己在正常浏览,实际所有数据对攻击者完全透明。

这就是 SSL Stripping(SSL 剥离)攻击 ,由安全研究员 Moxie Marlinspike 在 2009 年提出。仅靠服务端的 301 重定向无法防御它,因为攻击发生在重定向到达浏览器之前。

2.2 HSTS 如何打破这个攻击链

一旦浏览器收到过该域名的 HSTS 头并记住它,后续行为就变成:

复制代码
用户输入 example.com
  → 浏览器内部直接改写为 https://example.com(307 Internal Redirect,根本不发出 HTTP 请求)
  → 攻击者没有任何明文请求可以拦截

关键区别在于:重定向从"服务器告诉浏览器"变成了"浏览器自己强制执行",明文请求这个攻击窗口被彻底消除。

三、Strict-Transport-Security 响应头剖析

UseHsts() 最终产生的就是这样一个头:

复制代码
Strict-Transport-Security: max-age=2592000; includeSubDomains; preload

它由三个指令组成:

指令 含义
max-age 浏览器记住"仅 HTTPS"策略的秒数。每次收到该头都会刷新计时。max-age=0 表示立即清除策略。
includeSubDomains 策略同时应用于所有子域名。例如对 example.com 设置后,a.example.comb.example.com 也被强制 HTTPS。
preload 申请将域名加入浏览器内置的 HSTS 预加载列表(详见第六节)。这不是 RFC 标准指令,而是浏览器厂商约定。

四、ASP.NET Core 中的配置详解

4.1 默认行为

模板生成的代码通常是这样:

csharp 复制代码
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();   // 仅在非开发环境启用
}

app.UseHttpsRedirection();

注意一个重要细节:默认 max-age 是 30 天(2,592,000 秒),而不是 RFC 推荐用于生产的较长值。这是框架刻意保守的选择------避免你在测试阶段误配后被长期"锁死"。

4.2 自定义配置

通过 AddHsts 在服务容器中配置选项:

csharp 复制代码
builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(365);          // 生产推荐至少 1 年
    options.ExcludedHosts.Add("localhost");           // 默认已包含一系列本地主机名
    options.ExcludedHosts.Add("internal.mycorp.lan");
});

HstsOptions 的核心成员:

选项 类型 说明
MaxAge TimeSpan 映射到 max-age,默认 30 天
IncludeSubDomains bool 是否添加 includeSubDomains,默认 false
Preload bool 是否添加 preload,默认 false
ExcludedHosts IList<string> 不发送 HSTS 头的主机名列表

4.3 ExcludedHosts 的默认值

这是容易踩坑的地方。ExcludedHosts 默认已经包含以下主机,因此即便你启用了 HSTS,访问这些地址也不会收到该头:

  • localhost
  • 127.0.0.1(IPv4 回环)
  • [::1](IPv6 回环)

设计原因是:HSTS 策略一旦被浏览器记住,会强制整个 localhost 走 HTTPS,这会干扰本机上其他用 HTTP 调试的项目。框架默认排除它们以避免污染开发环境。如果你想为某个域名启用 HSTS,又恰好该域名在排除列表中,需要手动清空或移除。

五、中间件的工作机制与放置顺序

5.1 只在 HTTPS 连接上发送

UseHsts() 中间件有一个常被忽略的行为:它只在请求本身是 HTTPS 时才添加响应头 。这是 RFC 6797 的强制要求------HSTS 头在明文 HTTP 响应中必须被浏览器忽略,因为一个能被篡改的明文头毫无安全意义。所以如果你的应用部署在反向代理后面,代理用 HTTP 与 Kestrel 通信,就必须正确配置 ForwardedHeadersMiddleware,让应用通过 X-Forwarded-Proto 识别出原始请求是 HTTPS,否则 HSTS 头永远不会发出。

5.2 UseHstsUseHttpsRedirection 的分工

两者经常一起出现,但职责不同,互为补充:
#mermaid-svg-ZCO4EOI4ltgn78WQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZCO4EOI4ltgn78WQ .error-icon{fill:#552222;}#mermaid-svg-ZCO4EOI4ltgn78WQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZCO4EOI4ltgn78WQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .marker.cross{stroke:#333333;}#mermaid-svg-ZCO4EOI4ltgn78WQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZCO4EOI4ltgn78WQ p{margin:0;}#mermaid-svg-ZCO4EOI4ltgn78WQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .cluster-label text{fill:#333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .cluster-label span{color:#333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .cluster-label span p{background-color:transparent;}#mermaid-svg-ZCO4EOI4ltgn78WQ .label text,#mermaid-svg-ZCO4EOI4ltgn78WQ span{fill:#333;color:#333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .node rect,#mermaid-svg-ZCO4EOI4ltgn78WQ .node circle,#mermaid-svg-ZCO4EOI4ltgn78WQ .node ellipse,#mermaid-svg-ZCO4EOI4ltgn78WQ .node polygon,#mermaid-svg-ZCO4EOI4ltgn78WQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZCO4EOI4ltgn78WQ .rough-node .label text,#mermaid-svg-ZCO4EOI4ltgn78WQ .node .label text,#mermaid-svg-ZCO4EOI4ltgn78WQ .image-shape .label,#mermaid-svg-ZCO4EOI4ltgn78WQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZCO4EOI4ltgn78WQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZCO4EOI4ltgn78WQ .rough-node .label,#mermaid-svg-ZCO4EOI4ltgn78WQ .node .label,#mermaid-svg-ZCO4EOI4ltgn78WQ .image-shape .label,#mermaid-svg-ZCO4EOI4ltgn78WQ .icon-shape .label{text-align:center;}#mermaid-svg-ZCO4EOI4ltgn78WQ .node.clickable{cursor:pointer;}#mermaid-svg-ZCO4EOI4ltgn78WQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .arrowheadPath{fill:#333333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZCO4EOI4ltgn78WQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZCO4EOI4ltgn78WQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZCO4EOI4ltgn78WQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZCO4EOI4ltgn78WQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZCO4EOI4ltgn78WQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZCO4EOI4ltgn78WQ .cluster text{fill:#333;}#mermaid-svg-ZCO4EOI4ltgn78WQ .cluster span{color:#333;}#mermaid-svg-ZCO4EOI4ltgn78WQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZCO4EOI4ltgn78WQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZCO4EOI4ltgn78WQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZCO4EOI4ltgn78WQ .icon-shape,#mermaid-svg-ZCO4EOI4ltgn78WQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZCO4EOI4ltgn78WQ .icon-shape p,#mermaid-svg-ZCO4EOI4ltgn78WQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZCO4EOI4ltgn78WQ .icon-shape .label rect,#mermaid-svg-ZCO4EOI4ltgn78WQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZCO4EOI4ltgn78WQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZCO4EOI4ltgn78WQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZCO4EOI4ltgn78WQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否(HTTP)
是(HTTPS)
请求到达
是 HTTPS 吗?
UseHttpsRedirection

返回 307/308 重定向到 HTTPS
UseHsts

添加 Strict-Transport-Security 头
浏览器重新发起 HTTPS 请求
浏览器记住策略

后续直接走 HTTPS

  • UseHttpsRedirection:负责把当前这次 HTTP 请求重定向到 HTTPS(服务端动作)。
  • UseHsts:负责让未来所有请求由浏览器自己强制 HTTPS(客户端动作)。

两者结合:第一次访问靠重定向兜底,之后靠 HSTS 从根本上杜绝明文请求。

5.3 推荐的中间件顺序

UseHsts() 应放在管道较前的位置,通常紧随异常处理之后、UseHttpsRedirection 之前:

csharp 复制代码
app.UseExceptionHandler("/Error");
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ...

六、HSTS 预加载列表(Preload List)

6.1 什么是预加载

HSTS 有一个固有的"首次访问"缺口:浏览器从未访问过某域名时,它还没收到过 HSTS 头,因此第一次访问仍可能走 HTTP,存在被攻击的窗口。

预加载列表解决了这个问题。它是一份由 Google 维护、被 Chrome、Firefox、Safari、Edge 等主流浏览器内置编译进二进制的域名清单。列表中的域名从浏览器安装的第一刻起就被强制 HTTPS,连首次访问的缺口都不存在。

6.2 加入预加载的硬性条件

要在 hstspreload.org 提交域名,必须同时满足:

  1. 提供有效的 HTTPS 证书;
  2. 将所有 HTTP 流量重定向到同主机的 HTTPS;
  3. 所有子域名都支持 HTTPS;
  4. 在 HTTPS 的根域名响应中发送 HSTS 头,且:
    • max-age 至少为 31,536,000 秒(1 年)
    • 包含 includeSubDomains
    • 包含 preload

对应的 ASP.NET Core 配置:

csharp 复制代码
builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(365);
});

6.3 预加载是把双刃剑

加入容易、移除极难是必须认清的现实。一旦进入预加载列表,想退出需要提交移除申请,并等待数周乃至数月,等待新版本浏览器发布并被用户更新。在此期间,列表中的域名及其所有子域名都无法通过 HTTP 提供服务。

因此实践建议是:确认所有子域名(包括未来可能新建的)都能长期支持 HTTPS 后再开启 preload,否则一个还在用 HTTP 的内部子域名会突然全面不可访问。

七、实践中的常见陷阱

陷阱一:开发环境误启用导致本机被锁。 若在开发中对 localhost 发出了 HSTS 头(例如手动从 ExcludedHosts 移除了它),浏览器会记住策略,导致本机其他 HTTP 调试项目无法访问。清除方法是在 Chrome 访问 chrome://net-internals/#hsts,在 "Delete domain security policies" 中输入域名删除。

陷阱二:反向代理下头未发出。 如前所述,代理与应用间是 HTTP 时,必须配好转发头中间件,否则应用认为请求是 HTTP,跳过发送 HSTS 头。

陷阱三:max-age 设置过大却未充分测试。 一旦发出长 max-age,在证书出问题或想临时回退 HTTP 时,用户浏览器仍会强制 HTTPS 且无法访问。回退唯一手段是发送 max-age=0 的头,但只有再次成功访问 HTTPS 的用户才能收到这个"解锁"指令。建议先用较短 max-age(如几分钟到几天)灰度验证,确认稳定后再逐步调长。

陷阱四:误以为 HSTS 能替代 HTTPS 重定向。 它不能。对从未访问过、又不在预加载列表中的用户,仍需 UseHttpsRedirection 处理首次的 HTTP 请求。两者缺一不可。

八、总结

app.UseHsts() 是一行看似简单的代码,背后是一套针对 SSL 剥离攻击的纵深防御机制。它的本质是把 HTTPS 的强制权从服务端转移到客户端,从而消除明文请求这个最脆弱的攻击面。

正确使用它需要把握几个要点:理解 max-ageincludeSubDomainspreload 三个指令的含义与风险;清楚它只在 HTTPS 连接上生效、需与 UseHttpsRedirection 配合;警惕 ExcludedHosts 的默认行为和预加载列表"难以撤回"的特性。生产环境的稳妥做法是:先以保守的 max-age 验证整条链路(含反向代理转发头),稳定后再逐步调长、考虑子域名覆盖,最后在确有把握时才申请预加载。

相关推荐
学编程的小程1 小时前
DISTINCT 的“惯性陷阱“:当去重操作沦为性能累赘
后端
雪宫街道2 小时前
SpringBoot 向 IOC 容器注册组件的两种姿势:@Configuration 与 @Import
java·spring boot·后端·spring
techdashen2 小时前
Cargo 1.94 开发周期全解析
开发语言·后端·rust
枕星而眠2 小时前
Linux守护进程完全指南:从原理到实战
linux·运维·服务器·c++·后端
金融支付架构实战指南3 小时前
Milvus 向量检索服务 + SpringBoot 实战:电商商品语义检索与相似商品推荐
spring boot·后端·milvus·向量检索
齐 飞3 小时前
JDK21虚拟线程
java·后端
fox_lht3 小时前
15.4.循环和迭代器的性能比较
开发语言·后端·学习·rust
摇滚侠3 小时前
SpringMVC 入门到实战 HttpMessageConverter 65-74
java·后端·spring·intellij-idea
Csvn4 小时前
用户与权限管理 — 从创建到精细化管控
后端