浅谈 HTTPS 强制跳转以及 HSTS

  在上一篇博文 『用 acme.sh 申请通配符证书及 HTTPS 相关配置』 中已经介绍了如何申请通配符证书并安装到 Nginx 服务中,在此不再赘述。
  为了让所有访客都通过 HTTPS 进行安全访问,我们需要将使用 HTTP 的连接强制跳转到 HTTPS 连接上。主要的方法有三种:

  • 前端跳转(不推荐)
  • 301/302 重定向(推荐)
  • HSTS(推荐)

  接下来我们详细说说这三种方法的实现方法以及利弊。

方法一:前端跳转

  前端使用 JavaScript 进行跳转,虽然有点蠢,但的确不失为一种办法。只需在所有页面的 HTML 里加入一段 JS 就能实现:

1
2
3
4
<script type="text/javascript">
if (window.location.protocol != "https")
window.location.href = "https" + window.location.href.substring(window.location.protocol.length);
</script>

优点

  我实在想不出来这种方法有什么优点。。。大概只有在我们控制不了服务端只能修改前端的时候才会选择这种下下策吧。

缺点

  • 因为是在浏览器端进行跳转,所以我们没法保证用户一定跳转了,很不安全
  • 跳转后又要请求下载一遍所有资源,浪费带宽
  • 在跳转之前所有的内容已经通过 HTTP 明文传输到客户端了,完全起不到保护的作用
  • 太多了,不一一列举了…

  当然,你可以让服务端对所有 HTTP 请求只返回这段 JS,对 HTTPS 请求返回正确页面,来增加一点点安全性。但是,既然你都已经分开处理 HTTP 和 HTTPS 请求了,那为什么不用接下来这两种方法呢?

方法二:301/302 重定向

  由服务端控制,如果请求的协议是 HTTP,则返回状态码 301(永久重定向) 或 302(暂时重定向),让浏览器重定向到 HTTPS 上。对于 Nginx,我们可以通过分别监听 80 和 443 端口来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 加一个监听 80 端口的 server,直接返回 301
server {
server_name _;

listen 80;
listen [::]:80;

return 301 https://$host$request_uri;
}

# 原本的 server 只接受 443 端口的请求
server {
server_name example.com;

listen 443 ssl;
listen [::]:443 ssl;

... ...
}

优点

  • 所有的内容都只能通过 HTTPS 安全传输,安全性高
  • 状态码清晰明确,对搜索引擎友好,有利于收录
  • 想要关闭强制 HTTPS 访问时可以即刻生效

缺点

  • 浏览器会向服务端请求两次:第一次发起 HTTP 请求,收到 301 重定向,第二次再发起 HTTPS 请求
  • 可能遭受中间人的降级攻击
  • 当浏览器发现当前的 HTTPS 连接不安全,例如证书过期,域名无效等问题时,浏览器会警告用户,但仍会允许用户继续这种不安全的访问,不会强行阻止

方法三:HSTS

  为了省去浏览器第一次向服务端发起的 HTTP 请求,HSTS(HTTP Strict Transport Security,HTTP严格传输安全协议)应运而生。HSTS 的功能就是:即使用户输入的 URL 是 HTTP 协议,浏览器也会直接 307 重定向到 HTTPS 协议。
  实现方法就是在服务器返回给浏览器的响应头中加入 Strict-Transport-Security 这个头。HSTS Header 的语法如下:

1
Strict-Transport-Security: <max-age=>[; includeSubDomains][; preload]

  其中,max-age 是必选参数,是一个以秒为单位的数值,代表 HSTS Header 的过期时间,通常设置为 31536000,即一年的时间。includeSubDomains 是可选参数,如果包含它,则意味着当前域名及其子域名均开启 HSTS 保护。preload 是可选参数,如果你想申请将自己的域名加入到浏览器的内置列表那么需要包含这个参数,关于这个参数我们接下来细说。
  以 Nginx 为例,要想启用 HSTS,只需要在配置文件中加入这样一行:

1
2
# 过期时间一年,保护所有子域名
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

  重启 Nginx 服务后,我们在 MySSL 上重新生成评估报告,发现可以评到最高级 A+,意味着你的网站现在是全世界最安全的网站之一(其实并没有什么卵用)。

优点

  • 你的 HSTS 信息在访客浏览器中生效期间可以有效防止中间人攻击
  • 浏览器端直接 307 重定向,省去了服务端进行重定向
  • 证书无效时浏览器完全阻断访问

缺点

  • 一旦你加上了 Strict-Transport-Security 这个头,就不能轻易取消 HTTPS 服务了,否则你的访客在他浏览器的 HSTS 信息过期之前都无法正常访问
  • 可能会在某些浏览器中造成一些奇奇怪怪的谜之错误导致你的网站不能被正常访问

关于 HSTS Preload List

  我们仔细想想就不难发现,HSTS 虽然牛逼,可以很好的解决 HTTPS 降级攻击,但是有一个致命缺陷,就是对于 HSTS 生效前的首次 HTTP 请求,依然无法避免被劫持。换句话说,当用户第一次访问网站的时候,浏览器没有当前网站的 HSTS 信息,所以就会产生一次明文的 HTTP 请求,尽管服务端可以返回 301 重定向,但在这个过程中,仍然避免不了中间人攻击。
  为了解决这个问题,Chrome 提出了 HSTS Preload List 方案,就是在浏览器里内置一份定期更新的列表,对于列表中的域名,即使用户之前没有访问过,浏览器也会强行使用 HTTPS 协议。目前,主流的浏览器(Chrome、Firefox、Opera、Safari、IE11、Edge)都支持这份由 Chrome 维护的 HSTS Preload List。你可以在它的官网 hstspreload.org 上查询到一个网站是否在这份列表之中。
  如果你想要把你的域名加入到 HSTS Preload List 中,那么你需要满足官网中列出的所有条件,然后在 Strict-Transport-Security 头里加入 preload 参数。由于这份列表是硬编码到浏览器中的,所以需要过一段时间你的网站才会被加入到稳定版本的列表中。要注意,一旦你的网站被列入了 HSTS Preload List,想要再从里面出来可就不那么容易了,提交申请后同样需要等很长一段时间才会被彻底除名,官网对此过程的描述是“slow and painful”。所以,除非你能保证你的网站能够持续稳定地提供 HTTPS 服务,否则不要轻易使用 preload 参数

  总的来说,我感觉 HSTS 虽然提供了一个很好的方向,能够很大程度上增强通信的安全性,但仍旧有许多不靠谱的地方,亟需升级改善。所以我认为对于普通网站(非金融、支付相关),完全没有必要使用 HSTS 方式加以保护,301/302 重定向就足够了。当然,强迫症患者除外😂。