HTTP简介在前篇博客:Web三大技术要素中有介绍,三大技术要素之间相互关联依赖,比如HTML中的超链接使用URL,HTTP报文中的主体部分则是HTML文档或其内包含的MIME资源。
本文主要介绍HTTP协议部分,由于目前使用最广泛的仍然是HTTP/1.1 版本,截至2020年4月,W3Techs统计的前1000万网站中支持HTTP/2的大概占43.6%,支持QUIC的仅占4.2%。HTTP/2也是从HTTP/1.1演化升级而来,要了解HTTP/2同样离不开HTTP/1.1,因此本文先介绍主流的HTTP/1.1版本。
我们再看看HTTP协议在整个网络分层参考模型中的位置,HTTP属于应用层协议,底层依赖于TCP/IP协议完成数据报文的传输。如果对HTTP报文有加密传输的需求,在HTTP协议与TCP协议之间还可以增加SSL/TLS协议层,这就构成了HTTPS协议。
在介绍HTTP/1.1 报文结构前,再回顾下HTTP/1.1 解决了前代的哪些问题或者带来了哪些性能提升:
HTTP/1.0问题 | HTTP/1.1改进 |
---|---|
不能让多个请求共用一个连接 | 默认支持持久连接(keep-alive),管道化(pipelining)特性允许客户端在一次TCP连接中发送所有的请求,这对于提升性能和效率而言意义重大 |
缺少强制的 Host 首部 | 强制要求客户端提供 Host 首部,让虚拟主机托管(在一个 IP 上提供多个 Web 服务主机域名)成为可能 |
缓存控制相当简陋 | 扩展缓存相关首部以增强缓存控制,增加Range请求以支持断点续传,增加Upgrade 首部以支持升级到其它协议比如WebSocket |
仅支持基本的GET、POST 和 HEAD请求方法 | 增加了OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 等六种请求方法,扩展了功能 |
上面提到的最重要性能改进 — HTTP持久连接与管道化机制在前篇博客Web三大技术要素中也介绍过,这里再给出图示:
TCP三次握手与四次挥手的过程比较耗时,现在每个网页的平均资源数接近两百,如果每个资源请求都执行一次TCP连接/断开,对网络资源的浪费相当大。因此,HTTP/1.1 支持多个请求共用一次TCP连接,也即在请求网页时执行一次TCP连接,随后客户端向服务器发送该网页相关的所有资源请求,既节省了大量重复的TCP连接/断开开销,又不需要等收到资源响应后再发送下一个资源请求,极大提升了Web性能与效率。
HTTP/1.1 虽然支持以流水线的形式在一次TCP连接中发送指定网页的所有资源请求,但是服务器仍然只能按顺序响应请求,如果处理某个请求花了很长时间,那么队头阻塞(head of line blocking)会影响其他请求,这是HTTP/1.1 待改进的一个方面。
HTTP/1.1 诞生于1997年,到现在已经二十多年过去了,Web发展更是突飞猛进,HTTP/1.1 的性能瓶颈越来越多,比如每次资源请求都需要发送越来越大的的报文首部,每个网页中平均近两百个资源的报文首部大同小异,这又是对网络资源的极大浪费。随着对Web性能的需求越来越高,针对HTTP/1.1 的性能瓶颈出现了五花八门的优化方案,直到近20年后的2015年,HTTP/2才姗姗来迟,解决了HTTP/1.1 遗留的诸多瓶颈,跟上了目前跨平台多终端Web的性能需求。
HTTP协议采用Client / Browser — Server 架构通信,当我们使用Client / Browser 访问Web时,在浏览器地址栏输入想获取资源页面的URL,Browser 向Server 发送 HTTP请求报文,告诉服务器想请求哪个资源,Server 找到或生成请求的资源后,通过HTTP响应报文将资源发送给Browser / Client,浏览器或客户端接收到HTML资源页面后,将其处理成更直观的形式展示给你,整个过程图示如下:
HTTP 请求报文包括请求行、请求头部、空行、报文主体等部分构成,请求行包括请求方法、URL、协议版本三部分,请求首部主要包括请求的各种条件或属性,空行是为了分割首部与主体,报文主体则包含应被发送的数据。HTTP请求报文结构与示例如下:
HTTP 请求方法是客户端用来告知服务器自己意图的,HTTP/1.1支持的请求方法及功能描述如下表示(请求方法名要使用大写字母):
方法 | 描述 |
---|---|
GET | 用来请求访问已被 URL 指定的资源,请求条件或参数被包含在URL中,指定的资源经服务器端解析后返回响应内容,操作是安全且幂等的;如果请求的资源是静态文本则保持原样返回,如果是像 CGI / Servlet 那样的程序则返回经过执行后的输出结果 |
HEAD | 和 GET 方法一样,只是不返回报文主体部分,常用于确认URL 的有效性及资源更新的日期时间等 |
POST | 向指定的资源提交数据进行处理的请求,比如提交表单或上传文件,数据被包含在请求报文主体中,操作不是幂等的 |
PUT | 从客户端向服务器传送数据取代 URL 指定的资源内容,操作是幂等的(不管进行多少次相同的操作,结果都一样) |
PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新 |
DELETE | 请求服务器删除 URL 指定的资源,操作是幂等的 |
OPTIONS | 用来查询服务器针对指定URL 支持的 HTTP 方法 |
TRACE | 让客户端可以看到中间服务器进行了哪些更改或添加,主要用于测试或诊断 |
CONNECT | 将请求连接转换为透明的TCP/IP 隧道,通常是为了通过未加密的 HTTP 代理促进TLS/SSL 加密通信 (HTTPS) |
HTTP/1.1 请求方法中比较基本的有四种:GET、POST、PUT、DELETE(HEAD与PATCH方法可以分别看作GET与PUT方法的补充),可以分别对应访问资源的四种基本操作:查、增、改、删。符合REST(Representational State Transfer,表现层状态转化)互联网软件架构原则的RESTful API 正式通过HTTP的这四种基本请求方法完成对资源或数据对象的增、删、改、查等操作的,关于RESTful 架构设计会在下文给出简单介绍。
在辨析HTTP的四种基本请求方法之前,需要先解释下上表提到的两个概念 — 安全和幂等:
HTTP提供的四种基本请求方法中,只有GET方法要求是安全的,POST、PUT、DELETE方法都不是安全的;GET、PUT、DELETE三个方法都是幂等的,只有POST方法不是幂等的。为什么专门区分幂等性呢?举个例子,我们购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,我们再次点击付款按钮,如果付款请求不是幂等的,那么会进行第二次扣款,如果付款请求是幂等的,多次相同的付款请求跟一次付款请求的结果是一样的,不会进行多次重复扣款。
方法 | 是否安全 | 是否幂等 | RESTful API 操作对应HTTP请求方法 |
---|---|---|---|
GET | 是 | 是 | 查询操作 |
POST | 否 | 否 | 新增操作 |
PUT | 否 | 是 | 更新操作 |
DELETE | 否 | 是 | 删除操作 |
通过安全性与幂等性两个概念,应该可以清晰区分HTTP四种基本方法的使用了,比较容易混淆的是POST方法与PUT方法,这两个方法的本质区别在于是否满足幂等性,POST方法还可以提交URL 所指定资源的子资源(从属资源),比如分别用POST与PUT方法创建资源,POST方法可以只指定集合资源URL(也即只指定父资源URL,待创建子资源的名字可以由服务器分配),PUT方法则必须指定具体资源的URL(待创建子资源的名字需要客户端在URL中指定),很多时候我们要创建的子资源命名交由服务器分配更高效省事(比如可以避免URL重复等),所以在RESTful API 中常使用POST方法来创建资源;PUT方法本就是幂等性替换整个目标资源,因此在RESTful API 中常用PUT方法来更新资源(POST方法不是幂等操作,因此不适合用来更新资源)。
我们访问Web时,最常用的方法是GET和POST,GET方法常用来从服务器获取目标网页资源信息,POST方法常用来向服务器提交资源数据(比如创建新博客、发表新状态或者新评论、上传新文件等)。HTTP/1.1 的这些请求方法自身不带验证机制,存在安全性问题,一般需要配合Web应用程序的验证机制(比如注册并登录自己的账号),用户只能对部分拥有相应权限的资源使用PUT或DELETE方法(比如用户对自己创建的博客或评论才拥有更新或删除的权限)。
请求报文头部字段比较多,下文再详细介绍,请求报文主体一般使用HTML语法来描述资源信息,HTML语法在前篇博客:Web简史与三大技术要素中有过简介,这里也不再赘述了。
HTTP响应报文包括状态行、响应头部、空行、报文主体等部分构成,响应行包括协议版本、状态码、状态原因短语三部分,响应头部主要包括响应的各种条件或属性,空行用于分割首部和主体,报文主体则包含应被发送的数据。HTTP响应报文结构与示例如下:
HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用,HTTP状态码共分为5种类型:
状态码 | 类别 | 状态原因短语 | 常见状态码 |
---|---|---|---|
1XX | Informational(信息性状态码) | 服务器接收到请求,需要请求者继续执行操作 | 100、101 |
2XX | Success(成功状态码) | 请求已被正确处理完毕 | 200、204、206 |
3XX | Redirection(重定向状态码) | 资源位置已经变动,需要进行附加操作以完成请求 | 301、302、304 |
4XX | Client Error(客户端错误状态码) | 请求报文包含语法错误,服务器无法处理请求 | 400、401、403、404 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求时出错 | 500、501、502、503 |
比较常见的HTTP响应状态码及其描述举例如下(想了解更多响应码含义可参考:HTTP状态码):
状态码 | 状态名 | 描述 |
---|---|---|
100 | Continue | 客户端应继续其请求 |
101 | Switching Protocols | 服务器根据客户端的请求切换协议,只能切换到更高级的协议,例如切换到HTTP的新版本协议 |
200 | OK | 从客户端发来的请求在服务器端被成功处理了 |
204 | No Content | 与 200 OK 基本相同,但在返回的响应报文中不含报文主体 body 部分 |
206 | Partial Content | 也是服务器处理成功的状态,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分。响应报文中包含由 Content-Range 指定范围的实体内容,常用于 HTTP 分块下载或断电续传 |
301 | Moved Permanently | 表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL |
302 | Found | 表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问,跟 301 一样使用Location 字段指明后续要跳转的URL |
304 | Not Modified | 所请求的资源未修改,重定向已存在的缓冲文件,也称缓存重定向,用于缓存控制。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 |
400 | Bad Request | 客户端请求的语法错误,服务器无法理解 |
401 | Unauthorized | 发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息 |
403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 |
404 | Not Found | 请求的资源在服务器上不存在或未找到,所以无法提供给客户端 |
500 | Internal Server Error | 服务器内部错误,无法完成请求 |
501 | Not Implemented | 服务器不支持请求的功能,无法完成请求 |
502 | Bad Gateway | 通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误 |
503 | Service Unavailable | 由于超载或系统维护,服务器暂时无法处理客户端的请求,延时的长度可包含在服务器的Retry-After头信息中 |
响应报文头部字段也在下文跟请求报文头部字段放到一起介绍,这里先暂略了,响应报文主体也是使用HTML语法来描述资源信息的。HTTP/1.1 报文主体是可以被压缩传送的,也支持分块传输,但报文首部不能被压缩。
HTTP 首部字段是构成 HTTP 报文的要素之一,在客户端与服务器之间以 HTTP 协议进行通信的过程中,无论是请求还是响应都会使用首部字段,它能起到传递额外重要信息的作用,比如提供报文主体大小、所使用的语言、认证信息等内容。从前面给出的HTTP报文格式图示可以看出,HTTP 首部字段是由首部字段名和字段值构成的,中间用冒号“:” 分隔。
HTTP 首部字段根据实际用途被分为以下 4 种类型:
HTTP/1.1 规范RFC2616中定义的通用首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Cache-Control | 控制缓存的行为 | Cache-Control: private, max-age=0, no-cache |
Connection | 逐跳首部、连接的管理 | Connection: Keep-Alive |
Date | 创建报文的日期时间 | Date: Tue, 03 Jul 2020 04:40:59 GMT |
Pragma | 为与 HTTP/1.0兼容而定义的指令字段 | Pragma: no-cache |
Trailer | 事先说明在报文主体后记录了哪些首部字段 | Trailer: Expires |
Transfer-Encoding | 指定报文主体的传输编码方式,支持分块传输 | Transfer-Encoding: chunked |
Upgrade | 升级为其他协议, 常与Connection字段一起使用 |
Upgrade: TLS/1.0 Connection:Upgrade |
Via | 追踪请求和响应报文的传输路径, 常与TRACE 方法一起使用 |
Via: 1.0 gw.hackr.jp (Squid/3.1) |
Warning | 会告知用户一些与缓存相关的问题的警告 | Warning: [警告码][警告的主机:端口号]“[警告内容]”([日期时间]) |
这里重点介绍下缓存控制字段Cache-Control,缓存是指代理服务器或客户端本地磁盘内保存的资源副本。利用缓存可减少对源服务器的访问,因此也就节省了通信流量和通信时间。但有些资源不能缓存(比如涉及安全隐私的敏感信息)、有些不常访问的资源没必要缓存、有些经常访问的资源需要缓存、有些缓存资源已过期需要更新等,这些都可以靠首部字段Cache-Control来控制缓存的行为。缓存字段值可以配置多个指令,多指令间通过逗号分隔,用于请求和响应报文中的字段值或指令有一定差异,可用的缓存请求 / 响应指令对比如下:
先辨析两个容易混淆的指令:
即便缓存服务器(一种代理服务器)或客户端浏览器内有缓存,也不能保证每次都会返回对相同资源的请求,因为这关系到被缓存资源的有效性问题。当遇上源服务器上的资源更新时,如果还是使用不变的缓存,那就会演变成返回更新前的“旧”资源了。即使存在缓存,也会因为客户端的要求、缓存的有效期等因素,向源服务器确认资源的有效性。若判断缓存有效,则可直接从缓存服务器或客户端本地磁盘内读取资源数据;若判断缓存失效,缓存服务器将会再次从源服务器上获取“新”资源。
Connection 首部字段具备如下两个作用:控制不再转发给代理的首部字段;管理持久连接(配置值为Keep-Alive,前面已经介绍过了)。在客户端发送请求和服务器返回响应内,使用 Connection 首部字段,可以控制代理不再转发该首部字段,比如Connection:Upgrade表示经过代理时先删除掉Upgrade 首部字段再转发该报文。这就要求需要升级通信协议(比如Upgrade: WebSocket 或Upgrade: HTTP/2)的客户端与服务器之间不能有代理服务器,二者应该是直接连接。对于附有首部字段 Upgrade 的请求,服务器可用 101 Switching Protocols 状态码作为响应返回。
HTTP/1.1 规范RFC2616中定义的请求首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Accept | 用户代理可处理的媒体类型 | Accept:text/html,application/xhtml+xml,application/xml;q=0.9 |
Accept-Charset | 可处理的字符集及其优先级 | Accept-Charset: iso-8859-5,unicode-1-1;q=0.8 |
Accept-Encoding | 可处理的内容编码及其优先级 | Accept-Encoding: gzip,compress,deflate |
Accept-Language | 可处理的自然语言及其优先级 | Accept-Language: zh-cn,zh;q=0.7,en-us,en;q=0.3 |
Authorization | Web服务器要求客户端的认证信息 | Authorization: Basic dWVub3NlbjpwYXNzd29yZA== |
Expect | 期待服务器的特定行为 | Expect: 100-continue |
From | 用户的电子邮箱地址 | From: [email protected] |
Host | 请求资源所在服务器主机名或域名 | Host: www.baidu.com |
If-Match | 比较实体标记(ETag) | If-Match: “123456” |
If-Modified-Since | 比较资源的更新时间 | If-Modified-Since: Thu, 15 Apr 2018 00:00:00 GMT |
If-None-Match | 比较实体标记(与 If-Match 相反) | If-None-Match: * |
If-Range | 资源未更新时发送实体 Byte 的范围请求 | If-Range: “123456” |
If-Unmodified-Since | 比较资源的更新时间 (与If-Modified-Since相反) |
If-Unmodified-Since: Thu, 03 Jul 2018 00:00:00 GMT |
Max-Forwards | 最大传输逐跳数 | Max-Forwards: 10 |
Proxy-Authorization | 代理服务器要求客户端的认证信息 | Proxy-Authorization: Basic dGlwOjkpNLAGfFY5 |
Range | 实体的字节范围请求 | Range: bytes=5001-10000 |
Referer | 对请求中 URI 的原始获取方 | Referer: http://www.baidu.com/index.html |
TE | 传输编码格式及其优先级 | TE: gzip,deflate;q=0.5 |
User-Agent | HTTP 客户端程序的信息 | user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/81.0.4044.92 |
首部字段 Accept 是客户端告诉服务器自己能处理的媒体类型(比如文本文件、图片文件、音视频文件、二进制文件等MIME类型)和相应的优先级(使用q 表示优先级权重值,用分号与对应的媒体类型分割,权重值q 范围是0~1,不指定权重值q 则默认为 q = 1.0)。接下来的Accept-Charset字段、Accept-Encoding字段、Accept-Language分别告诉服务器自己能处理的字符集类型、编码类型、语言类型及其对应的优先级或权重值。
首部字段 Host 用来配置服务器主机名(如果URL 中已经是一个绝对路径可以唯一定位目标资源,则会忽略Host字段),借助Host字段可以在一个IP(也即一个物理主机)上配置多个域名(也即多个Web服务),实现虚拟主机托管功能。
形如 If-xxx 这种样式的请求首部字段,都可称为条件请求。服务器接收到附带条件的请求后,只有判断指定条件为真时,才会执行请求。
首部字段 Range 可以指定范围发送,也即只请求自己需要的那部分资源,而不必再次下载整个资源。通过范围请求,还可以实现断点续传功能,因为某些原因下载中断,可以通过范围请求只下载尚未获得的部分资源,已获得的资源不必重新下载,进一步提高了网络利用效率。
首部字段 User-Agent 会将创建请求的浏览器和用户代理名称等信息传达给服务器,服务器可以根据这些信息返回适配浏览器的页面,让同样的信息可以在不同类型的终端页面上都有更直观友好的展示。
HTTP/1.1 规范RFC2616中定义的响应首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Accept-Ranges | 是否接受字节范围请求 | Accept-Ranges: bytes |
Age | 推算资源创建经过的时间 | Age: 600 |
ETag | 资源的唯一标识信息 | ETag: “usagi-1234” |
Location | 令客户端重定向至指定URI | Location: http://www.baidu.com/index.html |
Proxy-Authenticate | 代理服务器对客户端的认证信息 | Proxy-Authenticate: Basic realm=“Usagidesign Auth” |
Retry-After | 对再次发起请求的时机要求 | Retry-After: 120 |
Server | HTTP服务器的安装信息 | Server: Apache/2.2.6 (Unix) PHP/5.2.5 |
Vary | 代理服务器缓存的管理信息 | Vary: Accept-Language |
WWW-Authenticate | 服务器对客户端的认证信息 | WWW-Authenticate: Basic realm=“Usagidesign Auth” |
首部字段 ETag 是一种可将资源以字符串形式做唯一性标识的方式,服务器会为每份资源分配对应的 ETag值,当资源更新时,ETag 值也需要更新。通过比较ETag值可以判断出当前缓存的资源是否有效,前面请求首部字段中的 If-Match / If-None-Match 便是通过比较实体标记 ETag 值来判断是否执行相应的请求。
首部字段 Location 可以将响应接收方引导至某个与请求 URI 位置不同的资源,该字段常配合 3xx :Redirection 的响应,提供重定向的URI。几乎所有的浏览器在接收到包含首部字段 Location 的响应后,都会强制性地尝试对已提示的重定向资源的访问。
首部字段 Retry-After 告知客户端应该在多久之后再次发送请求。主要配合状态码 503 Service Unavailable 响应,或 3xx Redirect 响应一起使用,字段值可以指定为具体的日期时间或创建响应后的秒数。
某些 Web 页面只想让特定的人浏览,或者干脆仅本人可见,为达到这个目标,必不可少的就是认证功能。认证功能可以让服务器或代理服务器判断用户或客户端是否有访问该资源的权限,服务器需要客户端提供的认证信息通过响应报文中的WWW-Authenticate 字段值传递,客户端向服务器提交的认证信息通过请求报文中的Authorization 字段值提供,BASIC认证步骤如下(代理服务器对客户端的认证过程与此类似):
HTTP/1.1 规范RFC2616中定义的实体首部字段如下:
首部字段名 | 描述 | 示例 |
---|---|---|
Allow | 资源可支持的HTTP方法 | Allow: GET, HEAD |
Content-Encoding | 实体主体适用的编码方式 | Content-Encoding: gzip |
Content-Language | 实体主体的自然语言 | Content-Language: zh-CN |
Content-Length | 实体主体的大小(单位:字节) | Content-Length: 15000 |
Content-Location | 替代对应资源的URI | Content-Location: http://www.baidu.com/index.html |
Content-MD5 | 实体主体的报文摘要 | Content-MD5: OGFkZDUwNGVhNGY3N2MxMDIwZmQ4NTBmY2IyTY== |
Content-Range | 实体主体的位置范围 | Content-Range: bytes 5001-10000/10000 |
Content-Type | 实体主体的媒体类型 | Content-Type: text/html; charset=UTF-8 |
Expires | 实体主体过期的日期时间 | Expires: Wed, 04 Jul 2020 08:26:05 GMT |
Last-Modified | 资源的最后修改日期时间 | Last-Modified: Wed, 23 May 2020 09:59:55 GMT |
首部字段 Allow 用于通知客户端能够支持 Request-URI 指定资源的所有 HTTP 方法。当服务器接收到不支持的 HTTP 方法时,会以状态码405 Method Not Allowed 作为响应返回。与此同时,还会把所有能支持的 HTTP 方法写入首部字段 Allow 后返回。
首部字段 Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5分别告知客户端服务器对实体的主体部分选用的内容编码(在不丢失实体信息的前提下所进行的压缩)方式、自然语言、长度 / 字节数(对实体主体进行内容编码传输时,不能再使用 Content-Length首部字段)、相应的URL、MD5摘要字符串 等信息。
首部字段 Content-Range 能告知客户端作为响应返回的实体的哪个部分符合范围请求,字段值以字节为单位,表示当前发送部分及整个实体大小。
首部字段 Content-Type 说明了实体主体内对象的媒体类型,和请求首部字段 Accept 一样,字段值用 type/subtype 形式赋值。
HTTP 是一种不保存状态,即无状态(stateless)协议,它不对之前发生过的请求和响应的状态进行管理,也即无法根据之前的状态进行本次的请求处理。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。
随着 Web 的不断发展,因无状态而导致业务处理变得棘手的情况增多了,比如用户登录到一家购物网站,即使他跳转到该站的其它页面后,也需要能继续保持登录状态,网站为了能够掌握是谁送出的请求,需要保存用户的状态。HTTP/1.1 虽然是无状态协议,但为了实现期望的保持状态功能,引入了 Cookie 技术,有了 Cookie 再用 HTTP 协议通信,就可以管理状态了。
Cookie 技术通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态,Cookie 会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie 的首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。
管理服务器与客户端之间状态的 Cookie,虽然没有被编入标准化HTTP/1.1 的 RFC2616 中,但在 Web 网站方面得到了广泛的应用。目前使用最广泛的 Cookie 标准是在网景公司制定的标准上进行扩展后的产物(可参考RFC6265),下面的表格内列举了与 Cookie 有关的首部字段:
首部字段名 | 首部类型 | 描述 | 示例 |
---|---|---|---|
Set-Cookie | 响应首部字段 | 开始状态管理所使用的Cookie信息 | Set-Cookie: sid=1342077140226724; path=/; expires=Wed,10-Oct-12 07:12:20 GMT |
Cookie | 请求首部字段 | 服务器接收到的Cookie信息 | Cookie: sid=1342077140226724 |
响应首部字段Set-Cookie 的值可以配置多种属性信息,比如Cookie名称及其值、有效期、适用目录、适用域名、适用协议等,当客户端想获得 HTTP 状态管理支持时,就会在请求报文中包含从服务器接收到的 Cookie 名称及其值。
本文主要使用两个工具来捕获分析HTTP报文:一个是Chrome浏览器自带的Network工具(在开发者工具中);另一个是cURL 命令行工具集。
我们使用curl 工具集(下载地址:https://curl.haxx.se/download.html)访问百度首页,请求报文与响应报文内容如下(响应报文主体省略了):
C:\Users\Administrator\Downloads\curl-7.70.0-win64-mingw\bin>curl -v http://www.baidu.com/index.html
* Trying 61.135.169.125:80...
* Connected to www.baidu.com (61.135.169.125) port 80 (#0)
> GET /index.html HTTP/1.1
> Host: www.baidu.com
> User-Agent: curl/7.70.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
< Connection: keep-alive
< Content-Length: 2381
< Content-Type: text/html
< Date: Fri, 08 May 2020 07:43:39 GMT
< Etag: "588604c4-94d"
< Last-Modified: Mon, 23 Jan 2017 13:27:32 GMT
< Pragma: no-cache
< Server: bfe/1.0.8.18
< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
<
<!DOCTYPE html>
<!--STATUS OK-->
<html>...</html>
* Connection #0 to host www.baidu.com left intact
上面 “> ”开头的行是客户端发出的请求,“< ”开头的行是服务器的应答(行首的“> ”和“< ”本身并不是请求或者应答的一部分,只是curl输出的一种标记),“* ”开头的行是HTTP连接建立以前curl输出的一些诊断信息,比如我们可以看到curl通过DNS查找到“www.baidu.com”对应的IP—— 61.135.169.121。
上面的请求报文使用GET 方法,协议版本为 HTTP/1.1,百度首页的URL为 “http://www.baidu.com/index.html”(首部字段Host 指定服务主机域名),首部字段Accept 值表示客户端可以处理任何格式的媒体类型,首部字段User-Agent 值表示当前使用的HTTP客户端信息为"curl/7.70.0",GET请求报文主体为空。
上面的响应报文状态码为200 OK,表示请求已经正确处理完毕。首部字段Accept-Ranges 值表示服务器支持范围请求;Connection 值表示该HTTP请求使用持久连接。
首部字段Cache-Control 值表示仅向特定用户返回响应(private)、缓存会向源服务器进行有效期确认后处理资源(no-cache)、资源不进行缓存(no-store)、要求代理服务器对缓存的响应有效性再进行确认(proxy-revalidate)、代理服务器不可更改媒体类型(no-transform);Pragma 值表示客户端要求所有的中间服务器不返回缓存的资源。
首部字段Content-Length 值表示响应报文主体长度为2381 bytes;Content-Type 值表示响应报文主体的媒体类型为 text/html;Date 值表示创建响应报文的日期时间为"Fri, 08 May 2020 07:43:39 GMT";Etag 值表示请求资源实体的唯一标识为"588604c4-94d";Last-Modified 值表示请求资源的最后修改时间为"Mon, 23 Jan 2017 13:27:32 GMT";Server 值表示当前使用的HTTP服务器应用程序信息为”bfe/1.0.8.18“。
首部字段Set-Cookie 是用来管理客户端状态信息的,上例中的值表示Cookie名称及其值为”BDORZ=27315“(也是需要进行状态管理的请求报文中Cookie字段的值)、该Cookie的有效期为86400 秒(max-age=86400)、该Cookie的适用域名为".baidu.com"、该Cookie的适用目录为”/“。
看到这里,你应该也发现了,HTTP报文的首部字段还是比较多的。前面也提到,现在每个Web页面的平均资源数接近两百,每请求一个资源都要发送一个HTTP请求报文,然后收到一个HTTP响应报文,这些报文中包含大量臃肿的首部字段(每个报文的平均首部长度大概500字节,如果算上Cookie字段,首部字段长度可能达到几千字节),同一个Web页面的不同资源请求/响应报文的首部字段有很多是重复的,来回传输这些重复臃肿的首部字段自然严重影响网络利用率。
借鉴前面介绍的TCP持久连接技术提高网络利用率的思路,可以让不同资源请求共用一个TCP连接,也可以让不同报文共用相同的首部字段,只传递不同的首部字段(甚至客户端与服务器端都维护一份首部字段索引列表,只通过HTTP报文传递不同的首部字段索引号),可以将这种减少首部字段数据量的技术称为首部压缩,HTTP/2协议便引入了首部压缩技术HPACK,进一步提高网络利用率,限于篇幅在后面的博文:HTTP/2原理详解中进行详细介绍。
HTTP/1.1 协议默认是以明文方式传输数据的,这就有可能存在如下风险:
我们要保证个人或组织的信息安全,也应该从这三方面着手想办法,比如将明文加密传输,即便密文被截获,对方没有对应的密钥也没法解读出有效信息(可参考密码学博客:对称加密与非对称加密);要避免信息被篡改,可以将信息通过哈希算法生成一个消息认证码(对消息的任何改动都很敏感),明文信息与对应的消息认证码一同加密后传输,对方解密后计算有效信息的消息认证码,并将计算结果与收到的消息认证码比对,若二者不一致则说明信息已被篡改(可以参考博客:哈希算法能用来干啥?);验证通信方的身份可以通过数字签名和第三方可信证书来确认。
HTTP 协议中没有加密机制,但可以通过和 SSL(Secure Socket Layer)或 TLS(Transport Layer Security)的组合使用,加密 HTTP 的通信内容,用 SSL / TLS 建立安全通信线路之后,就可以在这条线路上进行 HTTP通信了。SSL / TLS 不仅提供加密处理,而且还使用证书(由值得信任的第三方机构颁发,用以证明服务器和客户端是实际存在的)来验证通信方的身份,使用哈希摘要算法(比如SHA-256、SHA-3等)来证明报文的完整性。与 SSL / TLS 组合使用的 HTTP 被称为 HTTPS(HTTPSecure),HTTPS 相当于身披 SSL / TLS 外壳的 HTTP,限于篇幅在下一篇博客中详细介绍 TLS 协议加密原理 和 TLS 协议握手过程。
从TLS协议加密原理和握手过程可以了解到,HTTPS协议主要通过认证加密来保证通信的机密性和完整性,通过密钥协商方案获得认证加密所需的共享密钥,通过证书认证与数字签名算法来确认通信对端身份的真实合法性,看起来HTTPS对信息安全性的保护还是挺到位的。
随着现在搭建网站的成本越来越低,网民数量越来越多,网络攻击、窃取用户信息、网络诈骗等手段更加泛滥,同时计算机性能和加密算法效率的提升,又降低了加密认证对网络访问效率的影响,HTTPS 的应用快速普及。HTTP/2 更是被各大浏览器厂商默认强制使用TLS 网络安全协议,你如果留心会发现,我们日常访问的大多数网站都使用了HTTPS协议(可以通过网址URL 开头的https://判断,也可以通过浏览器地址输入框的安全锁标志判断)。
TLS 协议虽然可以保证信息的安全传输,也能验证客户端与服务器身份的真实合法性,也即可以保证网络访问设备的真实合法性,但无法确保使用这台设备的人就是被授权的用户。某些敏感信息比如个人银行账户登录转账等,银行服务端需要确认登录该账号的必须是账号拥有者本人,单纯确认用户经常使用的设备是不够的,如果手机丢失或者别人借用我们手机,也要能保证对方不能访问我们的银行账户等敏感信息,这就需要Web认证功能。
计算机本身无法判断坐在显示器前的使用者的身份,也无法确认网络的那头究竟有谁。可见,为了弄清究竟是谁在访问服务器,就得让对方的客户端自报家门。为确认访问用户本人是否真的具有访问系统的权限,就需要核对“登录者本人才知道的信息”、“登录者本人才会有的信息”等,核对的信息通常是指以下这些:
最常用的核对用户身份信息的方式是”用户名 — 密码“信息,这个密码可以是普通密码、动态令牌密码、生理信息密码等,这些信息是如何在客户端与服务器之间传输的呢?前面介绍过请求报文首部中的Authorization、Proxy-Authorization 字段和响应报文首部中的Proxy-Authenticate、WWW-Authenticate 字段吗?这几个字段就是用来传输用户身份认证信息的。
用户名和密码等用户身份认证信息是比较敏感,需要严格保密的,假如这些信息被别人获知,就可以凭借这些信息通过用户的身份认证,计算机会默认是出自本人的行为,因此掌控机密信息的密码绝不能让他人得到,更不能轻易地就被破解出来。既然需要用户身份认证信息需要严格保密,就需要对这些敏感信息进行加密传输,而不能使用明文传输,根据加密处理方式不同,可以将用户认证分为如下几种类型:
HTTPS协议可以保证信息的安全传输,同时能通过证书和签名验证客户端与服务器身份的真实合法性,协议本身不能验证访问用户身份的问题可以通过基于表单的认证方式核对登录者本人才知道或才会有的信息解决,借助Cookie 进行登录/认证状态管理,还可以让我们访问同一域名时免去次次都需要登录认证的麻烦,为用户授权访问提供了极大的便利。
HTTPS 协议虽然提高了通信的安全性,但降低了网络访问效率,申请数字证书也是有成本的,所以早期除了对通信安全比较敏感的网站(比如跟网购、支付相关的)使用HTTPS协议外,更多的网站使用普通的HTTP 协议进行通信,毕竟HTTP 协议有更低的成本和更高的网络访问效率。
随着计算机性能的提升和大家对网络安全的重视,大多数网站都更新为HTTPS 协议,但有些超链接更新比较滞后,用户输入网址可能也习惯输入”http://“,为了保证向前兼容不至于影响用户的正常访问,网站就需要对HTTP 请求进行重定向,借助301状态码的响应报文将"http://"请求URL 重定向到 ”https://“请求URL。这种将HTTP请求重定向到HTTPS请求的方案比较简单,能解决前向兼容问题,但首次发送的是HTTP请求报文,明文传输的HTTP重定向报文很容易被劫持并篡改重定向URL,存在被攻击的风险:
要解决HTTP请求劫持问题,很容易想到只要在请求过程中不出现HTTP请求,直接以HTTPS请求进行通信,就可以避免上面的攻击。HSTS(HTTP Strict Transport Security)便是按照上述逻辑实现的Web安全策略,HSTS规范于2012年由IETF公布为RFC6797。网站采用 HSTS 后,用户访问时无需手动在地址栏中输入 HTTPS,浏览器会自动采用 HTTPS 访问网站地址,从而保证用户始终访问到网站的加密链接,保护数据传输安全。
HSTS最为核心的是一个HTTP响应头(HTTP Response Header),正是它可以让浏览器得知,在接下来的一段时间内(由max-age参数定义),当前域名只能通过HTTPS进行访问,并且在浏览器发现当前连接不安全的情况下,强制拒绝用户的后续访问要求。HSTS Header的语法如下:
/* 此响应头只有在 https 访问返回时才生效,其中[ ]中的参数表示可选;
* max-age是必选参数,是一个以秒为单位的数值,它代表着HSTS Header的过期时间,通常设置为1年,即31536000秒;
* includeSubDomains是可选参数,如果包含它,则意味着当前域名及其子域名均开启HSTS保护;
* preload是可选参数,只有当你申请将自己的域名加入到浏览器内置列表的时候才需要使用到它。
*/
Strict-Transport-Security: <max-age=>[; includeSubDomains][; preload]
HSTS完整流程如下(以max-age有效期1年、当前域名及其子域名均开启HSTS保护为例,下图取自博文:HSTS详解):
只要是在有效期内,浏览器都将直接强制性的发起HTTPS请求,假如有效期过了怎么办呢?因为HSTS Header存在于每个响应中,随着用户和网站的交互,这个有效时间时刻都在刷新,再加上有效期通常都被设置成了1年,所以只要用户的前后两次请求之间的时间间隔没有超过1年,则基本上不会出现安全风险。就算超过了有效期,但是只要用户和网站再进行一次新的交互,用户的浏览器又将开启有效期为1年的HSTS保护。
细心的你可能发现了,HSTS存在一个比较薄弱的环节,那就是浏览器没有当前网站的HSTS信息的时候,或者第一次访问网站的时候,依然需要一次明文的HTTP请求和重定向才能切换到HTTPS,以及刷新HSTS信息。而就是这么一瞬间却给攻击者留下了可乘之机,使得他们可以把这一次的HTTP请求劫持下来,继续中间人攻击。
针对这种攻击,HSTS也有应对办法,那就是在浏览器里内置一个预加载列表HSTS Preload List,只要是在这个预加载列表里的域名,无论何时、何种情况,浏览器都只使用HTTPS发起连接。这个预加载列表由Google Chromium维护(FireFox、Safari、Edge等主流浏览器均在使用),你的网站如果满足相应条件(具体条件可参考网址:https://hstspreload.org/)可以通过HSTS Header 的preload 参数申请将网站域名加入到HSTS Preload List中,审核通过后你的网站就成功加入到预加载列表中了,以后访问你的网站及其所有子网站都强制使用HTTPS发起请求,让HTTP请求劫持攻击无处下手。
下面使用Microsoft Chromium Edge浏览器自带的Network工具(设置及其他 --> 更多工具 --> 开发人员工具 --> 网络)查看https报文的标头字段信息,可以在响应标头中看到strict-transport-security字段的信息如下: