彻底搞懂HTTP和HTTPS协议

探究http和https的发展史

Posted by Li Yucang on January 2, 2019

彻底搞懂 HTTP 和 HTTPS 协议

HTTP 发展历史

http 协议(超文本传输协议),是一个基于请求与响应,无状态的,应用层的协议,常基于 TCP/IP 协议传输数据,互联网上应用最为广泛的一种网络协议,所有的 WWW 文件都必须遵守这个标准。设计 HTTP 的初衷是为了提供一种发布和接收 HTML 页面的方法。

设计 HTTP 最初的目的是为了将超文本标记语言(HTML)文档从 Web 服务器传送到客户端的浏览器。也是说对于前端来说,我们所写的 HTML 页面将要放在我们的 web 服务器上,用户端通过浏览器访问 url 地址来获取网页的显示内容;

随着 web2.0 的到来,我们的网站从单纯的 html 网页模式向内容更丰富、更加注重交互并以用户为中心的应用创新,出现了如 blog,sns 等产品,同时我们的 HTML 页面有了 CSS,Javascript,随着 Ajax 的出现和大量使用,更是提升了我们对于网站交互的体验,以上这些其实都是基于 HTTP 协议的;

同样到了移动互联网时代,我们页面可以跑在手机端浏览器里面,但是和 PC 相比,手机端的网络情况更加复杂,这使得我们开始了不得不对 HTTP 进行深入理解并不断优化过程中。

http 协议发展历史:

版本 产生时间 内容 发展现状
HTTP/0.9 1991 年 不涉及数据包传输,规定客户端和服务器之间通信格式,只能 GET 请求 没有作为正式的标准
HTTP/1.0 1996 年 传输内容格式不限制,增加 PUT、PATCH、HEAD、 OPTIONS、DELETE 命令 正式作为标准
HTTP/1.1 1997 年 持久连接(长连接)、节约带宽、HOST 域、管道机制、分块传输编码 2015 年前使用最广泛
HTTP/2 2015 年 多路复用、服务器推送、头信息压缩、二进制协议等 逐渐覆盖市场

HTTP 0.9

HTTP 是基于 TCP/IP 协议的应用层协议,最早版本是 1991 年发布的 0.9 版。该版本极其简单:

  1. 只接受 GET 一种请求方法,且不支持请求头。
  2. 协议规定,服务器只能回应 HTML 格式的字符串,不能回应别的格式。
  3. 由于该版本不支持 POST 方法,所以客户端无法向服务器传递太多信息。
客户端请求格式
GET /index.html

服务器响应格式
<html>
  <body>Hello World</body>
</html>

HTTP1.0

HTTP1.0 最早在网页中使用是在 1996 年,那个时候只是使用一些较为简单的网页上和网络请求上。这是第一个在通讯中指定版本号的 HTTP 协议版本,至今仍被广泛采用,特别是在代理服务器中。

  1. 首先,任何格式的内容都可以发送。这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。这为互联网的大发展奠定了基础。

  2. 其次,除了 GET 命令,还引入了 POST 命令和 HEAD 命令,丰富了浏览器与服务器的互动手段。

  3. 再次,HTTP 请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。

  4. 其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。

客户端请求格式
GET /index.html HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*

服务器响应格式
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

<html>
  <body>Hello World</body>
</html>

回应的格式是”头信息 + 一个空行(\r\n) + 数据”。其中,第一行是”协议版本 + 状态码(status code) + 状态描述”。

Content-Type 字段

关于字符的编码,1.0 版规定,头信息必须是 ASCII 码,后面的数据可以是任何格式。因此,服务器回应的时候,必须告诉客户端,数据是什么格式,这就是 Content-Type 字段的作用。

下面是一些常见的 Content-Type 字段的值:

text/plain
text/html
text/css
image/jpeg
image/png
image/svg+xml
audio/mp4
video/mp4
application/javascript
application/pdf
application/zip
application/atom+xml

这些数据类型总称为 MIME type,每个值包括一级类型和二级类型,之间用斜杠分隔。

除了预定义的类型,厂商也可以自定义类型。

application/vnd.debian.binary-package

上面的类型表明,发送的是 Debian 系统的二进制数据包。

MIME type 还可以在尾部使用分号,添加参数。

Content-Type: text/html; charset=utf-8

上面的类型表明,发送的是网页,而且编码是 UTF-8。

客户端请求的时候,可以使用 Accept 字段声明自己可以接受哪些数据格式。

Accept: */*

上面代码中,客户端声明自己可以接受任何格式的数据。

MIME type 不仅用在 HTTP 协议,还可以用在其他地方,比如 HTML 网页。

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />等同于 <meta charset="utf-8" />

Content-Encoding 字段

由于发送的数据可以是任何格式,因此可以把数据压缩后再发送。Content-Encoding 字段说明数据的压缩方法。

Content-Encoding: gzip
Content-Encoding: compress
Content-Encoding: deflate

客户端在请求时,用 Accept-Encoding 字段说明自己可以接受哪些压缩方法。

Accept-Encoding: gzip, deflate

HTTP1.1

1997 年 1 月,HTTP/1.1 版本发布,只比 1.0 版本晚了半年。它进一步完善了 HTTP 协议,一直用到了 20 年后的今天,直到现在还是最流行的版本。 持久连接被默认采用,并能很好地配合代理服务器工作。还支持以管道方式同时发送多个请求,以便降低线路负载,提高传输速度。

持久连接

1.1 版的最大变化,就是引入了持久连接(persistent connection),即 TCP 连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive

客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送 Connection: close,明确要求服务器关闭 TCP 连接。目前,对于同一个域名,大多数浏览器允许同时建立 6 个持久连接。

管道机制

1.1 版还引入了管道机制(pipelining),即在同一个 TCP 连接里面,客户端可以同时发送多个请求。这样就进一步改进了 HTTP 协议的效率。

举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。管道机制则是允许浏览器同时发出 A 请求和 B 请求,但是服务器还是按照顺序,先回应 A 请求,完成后再回应 B 请求。

Content-Length 字段

一个 TCP 连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的。这就是 Content-length 字段的作用,用于声明本次回应的数据长度。

Content-Length: 3495

上面代码告诉浏览器,本次回应的长度是 3495 个字节,后面的字节就属于下一个回应了。

在 1.0 版中,Content-Length 字段不是必需的,因为浏览器发现服务器关闭了 TCP 连接,就表明收到的数据包已经全了。

分块传输编码

使用 Content-Length 字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。

对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用”流模式”(stream)取代”缓存模式”(buffer)。因此,1.1 版规定可以不使用 Content-Length 字段,而使用”分块传输编码”(chunked transfer encoding)。只要请求或回应的头信息有Transfer-Encoding字段,就表明回应将由数量未定的数据块组成。

Transfer-Encoding: chunked

每个非空的数据块之前,会有一个 16 进制的数值,表示这个块的长度。最后是一个大小为 0 的块,就表示本次回应的数据发送完了。下面是一个例子。

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

25
This is the data in the first chunk

1C
and this is the second one

3
con

8
sequence

0

缓存处理

在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。

Range 和 Content-Range 字段

HTTP1.1 则在请求头引入了 Range 和 Content-Range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接,把一个大的文件切割成小的文件块传输,产生了我们所谓的断点续传功能;

Range 用于请求头中,指定第一个字节的位置和最后一个字节的位置,一般格式:

Range:(unit=first byte pos)-[last byte pos]

Content-Range用于响应头,指定整个实体中的一部分的插入位置,他也指示了整个实体的长度。在服务器向客户返回一个部分响应,它必须描述响应覆盖的范围和整个实体长度。一般格式:

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]

错误通知的管理

在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

增加了多种请求方法

1.1 版还新增了许多动词方法:PUT、PATCH、HEAD、 OPTIONS、DELETE

方法 说明 支持的 http 协议版本
GET 获取资源 1.0、1.1
POST 传输实体主体 1.0、1.1
PUT 传输文件 1.0、1.1
HEAD 获取报文首部 1.0、1.1
DElETE 删除文件 1.0、1.1
OPTIONS 询问支持的方法 1.1
TRACE 追踪路径 1.1
CONNECT 要求用隧道协议连接代理 1.1
LINK 建立和资源的连接 1.0
UNLINK 断开连接关系 1.0

Host 头处理

客户端请求的头信息新增了 Host 字段,用来指定服务器的域名。

Host: www.example.com

在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。HTTP1.1 的请求消息和响应消息都应支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)。

HTTP/1.x 的缺陷

连接无法复用:连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对大量小文件请求影响较大(没有达到最大窗口请求就被终止)。

  • HTTP/1.0 传输数据时,每次都需要重新建立连接,增加延迟。

  • HTTP/1.1 虽然加入 keep-alive 可以复用一部分连接,但域名分片等情况下仍然需要建立多个 connection,耗费资源,给服务器带来性能压力。

Head-Of-Line Blocking(HOLB):导致带宽无法被充分利用,以及后续健康请求被阻塞。HOLB 是指一系列包(package)因为第一个包被阻塞;当页面中需要请求很多资源的时候,HOLB(队头阻塞)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

  • HTTP 1.0:下个请求必须在前一个请求返回后才能发出,request-response 对按序发生。显然,如果某个请求长时间没有返回,那么接下来的请求就全部阻塞了。

  • HTTP 1.1:尝试使用 pipeling 来解决,即浏览器可以一次性发出多个请求(同个域名,同一条 TCP 链接)。但 pipeling 要求返回是按序的,那么前一个请求如果很耗时(比如处理大图片),那么后面的请求即使服务器已经处理完,仍会等待前面的请求处理完才开始按序返回。所以,pipeling 只部分解决了 HOLB

如上图所示,红色圈出来的请求就因域名链接数已超过限制,而被挂起等待了一段时间。

协议开销大: HTTP1.x 在使用时,header 里携带的内容过大,在一定程度上增加了传输的成本,并且每次请求 header 基本不怎么变化,尤其在移动端增加用户流量。

为了避免这个问题,只有两种方法:一是减少请求数,二是同时多开持久连接。这导致了很多的网页优化技巧,比如合并脚本和样式表、将图片嵌入 CSS 代码、引入雪碧图、域名分片(domain sharding)等等。如果 HTTP 协议设计得更好一些,这些额外的工作是可以避免的。

SPDY 协议

2009 年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。

SPDY(读作“SPeeDY”)是 Google 开发的基于 TCP 的应用层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。SPDY 并不是一种用于替代 HTTP 的协议,而是对 HTTP 协议的增强。新协议的功能包括数据流的多路复用、请求优先级以及 HTTP 报头压缩。

互联网工程任务组(IETF)对谷歌提出的 SPDY 协议进行了标准化,于 2015 年 5 推出了类似于 SPDY 协议的 HTTP 2.0 协议标准(简称 HTTP/2)。谷歌因此宣布放弃对 SPDY 协议的支持,转而支持 HTTP/2。此外,著名的开源 HTTP 服务器软件 Nginx 也于 2015 年 9 月移除了对 SPDY 的支持,转而支持 HTTP/2。因此,建议新的网站不要部署 SPDY,转为部署 HTTP/2。

所以,从历史上来看,SPDY 协议只是 HTTP/2 的基础,其主要特性都在 HTTP/2 之中得到继承。鉴于此,我们不必要过多了解这个协议的具体内容了。

HTTP/2

2015 年,HTTP/2 发布。HTTP/2 是现行 HTTP 协议(HTTP/1.x)的替代,但它不是重写,HTTP 方法/状态码/语义都与 HTTP/1.x 一样。HTTP/2 基于 SPDY3,专注于性能,最大的一个目标是在用户和网站间只用一个连接(connection)。

HTTP/2 由两个规范(Specification)组成:

  1. Hypertext Transfer Protocol version 2 - RFC7540

  2. HPACK - Header Compression for HTTP/2 - RFC7541

二进制传输

HTTP/1.1 版的头信息肯定是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 采用二进制格式传输数据,二进制协议解析起来更高效。

HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。

二进制协议的一个好处是,可以定义额外的帧。HTTP/2 定义了近十种帧,为将来的高级应用打好了基础。如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便得多。

既然又要保证 HTTP 的各种动词,方法,首部都不受影响,那就需要在应用层(HTTP2.0)和传输层(TCP or UDP)之间增加一个二进制分帧层。

在二进制分帧层上, HTTP/2 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中 HTTP1.x 的首部信息会被封装到 Headers 帧,而我们的 request body 则封装到 Data 帧里面。

接下来我们介绍几个重要的概念:

  • 流:流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2…N);

  • 消息:是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成;

  • 帧:HTTP 2.0 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流,承载着特定类型的数据,如 HTTP 首部、负荷等等。

HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。

头信息压缩

HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

在 HTTP/1 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。

HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression):

  1. 头信息使用 gzip 或 compress 压缩后再发送;

  2. HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;

  3. 首部表在 HTTP/2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新;

  4. 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。

事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。

如果首部发生变化了,那么只需要发送变化了数据在 Headers 帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。

例如下图中的两个请求,请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。

多路复用

上面我们说过了 HTTP1.1 协议的”队头堵塞”问题,在 HTTP1.1 的协议中,我们传输的 request 和 response 都是基本于文本的,这样就会引发一个问题:所有的数据必须按顺序传输,比如需要传输:hello world,只能从 h 到 d 一个一个的传输,不能并行传输,因为接收端并不知道这些字符的顺序,所以并行传输在 HTTP1.1 是不能实现的。

在 HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。

大家可以通过该图直观感受下 HTTP/2 比 HTTP/1 到底快了多少。

在 HTTP/2 中,有了二进制分帧之后,HTTP /2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2 中:

  • 同域名下所有通信都在单个连接上完成;

  • 单个连接可以承载任意数量的双向数据流;

  • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。

这一特性,使性能有了极大提升:

  • 同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应,消除了因多个 TCP 连接而带来的延时和内存消耗;

  • 并行交错地发送多个请求,请求之间互不影响;

  • 并行交错地发送多个响应,响应之间互不干扰;

  • 在 HTTP/2 中,每个请求都可以带一个 31bit 的优先值,0 表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。

如上图所示,多路复用的技术可以只通过一个 TCP 连接就可以传输所有的请求数据。

HTTP/2 协议中,我们可以复用 TCP 连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了”队头堵塞”。

举例来说,在一个 TCP 连接里面,服务器同时收到了 A 请求和 B 请求,于是先回应 A 请求,结果发现处理过程非常耗时,于是就发送 A 请求已经处理好的部分, 接着回应 B 请求,完成后,再发送 A 请求剩下的部分。

由于多路复用,HTTP/2 协议比 HTTP1.1 协议省去了 DNS Lookup,Initial connection,SSL 这些建立连接的步骤,大大提升了网站响应时间。

数据流 Stream

因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。

HTTP/2 将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流 ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID 一律为奇数,服务器发出的,ID 为偶数。

数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM 帧),取消这个数据流。1.1 版取消数据流的唯一方法,就是关闭 TCP 连接。这就是说,HTTP/2 可以取消某一次请求,同时保证 TCP 连接还打开着,可以被其他请求使用。

客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。

服务器推送

Server Push 即服务端能通过 push 的方式将客户端需要的内容预先推送过去,也叫“cache push”。

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch。

例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 时再发送这些请求。

服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。主动推送也遵守同源策略,换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。

HTTP/3

虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。

上文提到 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。但当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。

因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。但是对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。

那么可能就会有人考虑到去修改 TCP 协议,其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。

基于这个原因,Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上,HTTP/3 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。

RTT

网络请求中一个常见的名词是 RTT(Round Trip Time),表示客户端从发出一个请求数据,到接收到响应数据之间间隔的时间。

RTT 可以理解成由两部分组成,一部分受到物理条件的限制,比如间隔距离除以信号传递速度,以及包大小除以带宽。另一部分则是客户端、服务器以及沿途各路由器对包的处理解析时间。一般情况下,RTT 大约在几十毫秒左右,网络很好的情况下可以达到个位数,恶劣网络环境下达到几百毫秒也有可能。

RTT 是网络请求耗时中不可忽略的一部分,不仅仅是握手阶段需要三个 RTT,在实际网络请求中,还有可能因为丢包等问题而额外增加 RTT。因此任何一个能减少 RTT 的技术都值得认真考虑,因为他们真的能够显著降低网络请求耗时。

多路复用,避免队头阻塞

这句话说起来很容易,但理解起来并不那么显然,要想理解 QUIC 协议到底做了什么以及这么做的必要性,我想还是从最基础的 HTTP 1.0 聊起比较合适。

Pipiline

根据谷歌的调查, 现在请求一个网页,平均涉及到 80 个资源,30 多个域名。考虑最原始的情况,每请求一个资源都需要建立一次 TCP 请求,显然不可接受。HTTP 协议规定了一个字段 Connection,不过默认的值是 close,也就是不开启。

早在 1999 年提出的 HTTP 1.1 协议 中就把 Connection 的默认值改成了 Keep-Alive,这样同一个域名下的多个 HTTP 请求就可以复用同一个 TCP 连接。这种做法被称为 HTTP Pipeline,优点是显著的减少了建立连接的次数,也就是大幅度减少了 RTT。以上面的数据为例,如果 80 个资源都要走一次 HTTP 1.0,那么需要建立 80 个 TCP 连接,握手 80 次,也就是 80 个 RTT。如果采用了 HTTP 1.1 的 Pipeline,只需要建立 30 个 TCP 连接,也就是 30 个 RTT,提高了 62.5% 的效率。

Pipeline 解决了 TCP 连接浪费的问题,但它自己还存在一些不足之处,也就是所有管道模型都难以避免的队头阻塞问题。

队头阻塞

我们再举个简单而且直观的例子,假设加载一个 HTML 一共要请求 10 个资源,那么请求的总时间是每一个资源请求时间的总和。最直观的体验就是,网速越快请求时间越短。然而如果某一个资源的请求被阻塞了(比如 SQL 语句执行非常慢)。但对于客户端来说所有后续的请求都会因此而被阻塞。

队头阻塞(Head of line blocking,下文简称 HOC)说的是当有多个串行请求执行时,如果第一个请求不执行完,后续的请求也无法执行。比如上图中,如果第四个资源的传输花了很久,后面的资源都得等着,平白浪费了很多时间,带宽资源没有得到充分利用。

因此,HTTP 协议允许客户端发起多个并行请求,比如在笔者的机器上最多支持六个并发请求。并发请求主要是用于解决 HOC 问题,当有三个并发请求时,情况会变成这样:

可见虽然第四个资源的请求被阻塞了,但是其他的资源请求并不一定会被阻塞,这样总的来说网络的平均利用率得到了提升。

支持并发请求是解决解决 HOC 问题的一种方案,这句话没有错,但是我们要理解到: “并发请求并非是直接解决了 HOC 的问题,而是尽可能减少 HOC 造成的影响”,以上图为例,HOC 的问题依然存在,只是不会太浪费带宽而已。有读者可能会好奇,为什么不多搞几个并发的 HTTP 请求呢?刚刚说过笔者的电脑最多支持 6 个并发请求,谷歌曾经做过实验,把 6 改成 10,然后尝试访问了三千多个网页,发现平均访问时间竟然还增加了 5% 左右。这是因为一次请求涉及的域名有限,再多的并发 HTTP 请求并不能显著提高带宽利用率,反而会消耗性能。

SPDY

有没有办法解决队头阻塞呢,答案是肯定的。SPDY 协议的做法很值得借鉴,它采用了多路复用(Multiplexing) 技术,允许多个 HTTP 请求共享同一个 TCP 连接。我们假设每个资源被分为多个包传递,在 HTTP 1.1 中只有前面一个资源的所有数据包传输完毕后后面资源的包才能开始传递(HOC 问题),而 SPDY 并不这么要求,大家可以一起传输。

这么做的代价是数据会略微有一些冗余,每一个资源的数据包都要带上标记,用来指明自己属于哪个资源,这样客户端最后才能把他们正确的拼接起来。不同的标记可以理解为图中不同的颜色,每一个小方格可以理解为资源的某一个包。

TCP 窗口

是不是觉得 SPDY 的多路复用已经够厉害了,解决了队头阻塞问题?很遗憾的是,并没有,而且我可以很肯定的说,只要你还在用 TCP 链接,HOC 就是逃不掉的噩梦,不信我们来看看 TCP 的实现细节。

我们知道 TCP 协议会保证数据的可达性,如果发生了丢包或者错包,数据就会被重传。于是问题来了,如果一个包丢了,那么后面的包就得停下来等这个包重新传输,也就是发生了队头阻塞。当然 TCP 协议的设计者们也不傻,他们发明了滑动窗口的概念:

这样的好处是在第一个数据包(1-1000) 发出后,不必等到 ACK 返回就可以立刻发送第二个数据包。可以看出图中的 TCP 窗口大小是 4,所以第四个包发送后就会开始等待,直到第一个包的 ACK 返回。这样窗口可以向后滑动一位,第五个包被发送。

如果第一、二、三个的包都丢失了也没有关系,当发送方收到第四个包时,它可以确信一定是前三个 ACK 丢了而不是数据包丢了,否则不会收到 4001 的 ACK,所以发送方可以大胆的把窗口向后滑动四位。

滑动窗口的概念大幅度提高了 TCP 传输数据时抗干扰的能力,一般丢失一两个 ACK 根本没关系。但如果是发送的包丢失,或者出错,窗口就无法向前滑动,出现了队头阻塞的现象

QUIC 多路复用与纠错

所以说 HOC 不仅仅在 HTTP 层存在,在 TCP 层也存在,这也正是 QUIC 协议要解决的问题。回顾 SPDY 是如何解决 HOC 的,没错,多路复用(Multiplex)。QUIC 协议也采用了多路复用技术。

QUIC 协议基于 UDP 实现,我们知道 UDP 协议只负责发送数据,并不保证数据可达性。这一方面为 QUIC 的多路复用提供了基础,另一方面也要求 QUIC 协议自己保证数据可达性。

SPDY 为各个数据包做好标记,指明他们属于哪个 HTTP 请求,至于这些包能不能到达客户端,SPDY 并不关心,因为数据可达性由 TCP 协议保证。既然客户端一定能收到包,那就只要排序、拼接就行了。QUIC 协议采用了多路复用度思想,但同时还得自己保证数据的可达性。

TCP 协议的丢包重传并不是一个好想法,因为一旦有了前后顺序,队头阻塞问题将不可避免。而无序的数据发送给接受者以后,如何保证不丢包,不错包呢?这看起来是个不可能完成的任务,不过如果把要求降低成:“最多丢一个包,或者错一个包”,事情就简单多了,操作系统中有一种存储方式叫 RAID 5,采用的是异或运算加上数据冗余的方式来保证前向纠错(FEC: Forward Error Correcting)。

我们知道异或运算的规则是,0 ^ 1 = 1、1 ^ 1 = 0,也就是相同数字异或成 1,不同数字异或成 0。对两个数字做异或运算,其实就是将他们转成二进制后按位做异或,因此对于任何数字 a,都有:

a ^ a = 0
a ^ 0 = a

同时很容易证明异或运算满足交换律和结合律,我们假设有下面这个等式:

A1 ^ A2 ^ A3 ^ ... ^ An = T

如果想让等式的左边只留下一个一个元素,只要在等号两边做 n-1 次异或就可以了:

(A1 ^ A1) ^ A2 ^ A3 ^ ... ^ An = T ^ A1
// 所以
A2 ^ A3 ^ ... ^ An = T ^ A1
// 所以
A3 ^ ... ^ An = T ^ A1 ^ A2
// 所以 ......
Ai = T ^ A1 ^ A2 ^ ... Ai-1 ^ Ai+1 ^ Ai+2 ^ ... ^ An

换句话说,A1 到 An 和 T 这总共 n+1 个元素中,不管是任何一个元素缺失,都可以从另外 n 个元素推导出来。如果把 A1、A2 一直到 An 想象成要发送的数据,T 想象成冗余数据,那么除了丢包重传,我们还可以采用冗余数据包的形式来保证数据准确性。

举个例子,假设有 5 个数据包要发送,我可以额外发送一个包(上面例子中的 T),它的值是前五个包的异或结果。这样不管是前五个包中丢失了任何一个,或者某个包数据有错(可以当成丢包来处理),都可以用另外四个包和这个冗余的包 T 进行异或运算,从而恢复出来。

当然要注意的是,这种方案仅仅在只发生一个错包或丢包时有效,如果丢失两个包就无能为力了(这也就是为什么只发一个冗余包就够的原因)。因此数据包和冗余包之间的比值需要精心设计,如果比值过高,很容易出现丢两个包的情况,如果比值过低,又会导致冗余度太高,需要设计者根据概率计算结果进行权衡。

利用冗余数据的思想,QUIC 协议基本上避免了重发数据的情况,这种利用已有数据就能进行错误恢复的技术叫做前向恢复(FEC: Fowrard Error Correcting)。当然 QUIC 协议还是支持重传的,比如某些非常重要的数据或者丢失两个包的情况。

更少的 RTT

我们考虑一次 HTTPS 请求,它的基本流程是三次 TCP 握手外加四次 SSL/TLS 握手,从图中可以看到这需要三个 RTT:

对于 HTTP 2.0 来说,本来需要一个额外的 RTT 来进行协商,判断客户端与服务器是不是都支持 HTTP 2.0,不过好在它可以和 SSL 握手的请求合并。这也就是为什么大多数主流浏览器(比如 Chrome、Firefox) 仅支持 HTTPS 2.0 而不单独支持 HTTP 2.0 的原因,毕竟 HTTP 2.0 需要一个额外的 RTT,HTTPS 2.0 需要两个额外的 RTT,仅仅是增加一个 RTT 就能获得数据安全性,还是很划算的。

SSL 握手优化

这里我们简单复习一下 SSL 握手的大致流程:

  1. 客户端发送第一个握手,包含一个随机数,以及对协议的支持情况(版本、加密方法、压缩方法等)
  2. 服务器返回证书,以及服务端生成随机数
  3. 客户端校验证书,生成一个新的随机数,用证书中的公钥加密后发给服务端
  4. 服务端确认消息,双方根据上述三个随机数生成后续会话的公钥

由于需要确认证书,生成多个随机数来保证安全,握手阶段的两个 RTT 很难节省。不过之前我们见过 HTTP 的 Pipeline 技术可以复用 TCP 连接,那么按照类似的思想,SSL 连接也可以被恢复。思考一下为什么 SSL 要设计这么复杂的握手机制,它本质上是为了保证对称秘钥的安全传输,所以 SSL 会话恢复主要考虑的也是如何恢复对称秘钥。

一个常用的方案是采用 Session Ticket,实现起来很容易: 一旦 SSL 会话建立起来,服务端把会话的基本信息,比如对称秘钥、加密方法等信息加密后发给客户端,客户端可以缓存下来这个 Session Ticket。需要恢复 SSL 会话时直接把它发回给服务端校验即可,这样可以在 SSL 层减少一个 RTT。

TCP 快速打开

聊完了 SSL 层,下面说说 TCP 的优化方案。我们都知道 TCP 的三次握手需要花费一个 RTT,有没有可能做到 0-RTT 呢?比如我们在握手的时候就带上要传递的数据。

实际上 TCP 协议已经规定了这种情况的处理方式,即客户端可以在发送第一个 SYN 握手包时携带数据,但是 TCP 协议的实现者绝对不允许(原文: MUST NOT) 把这个数据包上传给应用层。这主要是为了防止 TCP 泛洪攻击。

TCP 泛洪攻击是指攻击者利用多台机器发送 SYN 请求从而耗尽服务器的 backlog 队列,backlog 队列维护的是那些接受了 SYN 请求但还没有正式开始会话的连接。这样做的好处是服务器不会过早的分配端口、建立连接。RFC 4987 详细的描述了各种防止 TCP 泛洪攻击的方法,包括尽早释放 SYN,增加队列长度等等。

如果 SYN 握手的包能被传输到应用层,那么现有的防护措施都无法防御泛洪攻击,而且服务端也会因为这些攻击而耗尽内存和 CPU。所以人们设计了 TFO (TCP Fast Open),这是对 TCP 的拓展,不仅可以在发送 SYN 时携带数据,还可以保证安全性。

TFO 设计了一个 cookie,它在第一次握手时由 server 生成,cookie 主要是用来标识客户端的身份,以及保存上次会话的配置信息。因此在后续重新建立 TCP 连接时,客户端会携带 SYN + Cookie + 请求数据,然后不等 ACK 返回就直接开始发送数据。

服务端收到 SYN 后会验证 cookie 是否有效,如果无效则会退回到三次握手的步骤,如下图所示:

同时,为了安全起见,服务端为每个端口记录了一个值 PendingFastOpenRequests,用来表示有多少请求利用了 TFO,如果超过预设上限就不再接受。

关于 TFO 的优化,可以总结出三点内容:

  1. TFO 设计的 cookie 思想和 SSL 恢复握手时的 Session Ticket 很像,都是由服务端生成一段 cookie 交给客户端保存,从而避免后续的握手,有利于快速恢复。

  2. 第一次请求绝对不会触发 TFO,因为服务器会在接收到 SYN 请求后把 cookie 和 ACK 一起返回。后续客户端如果要重新连接,才有可能使用这个 cookie 进行 TFO

  3. TFO 并不考虑在 TCP 层过滤重复请求,以前也有类似的提案想要做过滤,但因为无法保证安全性而被拒绝。所以 TFO 仅仅是避免了泛洪攻击(类似于 backlog),但客户端接收到的,和 SYN 包一起发来的数据,依然有可能重复。不过也只有可能是 SYN 数据重复,所以 TFO 并不处理这种情况,要求服务端程序自行解决。这也就是说,不仅仅要操作系统的支持,更要求应用程序(比如 MySQL) 也支持 TFO。

0-RTT

TFO 使得 TCP 协议有可能变成 0-RTT,核心思想和 Session Ticket 的概念类似: 将当前会话的上下文缓存在客户端。如果以后需要恢复对话,只需要将缓存发给服务器校验,而不必花费一个 RTT 去等待。

结合 TFO 和 Session Ticket 技术,一个本来需要花费 3 个 RTT 才能完成的请求可以被优化到一个 RTT。如果使用 QUIC 协议,我们甚至可以更进一步,将 Session Ticket 也放到 TFO 中一起发送,这样就实现了 0-RTT 的对话恢复。

Why QUIC

从以上分析可以发现,HTTP2 和 SSL 可以说已经进行了大量的优化,可以提升的空间非常小。而 TCP 存在诸多不足之处,一方面它设计较早,而且主要目的是设计一种通用、可靠的传输协议,并非专门为网页或者 App 而设计,另一方面对 TCP 的改进要比对 SSL 和 HTTP 的改进麻烦的多,因为 TCP 是由各个操作系统实现,就以 TFO 为例吧,它在新版本的 Linux 内核中被实现,但想等到它普及开来就不知道要到猴年马月了,有兴趣的读者可以参考参考现在 Windows XP 系统的市场占有率。

反观 HTTP 和 SSL,虽然早期 HTTP 1.0 的问题更多,但是经过 1.1、SPDY、2.0 等版本的更迭,已经非常优秀了。其中的根本原因还是在于 HTTP 和 SSL 位于应用层,优化升级比较容易实现,所以经过长年累月的优化升级,现在大部分瓶颈都集中于 TCP 层。但 TCP 不仅优化点较多,而且还不容易更新。那么能不能在传输层搞一个和 TCP、UDP 类似的协议呢?答案也是否定的,其实曾经有一个 SCTP 协议打算进行一系列优化,但并没有被广泛接受。这是因为数据在传输的过程中需要经过各个路由器,这些设备只能识别并解析 TCP 和 UDP 协议的数据包,无法解析新的协议。所以 SCTP 也只能用于内网的实验环境中。

TCP 要改进,但不方便改,新增一个协议又不被已有的设备支持,看起来唯一的方案就是使用 UDP 了。虽然 UDP 协议不保证数据可达性,但这也是 UDP 的优点所在,它天然支持 0-RTT 的通信,所以一个比较新颖激进的想法就冒出来了:

采用 UDP 作为底层协议,在 UDP 之上实现数据可达性

目前,QUIC 协议内置在 Chrome 浏览器中,每次更新只需要升级浏览器即可,在 2014 年前就已经迭代了 13 个版本。

HTTPS

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版。即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,用于安全的 HTTP 数据传输。,信息的加密过程就是在 SSL 中完成的

https 出现的背景

首先,我们用 wireshark 对 http 协议下网络通讯进行抓包,效果如下图:

有没有感觉这个的信息传输是完全暴露在互联网上面,你请求的所有信息都可以被窥测到,是不是感觉心一凉,不过不用担心,我们的安全信息现在都是采用 https 的传输,后面讲到 https 的时候大家心里会顿时轻松。https 协议下的请求如下图所示:

可以看到,http 的信息传输中,信息很容易被劫持,更有甚者,黑客可以伪装服务器将篡改后的信息返回给用户,试想一下,如果劫持的是你的银行信息,是不是很可怕。所以对于 http 传出存在的问题可以总结如下:

(1)信息篡改:修改通信的内容

(2)信息劫持:拦截到信息通信的内容

这些是 http 不安全的体现,说完 http,我们回到本文的主题 https,看下人家是怎么保护信息的,所有的请求信息都采用了 TLS 加密,如果没有秘钥是无法解析传输的是什么信息

https 加密方法

当客户端发送 Hello 字符串的时候,在进行信息传输前,采用加密算法(上图中的秘钥 S)将 hello 加密程 JDuEW8&*21!@#进行传输,即使中间被劫持了,如果没有对应的秘钥 S 也无法知道传出的信息为何物。

在上图中信息的加密和解密都是通过同一个秘钥进行的,对于这种加密我们称之为对称加密,只要 A 和 B 之间知道加解密的秘钥,任何第三方都无法获取秘钥 S,则在一定条件下,基本上解决了信息通信的安全问题。但实际的通讯模型远比上图复杂,下图为实际的通信模型:

由于 server 和所有的 client 都采用同一个秘钥 S,则黑客们作为一个 client 也可以获取到秘钥 S,此地无银三百两。所以在实际的通讯中,一般不会采用同一个秘钥,而是采用不同的秘钥加解密,如下图:

如上图,A 和 server 通信采用对称加密 A 算法,B 和 server 通信采用对称秘钥 B 算法,因此可以很好的解决了不同的客户端采用相同的秘钥进行通讯的问题。

那现在又存在问题了,A 通过明文传输和 server 协商采用了加密算法 A,但这条信息本身是没有加密的,因此 ××× 们还是可以窃取到秘钥的,整个的通讯仍然存在风险。那该如何处理呢?有人说,把这条信息(协调秘钥的过程)再次加密,那是不是还要协商加密秘钥,如此反复,永无止境。从根本上无法解决信息通讯的安全问题。

如何对协商过程进行加密?

在密码学跟对称加密一起出现的,应用最广的加密机制非对称加密,如上图,特点是私钥加密后的密文,只要是公钥,都可以解密,但是反过来公钥加密后的密文,只有私钥可以解密。私钥只有一个人有,而公钥可以发给所有的人。

基于上述的特点,我们可以得出如下结论:

(1)公钥是开放给所有人的,但私钥是需要保密的,存在于服务端

(2)服务器端 server 向 client 端(A、B…..)的信息传输是不安全的:因为所有人都可以获取公钥

(3)但 client 端(A、B…..)向 server 端的信息传输确实安全的:因为私钥只有 server 端存在

因此,如何协商加密算法的问题,我们解决了,非对称加密算法进行对称加密算法协商过程。

信息通信采用 http 是不安全的,存在信息劫持、篡改的风险,https 是加密传输,是安全的通信,对于 https 加密的过程,我们首先介绍的对称加密,采用对称加密进行通信存在秘钥协商过程的不安全性,因此我们采用了非对称加密算法解决了对协商过程的加密,因此 https 是集对称加密和非对称加密为一体的加密过程。

如何安全的获取公钥?

细心的人可能已经注意到了如果使用非对称加密算法,我们的客户端 A,B 需要一开始就持有公钥,要不没法开展加密行为啊。

这下,我们又遇到新问题了,如何让 A、B 客户端安全地得到公钥?

client 获取公钥最最直接的方法是服务器端 server 将公钥发送给每一个 client 用户,但这个时候就出现了公钥被劫持的问题,如上图,client 请求公钥,在请求返回的过程中被劫持,那么我们将采用劫持后的假秘钥进行通信,则后续的通讯过程都是采用假秘钥进行,数据库的风险仍然存在。

在获取公钥的过程中,我们又引出了一个新的话题:如何安全的获取公钥,并确保公钥的获取是安全的, 那就需要用到终极武器了:SSL 证书(需要购买)和 CA 机构。

如上图所示,在第 ② 步时服务器发送了一个 SSL 证书给客户端,SSL 证书中包含的具体内容有证书的颁发机构、有效期、公钥、证书持有者、签名等信息,通过第三方的校验保证了身份的合法,解决了公钥获取的安全性。

CA 证书签发过程

  1. 服务方 S 向第三方机构 CA 提交公钥、组织信息、个人信息(域名)等信息并申请认证(申请证书不需要提供私钥,确保私钥永远只能服务器掌握)。

  2. CA 通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等。

  3. 如信息审核通过,CA 会向申请者签发认证文件-证书。证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名。签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA 的私钥对信息摘要进行加密,密文即签名。

  4. 客户端 C 向服务器 S 发出请求时,S 返回证书文件。

  5. 客户端 C 读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法

  6. 客户端然后验证证书相关的域名信息、有效时间等信息。

  7. 客户端会内置信任 CA 的证书信息(包含公钥),如果 CA 不被信任,则找不到对应 CA 的证书,证书也会被判定非法。

在这个过程注意几点:

  1. 申请证书不需要提供私钥,确保私钥永远只能服务器掌握

  2. 证书的合法性仍然依赖于非对称加密算法,证书主要是增加了服务器信息以及签名

  3. 内置 CA 对应的证书称为根证书。颁发者和使用者相同,自己为自己签名(用 CA 自己的私钥签名),即自签名证书(此证书中的公钥即为 CA 的公钥,可以使用这个公钥对证书的签名进行校验,无需另外一份证书)

  4. 证书=公钥+申请者与颁发者信息+签名

有同学看完后可能会问,证书如果被掉包了怎么办?

数字证书包括了加密后服务器的公钥、权威机构的信息、服务器域名,还有经过 CA 私钥签名之后的证书内容(经过先通过 Hash 函数计算得到证书数字摘要,然后用权威机构私钥加密数字摘要得到数字签名),签名计算方法以及证书对应的域名。当客户端收到这个证书之后,使用本地配置的权威机构的公钥对证书进行解密得到服务端的公钥和证书的数字签名,数字签名经过 CA 公钥解密得到证书信息摘要,然后根据证书上描述的计算证书的方法计算一下当前证书的信息摘要,与收到的信息摘要作对比,如果一样,表示证书一定是服务器下发的,没有被中间人篡改过。

因为中间人虽然有权威机构的公钥,能够解析证书内容并篡改,但是篡改完成之后中间人需要将证书重新加密,但是中间人没有权威机构的私钥,无法加密,强行加密只会导致客户端无法解密,如果中间人强行乱修改证书,就会导致证书内容和证书签名不匹配。

那第三方攻击者能否让自己的证书显示出来的信息也是服务端呢?

(伪装服务端一样的配置)显然这个是不行的,因为当第三方攻击者去 CA 那边寻求认证的时候 CA 会要求其提供例如域名的 whois 信息、域名管理邮箱等证明你是服务端域名的拥有者,而第三方攻击者是无法提供这些信息所以他就是无法骗 CA 他拥有属于服务端的域名

总结:HTTPS 要使客户端与服务器端的通信过程得到安全保证,必须使用对称加密算法,但是协商对称加密算法的过程,需要使用非对称加密算法来保证安全,然而直接使用非对称加密的过程本身也不安全,会有中间人篡改公钥的可能性,所以客户端与服务器不直接使用公钥,而是使用数字证书签发机构颁发的证书来保证非对称加密过程本身的安全。这样通过这些机制协商出一个对称加密算法,就此双方使用该算法进行加密解密。从而解决了客户端与服务器端之间的通信安全问题。

基本的运行过程

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。

但是,这里有两个问题。

(1)如何保证公钥不被篡改?

解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

(2)公钥加密计算量太大,如何减少耗用的时间?

解决方法:每一次对话(session),客户端和服务器端都生成一个”对话密钥”(session key),用它来加密信息。由于”对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密”对话密钥”本身,这样就减少了加密运算的消耗时间。

因此,SSL/TLS协议的基本过程是这样的:

(1) 客户端向服务器端索要并验证公钥。

(2) 双方协商生成”对话密钥”。

(3) 双方采用”对话密钥”进行加密通信。

上面过程的前两步,又称为”握手阶段”(handshake)。

握手阶段的详细过程

“握手阶段”涉及四次通信,我们一个个来看。需要注意的是,”握手阶段”的所有通信都是明文的。

客户端发出请求(ClientHello)

首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。

在这一步,客户端主要向服务器提供以下信息。

(1) 支持的协议版本,比如TLS 1.0版。

(2) 一个客户端生成的随机数,稍后用于生成”对话密钥”。

(3) 支持的加密方法,比如RSA公钥加密。

(4) 支持的压缩方法。

这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应该向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。

对于虚拟主机的用户来说,这当然很不方便。2006年,TLS协议加入了一个Server Name Indication扩展,允许客户端向服务器提供它所请求的域名。

服务器回应(SeverHello)

服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。

(1) 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。

(2) 一个服务器生成的随机数,稍后用于生成”对话密钥”。

(3) 确认使用的加密方法,比如RSA公钥加密。

(4) 服务器证书。

除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供”客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。

如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。

(1) 一个随机数。该随机数用服务器公钥加密,防止被窃听。

(2) 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

(3) 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称”pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把”会话密钥”。

至于为什么一定要用三个随机数,来生成”会话密钥”:

“不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。

对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。

pre master的存在在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。”

此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。

服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的”会话密钥”。然后,向客户端最后发送下面信息。

(1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用”会话密钥”加密内容。

获取证书

CA 证书

现如今不比以前了,云服务的概念不仅从理论上深入到了互联网应用,而且变成了一个社会的基础设施工作,世界云服务 3A:亚马逊 AWS、微软 Azure、阿里云,阿里云作为国人的骄傲跻身世界三大云服务厂商,且在国内,阿里云的市场份过半,且阿里云的操作系统“飞天系统”为自主研发,而不是采用开源的 OpenStack。因此这些云服务厂商都提供了友好的 CA 证书申请流程,本文只以腾讯云、AlphaSSL 为例进行说明申请的流程

基本分类

根据证书的安全级别,可分为:

(1) DV SSL 证书是只验证网站域名所有权的简易型(Class 1 级)SSL 证书,可 10 分钟快速颁发,能起到加密传输的作用,但无法向用户证明网站的真实身份。

目前市面上的免费证书都是这个类型的,只是提供了对数据的加密,但是对提供证书的个人和机构的身份不做验证。

(2) OV SSL,提供加密功能,对申请者做严格的身份审核验证,提供可信 ××× 明。

和 DV SSL 的区别在于,OV SSL 提供了对个人或者机构的审核,能确认对方的身份,安全性更高。

所以这部分的证书申请是收费的~

(3)EV SSL,超安=EV=最安全、最严格 超安 EV SSL 证书遵循全球统一的严格身份验证标准,是目前业界安全级别最高的顶级 (Class 4 级)SSL 证书。

金融证券、银行、第三方支付、网上商城等,重点强调网站安全、企业可信形象的网站,涉及交易支付、客户隐私信息和账号密码的传输。

这部分的验证要求最高,申请费用也是最贵的。

根据保护域名的数量需求,SSL 证书又分为:

  1. 单域名版:只保护一个域名,例如 www.abc.com 或者 login.abc.com 之类的单个域名

  2. 多域名版:一张证书可以保护多个域名,例如同时保护 www.abc.com , www.bcd.com, pay.efg.com 等

  3. 通配符版:一张证书保护同一个主域名下同一级的所有子域名,不限个数,形如 *.abc.com 。注意,通配符版只有 DVSSL 和 OVSSL 具有, EVSSL 不具有通配符版本。

阿里云目前已经不提供免费的 SSL 证书,即 DV SSL,但目前国内可以提供免费的 SSL 证书的云厂商有腾讯云,至于什么时候收费,笔者暂时不太清楚,但至少这个时期是 OK 的

腾讯云

登录腾讯云控制台,找到 SSL 证书,进入购买页面,找到域名型免费性(DV),点击“免费申请”:

进入域名验证环节,需要注意:通用域名必须是指定的一个明确的域名地址,不能是通配域名,其次私钥密码在申请的过程中是选填,但在国外网站申请的时候确实必填。

选择验证方式,笔者一般会通过文件的方式,直接通过 nginx 创建一个文件目录,进行通信就可以完成身份的验证,具体的验证过程可以参考腾讯云的详细说明。

等待审核通过,一般在 1-3 小时的时间。具笔者计算,一个统配域名在国外买在 1800 人民币的样子,但在国内购买需要 2500 以上。接下来重点介绍 AlphaSSL 购买流程

AlphaSSL

申请网址:https://www.alphassl.com/ssl-certificates/select-region/

(1):选择所在区域,此处选择 other(国外没有将 Asia 作为一个明确的区域标识气愤,但谁让我们技不如人呢)

(2)产品详情:此处注意购买统配的域名,这个买起来更划算。

(3)基本信息的填写,没有什么需要注意的

(4)CSR 这个步骤是最容易出错,且不太能让人理解的地方,笔者在这里做个简单的普及。

CSR(证书请求文件) 包含申请证书所需要的相关信息,其中最重要的是域名,填写的域名必须是你要 https 方式访问的那个域名。如 abc.com 或 web.abc.com,其中 CSR 生成的方式很多,笔者直接用了网上的一个生成网站:https://myssl.com/csr_create.html

填写好相关的信息,尤其是域名信息一定要正确,可以根据生成的方式进行生成,生成之后产生了 2 个文件,一个为 CSR 文件,用来向 CA 机构申请的文件,一般以 CSR 结尾,一个是 KEY 文件,这个文件一定要保存好,这个文件就是对应第一章节将的 server 端的私钥,这个信息首先是重要,如果这个 KEY 文件没有保存好,是无法找回的,因为 KEY 生成的过程不可逆,即使填写的过程都一样,生成的 KEY 是不通的,具有随机性。

将生成的 CRS 粘贴进入第四步点击下一步就进入了付款环节,由于是国外购买,所以必须使用 VISA 的信用卡。一般付款之后,6 小时左右证书就可以下来。当然笔者在申请的过程中就把 KEY 文件给丢了,导致找客服(英文对话,核实身份),其实如果申请存在问题,7 天内是可以申请退款,其次如果你把 KEY 文件丢失了,可以通过找客服进行更新,详细可以参考:https://support.globalsign.com/customer/portal/articles/1223116

自签名证书

原理和局限性

所谓自签名证书,就是自己颁发给自己的证书 ,所以颁证的主体是不可信任的。

自签证书是不会被浏览器信任的证书的,用户在访问自签证书时,浏览器会警告用户此证书不受信任,需要人工确认是否信任此证书,如下图:

想让浏览器不出现警告我们需要给所有的客户端安装该证书,如果你需要第二个证书,则还的挨个给所有的客户端安装证书 2 才会被信任。

既然自签证书是不可信任的,那为何还有人在用自签证书呢?

主要原因是:

  1. 自签证书是免费的
  2. 自签证书相对申请 CA 证书,流程更简单
  3. 自签证书同样可以对数据进行加密
  4. 自签证书的有效期可以设置很长,免去续签的麻烦
  5. 自签证书更方便测试,比如说你想生成多少个不同服务器 ip 的都可以
  6. 所以对于一些个人开发者来说使用自签证书可能会更方便,只要你能接受别人浏览你网站时弹出的提醒:不安全。

在密码学中,X.509 是一个标准,规范了公开秘钥认证、证书吊销列表、授权凭证、凭证路径验证算法等。

X.509 证书包含三个文件:key,csr,crt。

  1. key 是服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密

  2. csr 是证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名

  3. crt 是由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息

其他常见相关扩展名:

  1. .crt 证书文件 ,可以是 DER(二进制)编码的,也可以是 PEM( ASCII (Base64) )编码的 ,在类 unix 系统中比较常见
  2. .cer 也是证书 常见于 Windows 系统 编码类型同样可以是 DER 或者 PEM 的,windows 下有工具可以转换 crt 到 cer
  3. .csr 证书签名请求 一般是生成请求以后发送给 CA,然后 CA 会给你签名并发回证书
  4. .key 一般公钥或者密钥都会用这种扩展名,可以是 DER 编码的或者是 PEM 编码的 查看 DER 编码的(公钥或者密钥)的文件的命令为 openssl rsa -inform DER -noout -text -in xxx.key 查看 PEM 编码的(公钥或者密钥)的文件的命令为 openssl rsa -inform PEM -noout -text -in xxx.key
  5. .p12 证书 包含一个 X509 证书和一个被密码保护的私钥
创建自签名证书的步骤

第 1 步:生成私钥

使用 openssl 工具生成一个 RSA 私钥

$ openssl genrsa -des3 -out server.key 2048

说明:生成 rsa 私钥,des3 算法,2048 位强度,server.key 是秘钥文件名。

注意:生成私钥,需要提供一个至少 4 位的密码。

第 2 步:生成 CSR(证书签名请求)

生成私钥之后,便可以创建 csr 文件了。

此时可以有两种选择。理想情况下,可以将证书发送给证书颁发机构(CA),CA 验证过请求者的身份之后,会出具签名证书(很贵)。另外,如果只是内部或者测试需求,也可以使用 OpenSSL 实现自签名,具体操作如下:

$ openssl req -new -key server.key -out server.csr

说明:需要依次输入国家,地区,城市,组织,组织单位,Common Name 和 Email。其中 Common Name,可以写自己的名字或者域名,如果要支持 https,Common Name 应该与域名保持一致,否则会引起浏览器警告。

Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) [Internet Widgits Pty Ltd]:xx
Organizational Unit Name (eg, section) []:info technology
Common Name (e.g. server FQDN or YOUR name) []:xx.xxx.com
Email Address []:xx@xx.com

第 3 步:删除私钥中的密码

在第 1 步创建私钥的过程中,由于必须要指定一个密码。而这个密码会带来一个副作用,那就是在每次 Apache 启动 Web 服务器时,都会要求输入密码,这显然非常不方便。要删除私钥中的密码,操作如下:

cp server.key server.key.org openssl rsa -in server.key.org -out server.key

第 4 步:生成自签名证书

如果你不想花钱让 CA 签名,或者只是测试 SSL 的具体实现。那么,现在便可以着手生成一个自签名的证书了。

需要注意的是,在使用自签名的临时证书时,浏览器会提示证书的颁发机构是未知的。

$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

说明:crt 上有证书持有人的信息,持有人的公钥,以及签署者的签名等信息。当用户安装了证书之后,便意味着信任了这份证书,同时拥有了其中的公钥。证书上会说明用途,例如服务器认证,客户端认证,或者签署其他证书。当系统收到一份新的证书的时候,证书会说明,是由谁签署的。如果这个签署者确实可以签署其他证书,并且收到证书上的签名和签署者的公钥可以对上的时候,系统就自动信任新的证书。

第 5 步:安装私钥和证书

在 nginx 的配置文件中,新增如下配置项,在这个地方有一个参数:ssl on,如果这个参数开启,http 和 https 将不能共存。里面对应的信息都可以通过 CA 机构获取到:

listen 443 ssl;
ssl_certificate     /iyunwen/server/ssl/20180731.cer;
ssl_certificate_key /iyunwen/server/ssl/20180731.key;
ssl_prefer_server_ciphers on;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";

这里给出了阿里云 SSL 在主流 apache、nginx 的配置文档:https://help.aliyun.com/video_detail/54216.html?spm=a2c4g.11186623.4.1.WbwjQN

腾讯云 SSL 配置文档:https://cloud.tencent.com/document/product/400/4143

当然,如果不想弄这么复杂的话,也可以使用下面的命令一步到位:

openssl genrsa -out xx.xxx.com.key 2048
openssl req -new -x509 -key xx.xxx.com.key -out xx.xxx.com.cert -days 3650 -subj /CN=localhost