问题背景
前文说到因为 Netlify 在境外,访问速度不太理想,所以迁移到了阿里云。又因为之前在 Netlify 时,一键启用 HTTPS(再次怀念🥺),所以就没有手动配置相关证书。而在域名 charlesfeng.cn 备案下来后,我着手将其解析到自己的服务器上,完成前文的剩余工作。理想情况下,我只需要在阿里云 DNS 云解析上添加相应的 A 解析到对应 ip,然后在服务器上启用 Let's Encrypt 的免费证书就完成。
但是,理想很丰满,现实很残酷,在添加对应 DNS Record 后我通过浏览器始终无法访问网页。最开始认为是 DNS 还未生效,但是十来分钟后可以通过域名 ping 到对应 ip,而浏览器仍然无法访问,就很奇怪了。Chrome 和 Safari 行为相同,所以下文均以 Chrome 为例讲解。
debug 思路
DNS Record 未生效
首先排除此思路,因为如下图我已经可以 ping 到了,说明 DNS Resolver 已经同步了对应记录。如果需要复习 DNS 的解析过程可参考这里。
浏览器的自身缓存
其次,怀疑是 Chrome 浏览器自身缓存了 DNS Record,所以解析到原先 Netlify 的域名 charlesfeng.netlify.app 上去了,但是这有两个不合理点。
- 就算解析到原先 Netlify 的域名,那么我 Netlify 的域名也是能够正常访问的。
- 既然缓存了,那么就把缓存清掉。(不过我都刷新 N 次了,Chrome 还不去更新下吗?这样感觉很不科学。)于是,查找资料,通过
chrome://net-internals/#dns
清楚了缓存,still not working。
Cookie
此时,我突然福临心至,打开无痕模式尝试访问,结果成功了(这就很迷🥱)。同一台机器的 Chrome 是否无痕模式还能有两种操作...印象中,无痕模式不是只是不携带 Cookie 之类的吗?跟能不能访问博客有什么关系...不过还是尝试清除 Cookie,意料之中不起作用🥶
控制台
最后,我终于想起来看 Chrome 控制台了...结果如下图。(还是不够有直觉啊 555,我不是一个合格的程序员😤)
可以看到,当输入域名 http://charlesfeng.cn 后,首先返回了 307 状态码,重定向到 https://charlesfeng.cn,而我目前还没有配置 SSL 证书,所以自然无法安全访问。
因此,问题明确。
HSTS
观察到上图 HTTP 转发处有 Header——Non-Authoritative-Reason : HSTS
。首先我们需要了解 HSTS 是什么。下面是来自 MDN 的相关介绍。
HSTS (英语:HTTP Strict Transport Security,HTTP 严格传输安全)让网站可以通知浏览器它不应该再使用 HTTP 加载该网站,而是自动转换该网站的所有的 HTTP 链接至更安全的 HTTPS。它包含在 HTTP 的协议头
Strict-Transport-Security
中,在服务器返回资源时带上。换句话说,它告诉浏览器将 URL 协议从 HTTP 更改为 HTTPS(会更安全),并要求浏览器对每个请求执行此操作。
所以,猜测是 Netlify 自动在响应中设置了 HSTS,从而导致我这个域名被强制跳转 HTTPS 了。
访问我的另一个域名 cuihuaergou.top 验证下。(对,我的域名就是这么多🥰)可以看到确实在响应中设置了 Strict-Transport-Security
字段,max-age=31536000
表示自动使用 HTTPS 连接的时间为一年。(虽然害我这里查了会儿资料,但是 Netlify 这功能也太好了叭😭,学习了学习了)
详细的 Strict-Transport-Security
可以参见 MDN 的相关说明,需要特别注意的是非加密传输(即 HTTP)时设置的 HSTS 字段无效, Strict-Transport-Security
字段会被浏览器忽略。(因为攻击者可以通过中间人攻击的方式在连接中修改、注入或删除它。只有在你的网站通过 HTTPS 访问并且没有证书错误时,浏览器才认为你的网站支持 HTTPS 然后使用 Strict-Transport-Security
的值。)
下面记录下浏览器如何处理的。
你的网站第一次通过 HTTPS 请求,服务器响应
Strict-Transport-Security
头,浏览器记录下这些信息,然后后面尝试访问这个网站的请求都会自动把 HTTP 替换为 HTTPS。当 HSTS 头设置的过期时间到了,后面通过 HTTP 的访问恢复到正常模式,不会再自动跳转到 HTTPS。
每次浏览器接收到 Strict-Transport-Security 头,它都会更新这个网站的过期时间,所以网站可以刷新这些信息,防止过期发生。
Chrome、Firefox 等浏览器里,当您尝试访问该域名下的内容时,会产生一个 307 Internal Redirect(内部跳转),自动跳转到 HTTPS 请求。
注意最后一段话,Chrome 访问该域名时,会产生一个 307 的内部跳转,并自动重定向到该地址的 HTTPS 版本。这个 307 响应是虚假的(dummy),而非服务器生成的——即 Chrome 是先在内部进行了此操作,(注意此 307 状态码的描述是 Internal Redirect,而 307 状态码本身的描述是 Temporary Redirect,)然后才发出真正到达目标服务器的 HTTPS 请求。
(扩充知识:Edge 是直接改变 URL。)
除了网站手动设置 Header Strict-Transport-Security
,还可以通过预加载 HSTS 的方式,将自己的域名提交到 Chrome 自动包含的预加载列表中。
谷歌维护着一个 HSTS 预加载服务。按照如下指示成功提交你的域名后,浏览器将会永不使用非安全的方式连接到你的域名。虽然该服务是由谷歌提供的,但所有浏览器都有使用这份列表的意向(或者已经在用了)。但是,这不是 HSTS 标准的一部分,也不该被当作正式的内容。
- Chrome & Chromium 的 HSTS 预加载列表: https://www.chromium.org/hsts
- Firefox 的 HSTS 预加载列表:nsSTSPreloadList.inc
作用
作用与不足均参考维基。
HSTS 可以用来抵御 SSL 剥离攻击。SSL 剥离攻击是中间人攻击的一种,由 Moxie Marlinspike 于 2009 年发明。他在当年的黑帽大会上发表的题为 "New Tricks For Defeating SSL In Practice" 的演讲中将这种攻击方式公开。SSL 剥离的实施方法是阻止浏览器与服务器创建 HTTPS 连接。它的前提是用户很少直接在地址栏输入
https://
,用户总是通过点击链接或 3xx 重定向,从 HTTP 页面进入 HTTPS 页面。所以攻击者可以在用户访问 HTTP 页面时替换所有https://
开头的链接为http://
,达到阻止 HTTPS 的目的。HSTS 可以很大程度上解决 SSL 剥离攻击,因为只要浏览器曾经与服务器创建过一次安全连接,之后浏览器会强制使用 HTTPS,即使链接被换成了 HTTP。
另外,如果中间人使用自己的自签名证书来进行攻击,浏览器会给出警告,但是许多用户会忽略警告。HSTS 解决了这一问题,一旦服务器发送了 HSTS 字段,将不再允许用户忽略警告。
不足
用户首次访问某网站是不受 HSTS 保护的。这是因为首次访问时,浏览器还未收到 HSTS,所以仍有可能通过明文 HTTP 来访问。解决这个不足当前有两种方案,一是浏览器预置 HSTS 域名列表,Google Chrome、Firefox、Internet Explorer 和 Microsoft Edge 实现了这一方案。二是将 HSTS 信息加入到域名系统记录中。但这需要保证 DNS 的安全性,也就是需要部署域名系统安全扩展。截至 2016 年这一方案没有大规模部署。
由于 HSTS 会在一定时间后失效(有效期由
max-age
指定),所以浏览器是否强制 HSTS 策略取决于当前系统时间。部分操作系统经常通过网络时间协议更新系统时间,如 Ubuntu 每次连接网络时、OS X Lion 每隔9分钟会自动连接时间服务器。攻击者可以通过伪造 NTP 信息,设置错误时间来绕过 HSTS。解决方法是认证 NTP 信息,或者禁止 NTP 大幅度增减时间。比如 Windows 8 每 7 天更新一次时间,并且要求每次 NTP 设置的时间与当前时间不得超过 15 小时。
解决思路
虽然最后自己肯定会上 HTTPS,上了之后肯定就没这个问题了,但是咱不能这样对不对,咱是专业的,所以来解决下。
在 StackOverflow 上发现这样几种解决方案。
- 在 Chrome 的 URL 字段中输入以下内容:
chrome://net-internals/#hsts
,然后搜索您的网站并将其删除。(最后我采用的😋) - 您可能是在顶级域中设置的 HSTS 并包含了子域,因此可以从其中删除子域。(原文:You may also set this at a top level domain and include subdomains so you may need to delete from there.)(对于此处的「顶级域」我的理解是指一级域名 / 主域名,原文想描述的情况是
Strict-Transport-Security
字段中设置了子域的情况,语法为Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
。这样当子域不再需要 HTTPS 时,可以对父域的 Header 进行更新以丢弃includeSubDomains
。) - 更改服务器配置中的 Header
Strict-Transport-Security
字段:先发布max-age
为 0 的 Header,然后重新访问网站以清除此 Header,最后停止发布此 Header。这对于其他不太容易清除 Header 的浏览器很有帮助。
请注意,如果网站位于预加载列表中,则无法清除此设置,因为该网站已嵌入在 Web 浏览器的代码中。网站所有者可以提交请求,要求从预加载列表中进行删除。但这对于 Chrome 而言,需要花费几个月的时间才能走完其发布周期;而对于其他浏览器而言,则没有明确的时间表。出于安全原因,Chrome 也无法覆盖预加载的设置。
对于上述解决方案 1,更详细的说明在此处。可能是因为 Chrome 版本更新的问题(我是 v81.0.4044.138),具体界面跟其描述的不太一致,所以也记录下。(涨字数)
- In the address bar, type “chrome://net-internals/#hsts”.
- Type the domain name in the text field below “Delete domain security policies”.
- Click the “Delete” button.
- Type the domain name in the text field below “Query HSTS/PKP domain”.
- Click the “Query” button.
- Your response should be “Not found”.
在 Chrome 中键入 chrome://net-internals/#hsts
,先搜索域名,结果如下图。删除后,可以通过 HTTP 正常访问。
Safari 较 Chrome 的不同
估计 Safari 也是同样的问题,但是按照上述解决方案的 Safari 版本(如下所示)解决并没能成功...改天再找找吧😂(不存在的)
- Close Safari.
- Delete the ~/Library/Cookies/HSTS.plist file.
- Reopen Safari.
参考
- 如何刷新本机DNS缓存(Win+Linux+OSX)
- MDN 307 Temporary Redirect
- MDN HSTS
- MDN HTTP Strict Transport Security
- WIKI HTTP 严格传输安全
- How to configure HSTS on www and other subdomains
- Status Code:307 Internal Redirect 和 Non-Authoritative-Reason:HSTS 问题
- Non-Authoritative-Reason header field HTTP
- How to clear HSTS from your browser