全栈迁移HTTPS/HTTP2总结系列文章

全栈迁移HTTPS/HTTP2总结系列文章

第一篇 - x.xx.com全站迁移HTTPS/HTTP2总结:

x.xx.com全站迁移HTTPS总结系列文章导航:

  • (一):投放端业务迁移介绍

  • (二):业务HTTP排查和梳理

  • (三):灰度策略和错漏检测

  • (四):CSP与HSTS安全策略

  • (五):HTTPS/HTTP2与性能提升

为了给广告主提供更安全可靠的广告系统,避免一些浏览器将系统表示为不安全, 消除广告投放端被中间人劫持插入广告, 保证广告主的账户安全.

此项工作主要处理了以下任务:

  1. 系统的http调用排查和梳理:我们梳理了约10个关联产品的约20多个相关域名, 并排查出存在MixContent页面50多个
  2. x.xx.com 域名下8个服务的HTTPS改造:域名收归为三个域名收归为 x.xx.com (HTML和CGI),cdn.com (JS资源)cdn.com (CSS样式资源)
  3. HTTP2相关特性测试和支持:开启HTTP2, HTTPS下页面资源加载性能提升40%
  4. 灰度放量机制和错漏排查机制:设计灰度放量机制和开发配套的监控系统确保没有遗漏。
  5. 部署相关安全协议: Upgrade-Insecure-Requests, HSTS, CSP等

整体迁移工作划分

x.xx.com整站迁移HTTPS工作分为如下三块内容:

  1. 业务侧及各个依赖模块HTTPS改造
  2. 服务层接入HTTPS(NGINX)
  3. HTTPS灰度上线

现在将这些工作中的一些经验和工作的技术分享如下

业务侧http资源排查和梳理

在推动x.xx.com业务代码兼容HTTPS时, 需要考虑的点非常多, 把工作上的一些要点记录了一下, 并梳理了几点方法论, 希望可以提供一些参考

灰度策略和错漏检测:

在推进HTTPS灰度的阶段, 由于投放端依赖的服务众多, 项目历史包袱较重, 即便是收归了域名之后, 在项目代码中 做了全部的资源url 替换, 有部分资源url是写的数据库中的, 或者是第三方服务加载的. 这部分请求是http的, HTTPS 网页中加载的 HTTP 资源被称之为 Mixed Content(混合内容). 这种场景首先考虑的时候使用 使用 Content-Security-Policy: upgrade-insecure-requests 升级不安全的请求。

但是面临的问题是: 我们不知道这些触发MixContent的资源是否支持https, 所以前期先部署了 MixContent上报策略.
Content-Security-Policy-Report-Only: block-all-mixed-content

通过近两周的每日MixContent上报统计, 我们排查出 x.xx.com 下面所有的 MixContent, 并逐个推进排查(排查的依赖域名20+, 存在MixContent页面50+)

通过两周的排查, 并确认所有的 http资源请求都支持 https之后 , 再部署了 upgrade-insecure-requests策略. 到达这个阶段就完整的实现了灰度的用户 地址栏的绿色协议头不会因为MixContent灰掉了.

上线upgrade-insecure-requests策略之后, https页面下http的请求会自动升级为https, 如果存在某些资源不支持https, 同时我们在MixContent排查阶段没有发现, 那么这个资源就会404, 我们在x.xx.com下所有的业务都引入异常上报, 对页面资源 404的进行上报, 经过一周的统计, 没有发现有集中的某个资源404, 初步可以确定当前投放端下所有的资源都是支持https, 也回归验证了MixContent排查的效果. 异常上报的接入可以参考: badjs

HSTS&CSP安全策略:

在网站全站HTTPS后,如果用户手动敲入网站的HTTP地址,或者从其它地方点击了网站的HTTP链接,通常依赖于服务端301/302跳转才能使用HTTPS服务。而第一次的HTTP请求就有可能被劫持,导致请求无法到达服务器,从而构成HTTPS降级劫持。
HSTS的作用是强制客户端(如浏览器)使用HTTPS与服务器创建连接。
当客户端通过HTTPS发出请求时,在服务器返回的超文本传输协议(HTTP)响应头中包含Strict-Transport-Security字段。
比如,https://x.xx.com/ 的响应头含有Strict-Transport-Security: max-age=31536000; 这意味着:
在接下来的一年中,浏览器向x.xx.com发送HTTP请求时,浏览器应当自动将 http 转写成 https.

HTTP2与性能提升:

http 与http2加载性能对比
从上面两个图可以看出, HTTP模式下, 资源的请求仍然遵循Chrome对同一源最大6个TCP连接的规则, 从timeline看出HTTP下多个tcp的stalled的时间(灰色部分)总和比较长, 呈现瀑布状
在HTTP2 下, 由于tcp连接的多路复用, 可以可以看出整个stalled的时间(灰色部分)大致为一个tcp的时间.
从整个耗时来算, HTTP2下加载20个js资源耗时110ms, HTTP下耗时 180ms, 我们在升级HTTPS/HTTP2之后, 在js css等静态资源的加载上反而比之前快了40%左右.

开发测试环境全环境HTTPS化

在部署https的同时, x.xx.com也部署了 HSTS策略(HTTP严格传输安全), 而我们在开发测试环境中会经常使用HTTP请求来开发调试, 目前已经在开发测试环境预发布环境全部部署了HTTPS证书, 为了跟外网环境保持高度一致, 建议在开发测试中使用https协议. 如果遇到需要抓包的场景, 建议配置代理的HTTPS抓包支持:

如果一定需要使用http访问, 可以chrome://net-internals/#hsts 删除对应的域名的 hsts缓存即可

第二篇 - 老业务HTTP排查和梳理

在推动x.xx.com老的业务代码兼容HTTPS时, 需要考虑的点非常多, 其中重要的包括如下几点:

  1. 不能简单的做全局替换: x.xx.com整个站点下依赖的各种服务约十多个, 各种资源和域名超过五十多个, 全局替换的问题在于, 如何确认这些依赖的域名都支持HTTPS了, 貌似没有比较好的办法, 只能全局搜索并一个个梳理出来一个个推动改造, 最好是有一个check list, 详细的记录各个服务或者资源被调用的地方, 目前是否支持https, 当前推进的进度, 相关的负责人, 截止的时间点等等, 定期去review各个模块的进度
  1. 并非所有的资源或者URL都在代码库中: 比如在广告投放端中, 有很多URL是写入到DB中的, 或者CGI层只是作为一个透传, 下面还依赖好几层更底层的服务, 而且这些URL调用方并非是只有投放端, 最开始的反应是在CGI层做一层替换, 例如获取qq信息等我们可以通过修改透传数据接口, 强行修改返回的url协议头, 但是更多接口对于返回的内容是不确定的, 贸然修改 成本高, 风险也高. 针对这种情况, 我们的处理方式是: 使用Upgrade-Insecure-Requests策略, 但是需要所有的资源的域名都支持https, 那么问题来了, 如何确认这些域名都支持https? 可以参考一篇文章看看我们是如何在灰度的流程中做minxContent上报错漏检测

  2. 并非所有的依赖方能及时支持https改造, 甚至有些老的接口已经处于不在维护的状况. 建议CGI层统一对这些不支持https的服务做一层封装转发, 系统尽量只依赖CGI的接口, 减少在前端做跨域跨系统的请求

  3. 一些底层的库 还有一些项目本身依赖旧的底层库, 里面有部分异步请求加载模块, 有部分异步加载模块的URL都是固定http协议头的, 如果短时间没有办法剔除前端代码对这些模块的依赖, 只能强行拉取代码进行升级兼容. 比如 底层的上报地址需要修改// 的兼容模式, 测速上报可以修改为 (location.protocol === ‘https:’ ? ‘//report.xxx.com/cgi-bin/r.cgi’ : ‘//report.xxx.com//cgi-bin/r.cgi’) + params.join(‘&’)

  4. 开发,测试环境, 预发布环境要先支持https, 最好的方式联系 运维提供相关的证书, 部署到开发环境和测试环境, 和外网保持高度一致. 推动各环境 部署https证书也是做https改造的一个重要准备工作之一, 建议在做完这块的工作之后, 把各个环境, 各个域名的证书的负责人落地实到wiki或者邮件中去

第三篇 - 灰度策略和错漏检测:

在”(一):投放端业务迁移介绍”文章中提到过,, https全站改造第三个的步骤就是灰度上线, 那么如何设置灰度的策略,并且真正利用灰度这个流程来推动https改造.

先看一下x.xx.com灰度的流程:

  1. 开始1%的灰度放量
  2. 上线MixContent上报策略, 以及相关的统计日报表
  3. 在灰度1%-20%期间, 通过MixContent统计的来逐步排查页面中遗漏的http资源
  4. 上线资源404上报机制, 用于排查因为某些静态资源站点不支持https导致的404错误
  5. HTTPS页面部署Upgrade-Insecure-Requests策略, HTTPS页面下http请求自动升级为 HTTPS
  6. 灰度到50%, 观察两周.
  7. 全量, http请求默认重定向到https请求, 并开启HSTS策略

这里有两个关键点:

  1. 灰度期间外网同时支持https 和 http, 方便用户在https有问题的时候直接切换为http的页面
  2. 在灰度的前期, 我们允许https页面MixContent的情况出现(地址栏协议头变灰)

在推进HTTPS灰度的阶段, 由于投放端依赖的服务众多, 项目历史包袱较重, 即便是收归了域名之后, 在项目代码中 做了全部的资源url 替换, 有部分资源url是写的数据库中的, 或者是第三方服务加载的. 这部分请求是http的, HTTPS 网页中加载的 HTTP 资源被称之为 Mixed Content(混合内容). 这种场景首先考虑的时候使用 使用 Content-Security-Policy: upgrade-insecure-requests 升级不安全的请求。

upgrade-insecure-requests

首先它是一个 CSP 指令。 W3C 工作组考虑到了我们升级 HTTPS 的艰难,在 2015 年 4 月份就出了一个 Upgrade Insecure Requests 的草案,他的作用就是让浏览器自动升级当前页面内容的http请求。 对于页面内的 图片 样式 js XHR 等请求生效, 但是对于a标签跳转不会生效. 同时,当自动升级https请求失败时, 不会自动降级到http, 资源就404或者网络错误了.

所以我们面临的问题是: 不知道这些触发MixContent的资源是否支持https, 所以前期先部署了 MixContent上报策略

MixContent上报策略:

Content-Security-Policy-Report-Only: block-all-mixed-content

Content-Security-Policy-Report-Only 可以让我们只上报违反CSP策略的情况, 而不会真正的拦截这些MixContent资源
block-all-mixed-content: 这条指令在当前页面为通过 HTTPS 协议加载的情况下禁止通过 HTTP 渠道加载任何资源。任何混合类型的资源请求都是被禁止的,包括混合活动内容和混合被动内容。这一条也适用于 iframe 中的文档,确保整体页面都不包含混合内容。

upgrade-insecure-requests 指令会在 block-all-mixed-content 之前执行;如果前者执行成功,后者就不再发挥任何作用。推荐的做法是设置二者之一,而不是全部

MixContent数据统计和查询

配置MixContent上报数据之后, 需要同步上线MixContent数据统计和查询

数据统计和查询

通过近两周的每日MixContent上报统计, 我们排查出 x.xx.com 下面所有的 MixContent, 并逐个推进排查(排查的依赖域名20+, 存在MixContent页面50+)

确认所有的 http资源请求都支持 https之后 , 再部署了 upgrade-insecure-requests策略. 到达这个阶段就完整的实现了灰度的用户 地址栏的绿色协议头不会因为MixContent灰掉了

资源加载失败上报

上线upgrade-insecure-requests策略之后, https页面下http的请求会自动升级为https, 如果存在某些资源不支持https, 同时我们在MixContent排查阶段没有发现, 那么这个资源就会404, 我们在x.xx.com下所有的业务都引入异常上报, 对页面资源 404的进行上报, 经过一周的统计, 没有发现有集中的某个资源404, 初步可以确定当前投放端下所有的资源都是支持https, 也回归验证了MixContent排查的效果. 异常上报的接入可以参考 badjs

第四篇 - CSP与HSTS安全策略

在全站升级HTTPS之后, 并不意味着站点就是安全的, 本文主要从 HTTPS重定向劫持 HSTS策略, CSP策略 这三个方面讲述投放端在web页面安全这块的建设

HTTPS 重定向劫持

  1. 重定向劫持劫持 攻击
    中间人一旦发现有重定向到 HTTPS 网站的,拦下这个重定向,然后以 HTTPS 的方式,获取重定向后的内容,最后再以 HTTP 明文的方式,回复给用户。
  2. 重定向劫持之后一般会把页面中的https 请求替换为 http的, 不然第一次劫持了后面客户端依旧发起的是 https 的请求,无法劫持. 传统的方案是在中间人代理层替换 就是经典的中间人攻击工具 —— SSLStrip
  3. 新的攻击方案是在首次劫持返回的前端html 页面中插入代码, 劫持用户的https超链接, 降级为 http
    1. 页面被 ssl 劫持之后, 攻击者面对的主要问题是如果保证页面再次发起的请求是 http的,
      页面批量超链接替换, 表单提交,window.open 弹窗,框架页面都是需要进行http降级处理的,否则就报跨域错误了.

分析了攻击方法, 下面是应对的方案:

通过代码跳转, 混淆替换代码明文的 https 超链接, 部署hsts

http://blog.jobbole.com/78590/?utm_source=blog.jobbole.com&utm_medium=relatedPosts

HTST:

HTTP Strict Transport Security (HSTS)
访问网站时,用户很少直接在地址栏输入https://,总是通过点击链接,或者3xx重定向,从HTTP页面进入HTTPS页面。攻击者完全可以在用户发出HTTP请求时,劫持并篡改该请求。
另一种情况是恶意网站使用自签名证书,冒充另一个网站,这时浏览器会给出警告,但是许多用户会忽略警告继续访问。
“HTTP严格传输安全”(简称 HSTS)的作用,就是强制浏览器只能发出HTTPS请求,并阻止用户接受不安全的证书。
它在网站的响应头里面,加入一个强制性声明。以下例子摘自维基百科。

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

上面这段头信息有两个作用。
(1)在接下来的一年(即31536000秒)中,浏览器只要向example.com或其子域名发送HTTP请求时,必须采用HTTPS来发起连接。用户点击超链接或在地址栏输入http://www.example.com/,浏览器应当自动将http转写成https,然后直接向https://www.example.com/发送请求。
(2)在接下来的一年中,如果example.com服务器发送的证书无效,用户不能忽略浏览器警告,将无法继续访问该网站。
HSTS 很大程度上解决了 SSL 剥离攻击。只要浏览器曾经与服务器建立过一次安全连接,之后浏览器会强制使用HTTPS,即使链接被换成了HTTP。
该方法的主要不足是,用户首次访问网站发出HTTP请求时,是不受HSTS保护

https 安全问题不仅仅是后端只监听一个443端口就能解决的的事情, 更有前端的页面降级ssl 劫持, 重定向劫持等安全问题需要我们去关注

开发测试环境全环境HTTPS化

在部署https的同时, x.xx.com也部署了 HSTS策略(HTTP严格传输安全), 而我们在开发测试环境中会经常使用HTTP请求来开发调试, 目前已经在开发测试环境预发布环境全部部署了HTTPS证书, 为了跟外网环境保持高度一致, 建议在开发测试中使用https协议. 如果遇到需要抓包的场景, 建议配置代理的HTTPS抓包支持:

如果一定需要使用http访问, 可以chrome://net-internals/#hsts 删除对应的域名的 hsts缓存即可

CSP

在x.xx.com 全站迁移HTTPS过程中, 主要使用的CSP规则是 upgrade-insecure-requests 和 block-all-mixed-content, 作为在灰度升级阶段的一个辅助工具. 而在全站https之前, 我们是如何保证投放端的页面安全呢? 使用主要还是CSP规则, 同时配合投放端自研的监控上报系统进行数据展示(MixContent上报也是基于这个系统)
csp_report
即使在升级HTTPS之后, 在页面安全这块依旧不能掉以轻心

第吴篇 - HTTPS/HTTP2与性能提升

升级后的性能对比

http 与http2加载性能对比
从上面两个图可以看出, HTTP模式下, 资源的请求仍然遵循Chrome对同一源最大6个TCP连接的规则, 从timeline看出HTTP下多个tcp的stalled的时间(灰色部分)总和比较长, 呈现瀑布状
在HTTP2 下, 由于tcp连接的多路复用, 可以可以看出整个stalled的时间(灰色部分)大致为一个tcp的时间.
从真的耗时来算, HTTP2下加载20个js资源耗时110ms, HTTP下耗时 180ms, 我们在升级HTTPS之后, 在js css等静态资源的加载上反而比之前快了40%左右.

后面继续探究性能提升背后的原理.

一个HTTPS请求为例,简单描述下接入转发流程:

用户发起HTTPS请求,首先到达网关入口。
网关入口将该请求转发给https网关入口。由于网关入口只是四层的转发,所以无法识别HTTPS请求内容。
https网关入口会对HTTPS请求进行卸载,也就是完成SSL握手及应用层内容的解密,将HTTPS转换成HTTP协议,再按照分流配置转发给业务侧RS。
业务侧收到HTTP请求后,生产响应内容并回复给https网关入口。
https网关入口将业务的响应内容进行加密,回复给网关入口。
网关入口再返回给用户。

了解过一个https请求的链路之后, 我们将从 TLS 和 HTTP/2 这两方方向分析性能提升背后的原理

TLS 优化

耗时:

1
2
curl -w "TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\n" -so /dev/null https://x.xx.com
TCP handshake: 0.012235, SSL handshake: 0.044042

网关 的对其 tsl 握手的优化

网关针对HTTPS进行了全方位的优化。包括:

非对称加密—-异步代理计算
算法分离。
代理计算
异步执行。
网络协议栈全链路优化
TCP。包括拥塞窗口的调整,tcp fast open的支持,reuseport的支持。
SSL,分布式session cache, session ticket,False start, ocsp stapling file,动态record size等。
应用层。同时支持SPDY,HTTP2.
前后端优化
域名收归。通过页面资源及性能分析,确实域名收归方案,比如移动页面不超过3个
预建连接。https网关入口提供预连接页面,通过对热点页面的用户行为进行分析,提前建立连接,减少协议开销对用户体验的影响。
通过CDN及多地IDC就近完成HTTPS卸载。
session identifier

Session Identifier(会话标识符),是 TLS 握手中生成的 Session ID。服务端可以将 Session ID 协商后的信息存起来,浏览器也可以保存 Session ID,并在后续的 ClientHello 握手中带上它,如果服务端能找到与之匹配的信息,就可以完成一次快速握手。

Session Ticket

Session Identifier 机制有一些弊端,负载均衡中,多机之间往往没有同步 Session 信息,如果客户端两次请求没有落在同一台机器上就无法找到匹配的信息;2)服务端存储 Session ID 对应的信息不好控制失效时间,太短起不到作用,太长又占用服务端大量资源。
而 Session Ticket(会话记录单)可以解决这些问题,Session Ticket 是用只有服务端知道的安全密钥加密过的会话信息,最终保存在浏览器端。浏览器如果在 ClientHello 时带上了 Session Ticket,只要服务器能成功解密就可以完成快速握手。
部署的需要注意: 例如在 Nginx 中,就需要通过 ssl_session_ticket_key 参数让多台机器使用相同的 key 文件

HTTP/2

相比 HTTP/1.x,HTTP/2 在底层传输做了很大的改动和优化:

  1. HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式。二进制格式在协议的解析和优化扩展上带来更多的优势和可能。
  2. HTTP/2 对消息头采用 HPACK 进行压缩传输,能够节省消息头占用的网络的流量。而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。头压缩能够很好的解决该问题。
  3. 多路复用,直白的说就是所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。
  4. Server Push:服务端能够更快的把资源推送给客户端。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。当客户端需要的时候,它已经在客户端了。

本次x.xx.com迁移HTTPS 并且升级到HTTP2主要使用到的特性是 TCP的多路复用, 在收归静态资源域名之后, 减少TCP握手环节的耗时, 也减少了TSL握手的耗时. 对静态资源的并行加载性能提升非常明显

文末的HTTP2.0总结文档中详细的对比了HTTP/2 与 HTTP1.X, 对HTTP2关键的四个特性做了原理上的分析.
另外关于传输层安全也有一篇翻译文档专门介绍 传输层安全性(Transport Layer Security,TLS)-译