网络协议之HTTP

105 阅读1小时+
TCP/IP 网络分层模型说明OSI 网络分层模型说明
应用层 (application layer)由于下面的三层把基础打得非常好,所以在这一层就百花齐放了,有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等等,当然还有我们的 HTTP。传输单位则是消息或报文(message)。应用层 (application layer)面向具体的应用传输数据。
--表示层(presentation layer)把数据转换为合适、可理解的语法和语义。
--会话层(session layer)维护网络中的连接状态,即保持会话和同步。
传输层(transport layer)这个层次协议的职责是保证数据在 IP 地址标记的两点之间可靠地传输,是 TCP 协议工作的层次,另外还有它的一个小伙伴 UDP。传输单位是段(segment)。传输层(transport layer)相当于 TCP/IP 里的传输层。
网际层(internet layer)IP 协议就处在这一层。因为 IP 协议定义了 IP 地址 的概念,所以就可以在 链接层 的基础上,用 IP 地址取代 MAC 地址 ,把许许多多的局域网、广域网连接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再「翻译」成 MAC 地址就可以了。传输单位是包(packet)。网络层(network layer)相当于 TCP/IP 里的网际层。
链接层 (link layer/MAC)负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备 ,所以有时候也叫 MAC 层。传输单位是帧(frame)。数据链路层(data link layer)相当于 TCP/IP 的链接层。
物理层(physical layer)网络的物理形式,例如电缆、光纤、网卡、集线器等等。

HTTP 是什么?

超文本传输协议(HTTP)是一个用于传输超媒体文档(例如 HTML)的应用层协议。

它是为 Web 浏览器与 Web 服务器之间的通信而设计的,但也可以用于其他目的。

HTTP 遵循经典的客户端-服务端模型,客户端打开一个连接以发出请求,然后等待直到收到服务器端响应。

HTTP 是无状态协议,这意味着服务器不会在两个请求之间保留任何数据(状态)。

HTTP报文

HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:

  1. 起始行(start line):描述请求或响应的基本信息;
  2. 头部字段集合(header):使用 key-value 形式更详细地说明报文;
  3. 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

请求报文

请求行(request line)

请求报文的起始行叫做请求行(request line),它简要地描述了 客户端想要如何操作服务器端的资源

请求方法(Method -> GET/POST)+ 请求目标(Path -> URI)+ 版本号(Version of the protocol -> HTTP 1.0 / 1.1 / 2.0)

这三个部分通常使用空格(space)来分隔,最后要用 CRLF 换行表示结束。

请求方法

目前 HTTP/1.1 规定了八种方法,单词 都必须是大写的形式 ,我先简单地列把它们列出来,后面再详细讲解。

  1. GET:它的含义是请求 从服务器获取资源
  2. HEAD:获取资源的元信息(响应头);
  3. POST:向服务器提交数据,相当于写入或上传数据;
  4. PUT:向服务器提交数据,多用于更新数据;
  5. DELETE:删除资源(基本不用);
  6. CONNECT:要求服务器为客户端和另一台远程服务器建立特殊的连接隧道;
  7. OPTIONS:列出可对资源实行的操作方法,在响应头的 Allow 字段里返回
  8. TRACE:追踪请求 - 响应的传输路径。

GET 和 POST 的区别

作用

GET 用于获取资源,而 POST 用于传输实体主体。

参数

GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。

因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。

http://www.baidu.com/?百度一下,你就知道
http://www.baidu.com/?%E7%99%BE%E5%BA%A6%E4%B8%80%E4%B8%8B%EF%BC%8C%E4%BD%A0%E5%B0%B1%E7%9F%A5%E9%81%93

安全

在 HTTP 协议里,所谓的 安全 是指请求方法不会「破坏」服务器上的资源,即不会对服务器上的资源造成实质的修改。

GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。

幂等性

所谓的 幂等(Idempotent) 实际上是一个数学用语,被借用到了 HTTP 协议里,意思是多次执行相同的操作,结果也都是相同的,即多次幂后结果相等。

GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:

GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1

POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录:

POST /add_row HTTP/1.1   -> Adds a 1nd row
POST /add_row HTTP/1.1   -> Adds a 2nd row
POST /add_row HTTP/1.1   -> Adds a 3rd row

缓存

GET是可缓存的,POST不可缓存。

XMLHttpRequest

XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。

  • 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。
  • 而 GET 方法 Header 和 Data 会一起发送。

URI

URI(Uniform Resource Identifier) 本质上是一个字符串,这个字符串的作用是 唯一地标记资源的位置或者名字

  • scheme 资源应该使用哪种协议 httphttps。后面必须是 三个特定的字符 ://
  • user:passwd@ 身份信息 表示登录主机时的用户名和密码,以明文形式暴露出来,不推荐
  • authority 资源所在的主机名 通常的形式是 host:port ,端口可省略。
  • path 标记资源所在位置 。URI 的 path 部分必须以 / 开始,也就是必须包含 /
  • query 查询参数,多个 key = value 的字符串,这些 KV 值用字符 & 连接。
  • #fragment 片段标识符 它是 URI 所定位的资源内部的一个 锚点 ,浏览器在获取资源后 跳转到它指示的位置
https://search.jd.com/Search?keyword=openresty&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=openresty&psort=3&click=0
// scheme -> https , authority -> search.jd.com , path -> /Search... , query -> keyword=openresty...

URI只能支持ASCII,对于ASCII 码以外的字符集和特殊字符,URI通过百分号编码( Percent Encoding)进行转义,转义规则是直接把非 ASCII 码或特殊字符转换成十六进制字节值 ,然后前面再加上一个 %

http://www.baidu.com/?百度一下,你就知道
http://www.baidu.com/?%E7%99%BE%E5%BA%A6%E4%B8%80%E4%B8%8B%EF%BC%8C%E4%BD%A0%E5%B0%B1%E7%9F%A5%E9%81%93

响应报文

状态行(status line)

响应报文的起始行叫做状态行(status line),意思是 服务器响应的状态

版本号(Version of the protocol -> HTTP 1.0 / 1.1 / 2.0)+ 状态码(Status code -> 200 / 404)+ 原因(Status message -> 数字状态码补充)

这三个部分同样使用空格(space)来分隔,最后要用 CRLF 换行表示结束。

状态码

状态码类别含义
1XXInformational(信息性状态码)接收的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错

1XX 信息

  • 100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。

2XX 成功

  • 200 OK :是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
  • 204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
  • 206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。

3XX 重定向

  • 301 Moved Permanently永久重定向,含义是此次请求的资源已经不存在了,需改用新的 URI 再次访问。

  • 302 Found临时重定向,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。

    301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URI,浏览器会重定向到新的 URI

  • 303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。

  • 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。

  • 304 Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。

    • 这个是表示向服务器发起了请求,但是服务器响应该文件没有变化,没有传回数据内容,使用浏览器的缓存。
  • 307 Temporary Redirect :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。

4XX 客户端错误

  • 400 Bad Request :请求报文中存在语法错误。
  • 401 Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
  • 403 Forbidden :请求被拒绝。
  • 404 Not Found :表示请求的资源在本服务器上未找到,所以无法提供给客户端。

剩下的错误代码较明确地说明了错误的原因:

  • 405 Method Not Allowed:不允许使用某些方法操作资源,例如不允许 POST 只能 GET;
  • 406 Not Acceptable:资源无法满足客户端请求的条件,例如请求中文但只有英文;
  • 408 Request Timeout:请求超时,服务器等待了过长的时间;
  • 409 Conflict:多个请求发生了冲突,可以理解为多线程并发时的竞态;
  • 413 Request Entity Too Large:请求报文里的 body 太大;
  • 414 Request-URI Too Long:请求行里的 URI 太大;
  • 429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略;
  • 431 Request Header Fields Too Large:请求头某个字段或总体太大;

5XX 服务器错误

  • 500 Internal Server Error :服务器正在执行请求时发生错误。
  • 501 Not Implemented :表示客户端请求的功能还不支持。
  • 502 Bad Gateway:通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。
  • 503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

Headers

请求行 + 头部字段 = 请求头 ; 状态行 + 头部字段 = 响应头。

请求头和响应头的结构是基本一样的,唯一的区别是起始行,所以请求头和响应头里的字段可以放在一起介绍。

头部字段是 key-value 的形式,keyvalue 之间用 : 分隔,最后用 CRLF 换行表示字段结束。

HTTP 头字段非常灵活,不仅可以使用标准里的 Host、Connection 等已有头,也可以 任意添加自定义头 ,这就给 HTTP 协议带来了无限的扩展可能。

不过使用头字段需要注意下面几点:

  1. 字段名不区分大小写,例如 Host 也可以写成 host ,但首字母大写的可读性更好;
  2. 字段名里不允许出现空格,可以使用连字符 - ,但不能使用下划线 _ 。例如,test-name 是合法的字段名,而 test nametest_name 是不正确的字段名;
  3. 字段名后面必须紧接着 :,不能有空格,而 : 后的字段值前可以有多个空格;
  4. 字段的顺序是没有意义的,可以任意排列不影响语义;
  5. 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie

通用头部字段

头部字段名说明
Cache-Control控制缓存的行为
Connection控制不再转发给代理的首部字段、管理持久连接
Date创建报文的日期时间
Pragma报文指令
Trailer报文末端的首部一览
Transfer-Encoding指定报文主体的传输编码方式
Upgrade升级为其他协议
Via代理服务器的相关信息
Warning错误通知

请求头部字段

头部字段名说明
Accept用户代理可处理的媒体类型
Accept-Charset优先的字符集
Accept-Encoding优先的内容编码
Accept-Language优先的语言(自然语言)
AuthorizationWeb 认证信息
Expect期待服务器的特定行为
From用户的电子邮箱地址
Host请求资源所在服务器
If-Match比较实体标记(ETag)
If-Modified-Since比较资源的更新时间
If-None-Match比较实体标记(与 If-Match 相反)
If-Range资源未更新时发送实体 Byte 的范围请求
If-Unmodified-Since比较资源的更新时间(与 If-Modified-Since 相反)
Max-Forwards最大传输逐跳数
Proxy-Authorization代理服务器要求客户端的认证信息
Range实体的字节范围请求
Referer对请求中 URI 的原始获取方
TE传输编码的优先级
User-AgentHTTP 客户端程序的信息

响应头部字段

头部字段名说明
Accept-Ranges是否接受字节范围请求
Age推算资源创建经过时间
ETag资源的匹配信息
Location令客户端重定向至指定 URI
Proxy-Authenticate代理服务器对客户端的认证信息
Retry-After对再次发起请求的时机要求
ServerHTTP 服务器的安装信息
Vary代理服务器缓存的管理信息
WWW-Authenticate服务器对客户端的认证信息

实体头部字段

头部字段名说明
Allow资源可支持的 HTTP 方法
Content-Encoding实体主体适用的编码方式
Content-Language实体主体的自然语言
Content-Length实体主体的大小
Content-Location替代对应资源的 URI
Content-MD5实体主体的报文摘要
Content-Range实体主体的位置范围
Content-Type实体主体的媒体类型
Expires实体主体过期的日期时间
Last-Modified资源的最后修改日期时间

空行(CRLF)

空行,也就是 「CRLF」,十六进制的「0D0A」。

entity/body

实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。与 header 对应,被称为 body 。

  1. 数据类型表示实体数据的内容是什么,使用的是 MIME type,相关的头字段是 Accept 和 Content-Type;
  2. 数据编码表示实体数据的压缩方式,相关的头字段是 Accept-Encoding 和 Content-Encoding;
  3. 语言类型表示实体数据的自然语言,相关的头字段是 Accept-Language 和 Content-Language(通常不会发送);
  4. 字符集表示实体数据的编码方式,相关的头字段是 Accept-Charset(通常不会发送) 和 Content-Type;

MIME type

多用途互联网邮件扩展(Multipurpose Internet Mail Extensions),简称为 MIME。HTTP 取了其中的一部分,用来标记 body 的数据类型 ,这就是我们平常总能听到的 MIME type

常用的MIME type

  1. text:即文本格式的可读数据,text/html(超文本文档)、text/plain(纯文本)、text/css(样式表) 等。
  2. image:即图像文件,有 image/gifimage/jpegimage/png 等。
  3. audio/video:音频和视频数据,例如 audio/mpegvideo/mp4 等。
  4. application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/jsonapplication/javascriptapplication/pdf 等,另外,如果实在是不知道数据是什么类型,像刚才说的黑盒,就会是 application/octet-stream即不透明的二进制数据
Accept: text/html,application/xml,image/webp,image/png //客户端可接收类型,","分隔符列出多个类型。
Content-Type: text/html //实体数据的真实类型。 

Encoding type

HTTP 在传输时为了节约带宽,有时候还会 压缩数据 ,通过Encoding type解压缩。

常用的Encoding type只有下面三种:

  1. gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;
  2. deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
  3. br:一种专门为 HTTP 优化的新压缩算法(Brotli)。
Accept-Encoding: gzip, deflate, br //客户端支持的压缩格式,如果没有就表示客户端不支持压缩数据。
Content-Encoding: gzip //实体数据使用的压缩格式,如果没有就表示相应数据没被压缩。

语言类型和字符集

为了解决语言文字国际化的问题,又引入了两个概念:语言类型字符集

Accept-Language: zh-CN, zh, en //请求头字段,表示客户端可理解的自然语言,用`,` 做分隔符列出多个类型。
Content-Language: zh-CN //实体头字段,告诉客户端实体数据使用的语言类型。(不会发送)

Accept-Charset: gbk, utf-8 //请求头字段,表示浏览器请求的字符集。(不会发送)
Content-Type: text/html; charset=utf-8 //通用头字段里面包含服务器返回的实体数据字符集。

浏览器都支持多种字符集,通常不会发送 Accept-Charset,而服务器也不会发送 Content-Language,因为使用的语言完全可以由字符集推断出来,所以在请求头里一般只会有 Accept-Language 字段,响应头里只会有 Content-Type 字段。

内容协商的质量值和结果

Accept: text/html,application/xml;q=0.9,*/*;q=0.8
Vary: Accept-Encoding,User-Agent,Accept

在 HTTP 协议里用 Accept、Accept-Encoding、Accept-Language 等请求头字段进行内容协商的时候,还可以用一种特殊的 q 参数表示权重来设定优先级,这里的 qquality factor的意思。

权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个 ; ,然后是 q=value

这里要提醒的是 ; 的用法,在大多数编程语言里 ; 的断句语气要强于 , ,而在 HTTP 的内容协商里却恰好反了过来,; 的意义是小于 , 的。

这个 Vary 字段表示服务器依据了 Accept-Encoding、User-Agent 和 Accept 这三个头字段,然后决定了发回的响应报文。

Vary 字段可以认为是响应报文的一个特殊的 版本标记 。每当 Accept 等请求头变化时,Vary 也会随着响应报文一起变化。也就是说,同一个 URI 可能会有多个不同的「版本」,主要用在传输链路中间的代理服务器实现缓存服务 ,这个之后讲 HTTP 缓存 时还会再提到。

HTTP 传输大文件的方法

数据压缩

Accept-Encoding: gzip, deflate, br //客户端支持的压缩格式,如果没有就表示客户端不支持压缩数据。
Content-Encoding: gzip //实体数据使用的压缩格式,如果没有就表示相应数据没被压缩。

分块传输

Transfer-Encoding: chunked //通用头字段,body 分成了许多的块(chunk)逐个发送。

Transfer-Encoding: chunkedContent-Length 这两个字段是 互斥的 ,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked)。

  1. 每个分块包含两个部分,长度头和数据块;
  2. 长度头是以 CRLF(回车换行,即 \r\n )结尾的一行明文,用 16 进制数字表示长度;
  3. 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF
  4. 最后用一个长度为 0 的块表示结束,即 0\r\n\r\n
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n //length 是以十六进制的形式表示。\r\n 是 CRLF
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n

范围请求(range requests)

在视频中拖动进度条,这实际上是想获取一个大文件其中的片段数据 ,而分块传输并没有这个能力。

HTTP 协议为了满足这样的需求,提出了 范围请求 (range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分 。

Accept-Ranges: none //不支持范围请求
Accept-Ranges: bytes = x - y //支持范围请求, x 和 y 是字节为单位的数据范围,表示的是偏移量。

//假设文件是100字节
Accept-Ranges: bytes = 0- //表示从文档起点到文档终点,相当于 0-99 ,即整个文件;
Accept-Ranges: bytes = 10- //是从第 10 个字节开始到文档末尾,相当于 10-99;
Accept-Ranges: bytes = -1 //是文档最后一个字节,相当于 99-99;
Accept-Ranges: bytes = -10 //是从文档末尾倒数 10 个字节,相当于 90-99。    

服务器收到 Range 字段后,需要做四件事:

  1. 检查范围是否合法。范围越界返回状态码 416
  2. 范围正确,根据 Range 头计算偏移量,读取文件片段。返回状态码 206
  3. 服务器添加响应头字段Content-Range 。格式是bytes x-y/length
  4. 发送数据。
HTTP/1.1 206 Partial Content
Date: Wed, 15 Nov 2015 06:25:24 GMT
Last-Modified: Wed, 15 Nov 2015 04:58:08 GMT
Content-Range: bytes 21010-47021/47022
Content-Length: 26012
Content-Type: image/gif

... 26012 bytes of partial image data ...

多段数据

Range: bytes=0-9, 20-29, 30-39 //请求头

响应头使用的MIMEmultipart/byteranges,还有个参数boundary=xxx是用来分隔字节序列的。

每个字节序列还需要用 Content-TypeContent-Range 标记这段数据的类型和所在范围。

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00001111
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
 
 
--00001111
Content-Type: text/plain
Content-Range: bytes 0-9/96
 
// this is
--00001111
Content-Type: text/plain
Content-Range: bytes 20-29/96
 
ext json d
--00001111--

HTTP连接管理

短连接与长连接

当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问的 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。

长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。

  • 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close
  • 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 Connection : Keep-Alive

队头阻塞(Head-of-line blocking)

队头阻塞与短连接和长连接无关 ,而是由 HTTP 基本的 请求 - 应答 模型所导致的。

因为 HTTP 规定报文必须是 一发一收 ,这就形成了一个先进先出的 串行队列 。队列里的请求没有优先级,只有入队的先后顺序,排在最前面的请求被优先处理。如果队首的请求处理的慢,后面的所有请求就要一直等待。这就导致队头阻塞。

性能优化

并发连接(concurrent connections)

同时对一个域名发起多个长连接,用数量来解决质量的问题 。但这种方式也存在缺陷。如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成拒绝服务。

HTTP 协议建议客户端使用并发,但不能「滥用」并发。

域名分片(domain sharding)

HTTP 协议和浏览器不是限制并发连接数量吗?好,那我就多开几个域名,比如 shard1.chrono.comshard2.chrono.com,而这些域名都指向同一台服务器 www.chrono.com ,这样实际长连接的数量就又上去了,也就解决了队头阻塞问题。

HTTP 的重定向和跳转

https://im.qq.com/download/

QQ 主页点一下 **下载 **连接,会发生什么呢?

浏览器解析文字里的 URI -> 用这个 URI 发起一个新的 HTTP 请求 -> 获得响应报文,渲染出新 URI 指向的页面。

这样由浏览器使用者发起的跳转叫 主动跳转,由服务器发起的跳转,被叫做 重定向(Redirection)。

Location:https://im.qq.com/download/ //绝对URI。
Location:/index.html  //相对URI,Location是响应头部字段。

重定向的应用场景

  • 资源不可用,需要用另一个新的URI来代替。例如域名变更、服务器变更、网站改版、系统维护等。
  • 避免重复,让多个网址都跳到一个URI,增加访问入口不会增加额外的工作量。例如,有的网站都会申请多个名称类似的域名,然后把它们再重定向到主站上。

重定向的相关问题

  • 性能消耗 两次 请求-应答,比正常访问多一次。适当使用,不能滥用。

  • 循环跳转 如果重定向的策略设置欠考虑,可能会出现 A=>B=>C=>A 的无限循环。浏览器必须具有检测 循环跳转 的能力,在发现这种情况时应当停止发送请求并给出错误提示。

HTTP Cookie

HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。

Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。

Cookie 的工作过程

  1. 客户端发送 HTTP 请求到服务器
  2. 当服务器收到 HTTP 请求时,在响应头里面添加一个 Set-Cookie 字段
  3. 浏览器收到响应后保存下 Cookie
  4. 之后对该服务器每一次请求中都通过 Cookie 字段将 Cookie 信息发送给服务器。

Cookie 的属性

Cookie: name=value; name2=value2; name3=value3 //请求头
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly...

Cookie 的生命周期

Set-Cookie:Expires=Wed, 21 Oct 2015 07:28:00 GMT; Max-Age=10; //Max-Age优先级更高
  • Expires 俗称 过期时间,用的是 绝对时间点 ,可以理解为 截止日期(deadline)。
  • Max-Age 用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。

Cookie 的作用域

Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。

Set-Cookie: Domain = mozilla.org;

Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:

/docs
/docs/Web/
/docs/Web/HTTP

大多数情况只用一个/或者直接省略,表示域名下任意路径都允许使用Cookie。

Cookie 的安全性

  • HttpOnly Cookie 只能通过 HTTP 协议传输,禁止其他访问方式。浏览器会禁用document.cookie 等一切相关的 API。这样就会阻止 跨站脚本(XSS)
  • SameSite 可以设置成Strict / Lax / None分别代表严格限定Cookie跨站 / 允许GET/HEAD跨站 / 不限制。可以防范跨站请求伪造(XSRF)攻击
  • Secure 表示这个 Cookie 仅能用 HTTPS 协议加密传输 ,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。
    • **注意:**非安全站点(http:)已经不能再在 cookie 中设置 secure 指令了(在Chrome 52+ and Firefox 52+ 中新引入的限制)。

Cookie 的应用

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)。
  • 个性化设置(如用户自定义设置、主题等)。
  • 浏览器行为跟踪(如跟踪分析用户行为等)。

Cookie缺点

  • 长度限制 只有4KB。
  • 安全问题 由于Cookie明文传递,很容易被拦截、篡改,拦截之后会暴露session信息。
  • 额外开销 Cookie 在每次发起 HTTP 请求的时候都会被发送给服务器,会增加开销。
  • 某些客户端不支持 cookie 。

HTTP 的缓存控制

**缓存(cache)**是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。

强缓存

不需要发送请求到服务端,直接读取浏览器本地缓存,在 Chrome 的 Network 中显示的 HTTP 状态码是 200 ,在 Chrome 中,强缓存又分为 Disk Cache(存放在硬盘中)和 Memory Cache(存放在内存中),存放的位置是由浏览器控制的。是否强缓存由 ExpiresCache-ControlPragma 3 个 Header 属性共同来控制。

Pragma

Pragma:no-cache; //禁用缓存,只用于HTTP1.0的情况。响应头字段不支持这个属性。

Expires

Expires所定义的缓存时间是相对服务器上的时间而言的,其定义的是资源“失效时刻”,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。

Expires:Wed, 21 Oct 2015 07:28:00 GMT;//http1.0 指定缓存的过期时间。实体头字段。

Cache-Control

针对上述的“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,HTTP1.1新增了 Cache-Control 来定义缓存过期时间。

//可缓存性
Cache-Control:no-cache;//相当于`max-age=0,must-revalidate`即资源被缓存,但是缓存立刻过期。同时下次访问时强制验证资源有效性。
Cache-Control:no-store;//请求和响应都不缓存。通用头字段。
Cache-Control:public;//表明响应可以被发送请求的客户端,代理服务器等缓存。响应头字段。
Cache-Control:private;//表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。响应头字段。
//到期
Cache-Control:max-age=<seconds>;//缓存资源,但是在指定时间(单位为秒)后过期。时间是相对于请求的时间。通用头字段。 
Cache-Control:s-maxage=<seconds>;//覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。响应头字段。
Cache-Control:max-stale[=<seconds>];//指定时间内,即使缓存过期,资源依然有效。请求头字段。
Cache-Control:min-fresh=<seconds>;//缓存的资源至少要保持指定时间的新鲜期。请求头字段。
//重新验证和重新加载
Cache-Control:must-revalidate;//一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。响应头字段。
Cache-Control:proxy-revalidate;//与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。响应头字段。
//其他
Cache-Control:no-transform;//强制要求代理服务器不要对资源进行转换,禁止代理服务器对 Content-Encoding、Content-Range、Content-Type 字段的修改(因此代理的 gzip 压缩将不被允许)。请求头字段。
Cache-Control:only-if-cached;//仅仅返回已经缓存的资源,不访问网络,若无缓存则返回504。请求头字段。

假设所请求资源于4月5日缓存, 且在4月12日过期。

max-agemax-stalemin-fresh 同时使用时, 它们的设置相互之间独立生效, 最为保守的缓存策略总是有效。

这意味着,如果max-age = 10 daysmax-stale = 2 daysmin-fresh = 3 days, 那么:

  • 根据max-age的设置,覆盖原缓存周期,缓存资源将在4月15日失效( 5 + 10 = 15);

  • 根据max-stale的设置, 缓存过期后两天依然有效,此时响应将返回110(Response is stale)状态码, 缓存资源将在4月14日失效( 12 + 2 = 14);

  • 根据min-fresh的设置,至少要留有3天的新鲜期,缓存资源将在4月9日失效( 12 - 3 = 9);

由于客户端总是采用最保守的缓存策略, 因此, 4月9日后,对于该资源的请求将重新向服务器发起验证。

协商缓存

当客户端上某个资源保存的缓存时间过期了,但这时候服务器并没有更新过这个资源,如果这个资源很大,我们有必要再把这个资源重新发一遍吗?

答案是否定的。为了让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,HTTP1.1新增了几个首部字段来做这件事情。

Last-Modified / If-Modified-Since / If-Unmodified-Since

  1. 当客户端发送请求后,服务器端会将资源最后的修改时间以Last-Modifie:GMT的形式加在实体头字段上一起返回给客户端。
  2. 客户端为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查。
    • 若传递的时间与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回状态码304内容为空,这样就节省了带宽和时间。
    • 如果两个时间不一致,则服务器会发回该资源并返回状态码200,和第一次请求类似。

Last-Modified用于标记请求资源的最后一次修改时间, 格式为GMT(格林尼治标准时间)。

If-Modified-Since缓存校验字段, 其值为上次响应头的Last-Modified值,若与请求资源当前的Last-Modified值相同,那么将返回304状态码的响应,反之, 将返回200状态码响应。请求头字段。最常见的应用场景:

  • 最常见的应用场景是来更新没有特定 ETag 标签的缓存实体。

If-Unmodified-Since 缓存校验字段,其值为上次响应头的Last-Modified值,表示资源在指定的时间之后未修改则正常执行更新,否则返回412(Precondition Failed)状态码的响应。请求头字段。常用于如下两种场景:

  • 不安全的请求,比如说使用POST请求更新Wiki文档,文档未修改时才执行更新。
  • If-Range 字段同时使用时, 可以用来保证新的片段请求来自一个未修改的文档。
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT;//精确度比ETag低,备用机制。HTTP1.0。
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT;//当与 If-None-Match 一同出现时,它(If-Modified-Since)会被忽略掉。
If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT;

Last-Modified存在的问题:

  1. Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度;

  2. 某些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),但 Last-Modified 却改变了,导致文件没法使用缓存;

  3. 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形。

ETag / If-None-Match / If-Match

为了解决上述Last-Modified可能存在的的问题,HTTP1.1还推出了 ETag 实体头字段。

服务器通过算法,给资源计算得出一个唯一标识符,在把资源响应给客户端的时候,会在实体头字段加上ETag:唯一标识符一起返回给客户端。例如:

ETag: "x234dff";

客户端会保留该ETag字段,并在下一次请求时将其一并带过去给服务器。服务器只要对比客户端(作为If-None-Match字段的值一起发送)传来的ETag和自己服务器上该资源的ETag是否一致,就能很好的判断资源相对客户端而言是否被修改过了。

  • 如果服务器发现ETag匹配不上,那么直接以常规GET200回包形式将新资源(当然也包括了新的ETag)发给客户端。

  • 如果ETag一致,则直接返回304知会客户端直接使用本地缓存即可。

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"//强缓存 ETag 要求资源在字节级别必须完全相符。
ETag: W/"0815"//弱ETag 只要求资源在语义上没变化,但内部可能会发生了变化(HTML里的标签顺序调整,多出了几个空格)。

If-None-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"//请求头字段。
If-None-Match: W/"67ab43", "54ed21", "7892dd"
If-None-Match: * //星号是一个特殊值,可以代表任意资源。它只用在进行资源上传时,通常是采用 PUT 方法,来检测拥有相同识别ID的资源是否已经上传过了。
    
If-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"//请求头字段。
If-Match: W/"67ab43", "54ed21", "7892dd"
If-Match: * //星号指代任意资源。

If-None-Match

常用于判断缓存资源是否有效

  • 在请求方法为 GET 和 HEAD 的情况下

    • ETag列表不匹配,服务器返回状态码200的响应。
    • ETag列表匹配,服务器返回状态码304的响应。
  • 其他方法,尤其是PUT,将If-None-Match used 的值设置为 *,用来生成事先并不知道是否存在的文件,可以确保先前并没有进行过类似的上传操作,防止之前操作数据的丢失。

当与 If-Modified-Since 一同使用的时候,If-None-Match 优先级更高(假如服务器支持的话)。

If-Match

常用于判断条件是否满足

  • 对于 GETHEAD 方法,搭配 Range头字段 使用,可以用来保证新请求的范围与之前请求的范围是对同一份资源的请求。如果 ETag 无法匹配,那么需要返回 416 (Range Not Satisfiable,范围请求无法满足) 响应。
  • 对于其他方法来说,尤其是 PUT, If-Match 头字段可以用来避免更新丢失问题。它可以用来检测用户想要上传的不会覆盖获取原始资源之后做出的更新。如果请求的条件不满足,那么需要返回 412 (Precondition Failed,先决条件失败) 响应。

缓存头部对比

头部优势和特点劣势和问题
Expires1、HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。 2、以时刻标识失效时间。1、时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。 2、存在版本问题,到期之前的修改客户端是不可知的。
Cache-Control1、HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。 2、比Expires多了很多选项设置。1、HTTP 1.1 才有的内容,不适用于HTTP 1.0 。 2、存在版本问题,到期之前的修改客户端是不可知的。
Last-Modified1、不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。1、只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。 2、以时刻作为标识,无法识别一秒内进行多次修改的情况。 3、某些服务器不能精确的得到文件的最后修改时间。
ETag1、可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。 2、不存在版本问题,每次请求都回去服务器进行校验。1、计算ETag值需要性能损耗。 2、分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时发现ETag不匹配的情况。

缓存优先级

Pragma > Cache-Control > Expires

ETag > Last-Modified(同时存在的时候,Last-ModifiedETag 备份)。

浏览器可以在内存、硬盘中开辟一个空间用于保存请求资源副本。

请求一个资源时,会按照Service Worker -> Memory Cache -> Disk Cache -> Push Cache依次查找缓存。

200 from memory cache 表示不访问服务器,直接从内存中读取缓存。 储存较小的文件,关闭进程后缓存资源随之销毁。

200 from disk cache 表示不访问服务器,直接从硬盘中读取缓存。存储大文件,关闭进程后,缓存资源依然存在。

200 from prefetch cachepreload (预加载)或 prefetch (预先载入)的资源加载时,两者也是均存储在 http cache,当资源加载完成后,如果资源是可以被缓存的,那么其被存储在 http cache 中等待后续使用;如果资源不可被缓存,那么其在被使用前均存储在 memory cache

缓存请求流程

用户行为

  • 当使用F5刷新网页时,会跳过强缓存,但是会检查协商缓存。
  • 当使用Ctrl+F5 强制刷新网页时,直接从服务器下载新资源,跳过强缓存和协商缓存。

HTTP 的代理服务

代理在客户端和服务器原本的通信链路中插入的一个中间环节 ,也是服务器,但提供的是代理服务。

所谓的代理服务是指服务本身不产生内容,而是处于中间位置 转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。

这里主要说的是最常见的反向代理

代理的作用

  • 负载均衡:通过算法把外部的流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。
  • 健康检查:使用 心跳 等机制监控后端服务器,发现有故障就及时 踢出 集群,保证服务高可用;
  • 安全防护:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载;
  • 加密卸载:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本;
  • 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应;
  • 内容缓存:暂存、复用服务器响应,这个与 缓存控制密切相关,我们稍后再说。

代理相关头字段

Via 代理服务器用它标明代理的身份。Via 是一个通用头字段,请求头或响应头里都可以出现。

有多个代理节点的情况下,客户端发送请求和服务器返回响应via头字段顺序不一样:

Via 字段只解决了 客户端和源服务器判断是否存在代理的问题,还不能知道对方的真实信息

但服务器的 IP 地址应该是保密的,关系到企业的内网安全,所以一般不会让客户端知道。不过反过来,通常服务器需要知道客户端的真实 IP 地址,方便做访问控制、用户画像、统计分析

可惜的是 HTTP 标准里并没有为此定义头字段 ,但已经出现了很多 事实上的标准 ,最常用的两个头字段是 X-Forwarded-ForX-Real-IP

  • X-Forwarded-For:链式存储

    字面意思是为 谁而转发 ,形式上和 Via 差不多,也是每经过一个代理节点就会在字段里追加一个信息,但 Via 追加的是代理主机名(或者域名),而 X-Forwarded-For 追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就客户端的地址。

  • X-Real-IP:只有客户端 IP 地址

    是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息。

    如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。

代理协议

X-Forwarded-For缺点:

通过 X-Forwarded-For 操作代理信息 必须要解析 HTTP 报文头 ,会降低代理的转发性能

另一个问题是 X-Forwarded-For 等头 必须要修改原始报文 ,而有些情况下是不允许甚至不可能的(比如使用 HTTPS 通信被加密 )。

所以就出现了一个专门的 代理协议 (The PROXY protocol) ,它由知名的代理软件 HAProxy 所定义,也是一个 事实标准 ,被广泛采用(注意并不是 RFC)。

代理协议有 v1 和 v2 两个版本,v1 和 HTTP 差不多,也是明文,而 v2 是二进制格式。

v1版本在HTTP 报文前增加了一行 ASCII 码文本,这一行文本其实非常简单,开头必须是 PROXY 五个大写字母,然后是 TCP4 或者 TCP6 ,表示客户端的 IP 地址类型,再后面是请求方地址、应答方地址、请求方端口号、应答方端口号,最后用一个回车换行(\r\n)结束。

PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
GET / HTTP/1.1\r\n
Host: www.xxx.com\r\n
\r\n

你觉得代理有什么缺点?实际应用时如何避免?

代理的缺点是增加链路长度,会增加响应耗时,应尽量减少在代理商所做的的一些与业务无关的复杂耗时操作。

HTTP 的缓存代理

缓存控制 + 代理服务 = 缓存代理

加入了缓存后代理服务收到源服务器发来的响应数据后需要做两件事:

  • 第一个当然是把报文转发给客户端。
  • 而第二个就是把报文存入自己的 Cache 里。

下一次再有相同的请求,代理服务器就可以直接发送 304 或者缓存数据,不必再从源服务器那里获取。这样就降低了客户端的等待时间,同时节约了源服务器的网络带宽。

源服务器的缓存控制

数据是否允许代理缓存:

  • private:表示缓存只能在客户端保存,是用户 私有 的。
  • public:允许

缓存失效后重新验证:proxy-revalidate 代理缓存过期后必须验证,对应的是客户端的(must-revalidate)。

缓存的生存时间:s-maxage 限制在代理服务器上能缓存多久。

private, max-age = 5;
public, max-age = 5, s-maxage = 10;
max-age = 30, proxy-revalidate, no-transform;

客户端的缓存控制

only-if-cached

表示 只接受代理缓存的数据 ,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504(Gateway Timeout)。

Vary: Accept-Encoding

对于服务器而言,资源文件可能不止一个版本,比如说压缩和未压缩,针对不同的客户端,通常需要返回不同的资源版本。比如说老式的浏览器可能不支持解压缩,这个时候,就需要返回一个未压缩的版本; 对于新的浏览器,支持压缩,返回一个压缩的版本,有利于节省带宽,提升体验。 那么怎么区分这个版本呢, 这个时候就需要Vary了。

服务器通过指定Vary: Accept-Encoding, 告知代理服务器, 对于这个资源, 需要缓存两个版本: 压缩和未压缩。这样老式浏览器和新的浏览器,通过代理,就分别拿到了未压缩和压缩版本的资源,避免了都拿同一个资源的尴尬 。

Vary:Accept-Encoding,User-Agent;

Age 和 Date

出现此字段, 表示命中代理服务器的缓存。它指的是代理服务器对于请求资源的已缓存时间, 单位为秒。如下:

Age:2383321
Date:Wed, 08 Mar 2017 16:12:42 GMT

以上指的是,代理服务器在2017年3月8日16:12:42时向源服务器发起了对该资源的请求, 目前已缓存了该资源2383321秒。

Date 指的是响应生成的时间。 请求经过代理服务器时,返回的Date未必是最新的, 通常这个时候,代理服务器将增加一个Age字段告知该资源已缓存了多久。

缓存实践

静态资源

永远不会修改的内容:JS 和 CSS 文件,图片,和任何类型的二进制文件都属于这个类目。

永远,我确实说的是永远。为静态资源指定版本号是很通用的做法。它们无论什么时候改动了,它们的 URL 就改变了。

这里是一些针对静态资源的简单的规则:

  • 在文件或者路径中嵌入指纹。避免为指纹使用查询字符串。另外,确保生成的URL长度超过8个不同的字符。
<link rel = "Stylesheet" href="http://static.xxx.com/a_f02bc2.css">
<script src = "http://static.xxx.com/a_13cfa51.js"   
  • 使用这些 HTTP 头:
Cache-Control: public, max-age=31536000
Expires: (一年后的今天)
ETag: (基于内容生成)
Last-Modified: (过去某个时间)
Vary: Accept-Encoding

动态资源

针对应用程序私密性和新鲜度方面需求的不同,我们应该使用不同的缓存控制设置。

对于非私密性和经常性变动的资源(想像一下股票信息),我们应该使用下面这些:

Cache-Control: public, max-age=0 
Expires: (当前时间) 
ETag: (基于内容生成) 
Last-Modified: (过去某个时间) 
Vary: Accept-Encoding

这些设置的效果是:

  • 这些资源可以被公开地(通过浏览器和代理服务器)缓存起来。每一次在浏览器使用这些资源之前,浏览器或者代理服务器会检查这些资源是否有更新的版本,如果有,就把它们下载下来。
  • 这样的设置需要注意,浏览器在重新检查资源时效性方面有一定的灵活性。典型的是,当用户点击了「返回/前进」按钮时,浏览器不会重新检查这些资源文件,而是直接使用缓存的版本。

你如果需要更严格的控制,需要告知浏览器即使当用户点击了「返回/前进」按钮,也需要重新检查这些资源文件,那么可以使用:

Cache-Control: public, no-cache, no-store

不是所有的动态资源都会马上变成过时的资源。如果它们可以保持至少5分钟的时效,可以使用:

Cache-Control: public, max-age=300

经过这样的设置,浏览器只会在5分钟之后才重新检查。在这之前,缓存的内容会被直接使用。如果在5分钟后,这些过时的内容需要严格控制,你可以添加 must-revalidate 字段:

Cache-Control: public, max-age=300, must-revalidate

对于私密或者针对用户的内容,需要把 public 替换为 private 以避免内容被代理缓存。

Cache-Control: private, …

HTTP优缺点

优点

  • 简单、灵活和易于扩展;
    • 简单 基本的报文格式就是 header+body,头部信息也是简单的文本格式,用的也都是常见的英文单词。
    • 灵活和易于扩展 协议里的请求方法、URI、状态码、原因短语、头字段等每一个核心组成要素都没有被写死,允许开发者任意定制、扩充或解释。
  • 拥有成熟的软硬件环境,应用的非常广泛,是互联网的基础设施;
  • 无状态,可以轻松实现集群化,扩展性能;
  • 明文传输 不需要借助任何外部工具,就可以很容易地查看或者修改。

缺点

  • 无状态,没有记忆能力,它就无法支持需要连续多个步骤的事务操作,比方说网络购物;
  • 明文传输,数据完全肉眼可见,容易被窃听;
  • 不安全,无法验证通信双方的身份,也不能判断报文是否被窜改。

HTTPS

HTTPS 其实是一个「非常简单」的协议,里面规定了 新的协议名「https」,默认端口号 443 ,至于其他的请求 - 应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP。

HTTPS = HTTP + SSL/TLS

对称加密和非对称加密

对称加密

对称加密(Symmetric-Key Encryption),就是指加密和解密时使用的 密钥都是同一个 ,是 对称 的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。

目前常用的有:

  • AES (高级加密标准 Advanced Encryption Standard),密匙长度可以是128、192、256。

  • ChaCha20 Google 设计的另一种加密算法,密钥长度固定为 256 位。

加密分组模式

对称算法还有一个 分组模式 的概念,它可以让算法用固定长度的密钥加密任意长度的明文 ,把小秘密(即密钥)转化为大秘密(即密文)。

AEAD(Authenticated Encryption with Associated Data) ,在加密的同时增加了认证的功能,常用的是 GCM、CCM 和 Poly1305。

优缺点

  • 优点:运算速度快;
  • 缺点:无法安全地将密钥传输给通信方,也就是密匙交换成了问题。

非对称加密

它有两个密钥,一个叫 公钥(public key),一个叫 私钥(private key)。两个密钥是不同的(不对称) ,公钥可以公开给任何人使用,而私钥必须严格保密。

公钥和私钥有个特别的 单向 性,虽然都可以用来加密解密,但 公钥加密后只能用私钥解密 ,反过来,私钥加密后也只能用公钥解密

非对称加密可以解决 密钥交换 的问题。

目前常用的有:

  • RSA 它的安全性基于 整数分解 的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。现在推荐密匙长度是 2048。
  • ECC(Elliptic Curve Cryptography) 它基于 椭圆曲线离散对数 的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换,ECDSA 用于数字签名。

优缺点

  • 优点:可以更安全地将公开密钥传输给通信发送方;
  • 缺点:运算速度慢。

混合加密

虽然非对称密匙没有 密匙交换 问题,但是它的运行速度很慢。如果仅用非对称加密,虽然保证了安全,但通信速度有如蜗牛,实用性就变成了零。

所以就产生了 把对称加密和非对称加密结合起来混合加密,两者互相取长补短,即能高效地加密解密,又能安全地密钥交换。

  1. 在通信刚开始的时候使用非对称算法 ,比如 RSA、ECDHE,首先解决密钥交换的问题
  2. 然后用随机数产生对称算法使用的 会话密钥(session key),再用 公钥加密 。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。
  3. 对方拿到密文后用 私钥解密 ,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。

数字签名与证书

我们把消息加密了就能保证安全吗?

考虑下面的情况:

非对称加密的算法都是公开的,所有人都可以自己生成一对公钥私钥。

当服务端向客户端返回公钥 A1 的时候,中间人将其替换成自己的公钥 B1 传送给浏览器。

而浏览器此时一无所知,傻乎乎地使用公钥 B1 加密了密钥 K 发送出去,又被中间人截获,中间人利用自己的私钥 B2 解密,得到密钥 K,再使用服务端的公钥 A1 加密传送给服务端,完成了通信链路,而服务端和客户端毫无感知。

要保证安全,我们不仅要加密,还要保证消息的完整性以及身份认证。

摘要算法

摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数(Hash Function)。把一个大空间映射到小空间,由于对输入具有 单向性雪崩效应,可以用来做数据的完整性校验。

TLS 推荐的摘要算法是 SHA-2SHA-2 实际上是一系列摘要算法的统称,总共有 6 种,常用的有 SHA224、SHA256、SHA384,分别能够生成 28 字节、32 字节、48 字节的摘要。

但是它不具备机密性,在混合加密系统里用 会话密钥加密消息和摘要,这个术语叫做 哈希消息认证码(HMAC)

数字签名

现在还有个漏洞,通信的两个端点(endpoint) 也就是你怎么证明是你?服务器怎么证明是服务器?

非对称加密里的 私钥 ,使用私钥再加上摘要算法,就能够实现 数字签名 ,同时实现 身份认证不可否认

但又因为非对称加密效率太低,所以私钥只加密原文的摘要

这里的私钥是你自己需要有一个 私钥 ,服务器也需要有一个 私钥,你们互相交换公钥,除非你们的私钥被泄密,否则身份认证和不可否认就能保证。

数字证书和 CA

到现在,综合使用对称加密、非对称加密和摘要算法,我们已经实现了安全的四大特性,是不是已经完美了呢?

不是的,这里还有一个 公钥的信任 问题。因为谁都可以发布公钥,如何保证公钥不是伪造的?

找一个 公认的可信第三方 ,让它作为「信任的起点,递归的终点」,构建起公钥的信任链。这就是 CA(Certificate Authority,证书认证机构),使用 CA 的私钥对你的 公钥进行签名(包含序列号、用途、颁发者、有效时间等等和你的公钥打包再签名),形成 数字证书(Certificate)

那么 CA 怎么证明自己呢?这还是信任链的问题。小一点的 CA 可以让大 CA 签名认证 ,但链条的最后,也就是 Root CA ,就只能自己证明自己了。

也就是说,我的公钥是 CA 的私钥签名的,那么我需要拿到该 CA 的公钥进行解密,解密成功才能证明没有被伪造,那么最后还是信任链的问题,最终解决办法就是 Root CA,这就叫 自签名证书(Self-Signed Certificate)或者 根证书(Root Certificate),有了这个证书体系,操作系统和浏览器都内置了各大 CA 的根证书

也就是说,如果你的公钥不是 CA 颁发的,那么想要浏览器认为是安全的,就必须将它安装到系统的根证书存储区里。

TLS

TLS 协议的组成

记录协议

记录协议(Record Protocol)规定了 TLS 收发数据的基本单位:记录(record)。它有点像是 TCP 里的 segment,所有的其他子协议都需要通过记录协议发出。但多个记录数据可以在一个 TCP 包里一次性发出,也并不需要像 TCP 那样返回 ACK。

警报协议

警报协议(Alert Protocol)的职责是向对方发出警报信息,有点像是 HTTP 协议里的状态码。比如,protocol_version 就是不支持旧版本,bad_certificate 就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接。

握手协议

握手协议(Handshake Protocol)是 TLS 里最复杂的子协议,要比 TCP 的 SYN/ACK 复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统。

变更密码规范协议

最后一个是 变更密码规范协议(Change Cipher Spec Protocol),它非常简单,就是一个「通知」,告诉对方,后续的数据都将使用加密保护。那么反过来,在它之前,数据都是明文的。 (在 TLS 1.3 中这个协议已经删除,为了兼容 TLS 老版本,可能还会存在)。

心跳协议

Heartbeat 扩展为 TLS/DTLS 提供了一种新的协议,允许在不需要重协商的情况下,使用 keep-alive 功能。Heartbeat 扩展也为 path MTU (PMTU) 发现提供了基础(这个是 TLS 1.3 新加的,TLS 1.3 之前的版本没有这个协议。)。

TLS 1.2 连接过程解析

ECDHE握手过程

  1. 在 TCP 建立连接之后,浏览器会首先发一个 Client Hello 消息,也就是跟服务器打招呼。里面有客户端的TLS版本号、支持的密码套件,还有一个 随机数(Client Random) ,用于后续生成会话密钥。

    • 浏览器和服务器在使用 TLS 建立连接时需要选择一组恰当的加密算法来实现安全通信,这些算法的组合被称为 密码套件(cipher suite,也叫加密套件)
  2. 服务器接收到,立即返回 server_random,确认TLS版本号和使用的密码套件 ECDHE。

    • 服务器为了证明自己的身份,给客户端发送 Server Certificate。
    • 因为服务器选择了 ECDHE 算法,所以它会在证书后发送 Server Key Exchange 消息,里面是 椭圆曲线的公钥(Server Params) ,用来实现密钥交换算法,再加上自己的私钥签名认证。
    • 之后是 Server Hello Done 消息。
  3. 浏览器接收,先验证数字证书和签名。

    • 客户端按照密码套件的要求,也生成一个 椭圆曲线的公钥(Client Params) ,用 Client Key Exchange 消息发给服务器。

    • Client Random + Server Random 通过 ECDHE 算法得到 Pre-Master

    • Client Random、Server Random 和 Pre-Master 生成用于加密会话的主密匙 Master Secret

    master_secret = PRF(pre_master_secret, "master secret",
                        ClientHello.random + ServerHello.random) //Master Secret 公式
    

    这里的 PRF 就是伪随机数函数,它基于密码套件里的最后一个参数,比如这次的 SHA384,通过摘要算法来再一次强化 Master Secret 的随机性。

    主密钥有 48 字节,但它也不是最终用于通信的会话密钥,还会再用 PRF 扩展出更多的密钥,比如客户端发送用的会话密钥(client_write_key)、服务器发送用的会话密钥(server_write_key)等等,避免只用一个密钥带来的安全隐患。

    有了主密钥和派生的会话密钥,握手就快结束了。客户端发一个 Change Cipher Spec ,然后再发一个 Finished 消息,把之前所有发送的数据做个摘要,再加密一下,让服务器做个验证。

  4. 服务器也是同样的操作,发 Change Cipher SpecFinished 消息,双方都验证加密解密 OK,握手正式结束,后面就收发被加密的 HTTP 请求和响应了。

密码套件

TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 //密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法
  • 密钥协商算法使用 ECDHE;
  • 签名算法使用 RSA;
  • 握手后的通信使用 AES 对称算法,密钥长度 256 位,分组模式是 GCM;
  • 摘要算法使用 SHA384。

RSA握手过程

大体的流程没有变,只是 Pre-Master 不再需要用算法生成,而是客户端直接生成随机数,然后用服务器的公钥加密,通过 Client Key Exchange 消息发给服务器。服务器再用私钥解密,这样双方也实现了共享三个随机数,就可以生成主密钥。

  1. 客户端连上服务端。

  2. 服务端发送 CA 证书给客户端。

    发送的是一个 CA 链,不包含 ROOT 证书。

  3. 客户端验证该证书的可靠性。

    解析 CA 链,用链条上的 CA 进行验证,一层层地验证,直到找到根证书,就能够确定证书是可信的。

  4. 客户端从 CA 证书中取出公钥。

  5. 客户端生成一个随机密钥 k,并用这个公钥加密得到 k*。

  6. 客户端把 k* 发送给服务端。

  7. 服务端收到 k* 后用自己的私钥解密得到 k。

  8. 此时双方都得到了密钥 k,协商完成。

  9. 后续使用密钥 k* 加密信息。

RSA 和 ECDHE 握手过程的区别:

  • RSA 密钥协商算法「不支持」前向保密,ECDHE 密钥协商算法「支持」前向保密;
  • 使用了 RSA 密钥协商算法,TLS 完成四次握手后,才能进行应用数据传输,而对于 ECDHE 算法,客户端可以不用等服务端的最后一次 TLS 握手,就可以提前发出加密的 HTTP 数据,节省了一个消息的往返时间;
  • 使用 ECDHE, 在 TLS 第 2 次握手中,会出现服务器端发出的「Server Key Exchange」消息,而 RSA 握手过程没有该消息;

TLS 1.3 特性解析

强化安全

密码套件名代码
TLS_AES_128_GCM_SHA256{0x13,0x01}
TLS_AES_256_GCM_SHA384{0x13,0x02}
TLS_CHACHA20_POLY1305_SHA256{0x13,0x03}
TLS_AES_128_GCM_SHA256{0x13,0x04}
TLS_AES_128_GCM_8_SHA256{0x13,0x05}

握手分析

  1. 浏览器首先还是发一个 Client Hello

    • 因为 1.3 的消息兼容 1.2,所以开头的版本号、支持的密码套件和随机数(Client Random)结构都是一样的(不过这时的随机数是 32 个字节)。
  2. 服务器收到 Client Hello 同样返回 Server Hello 消息,还是要给出一个 随机数(Server Random)和选定密码套件。

    • supported_versions 里确认使用的是 TLS1.3,然后在 key_share 扩展带上曲线和对应的公钥参数。

这时只交换了两条消息,客户端和服务器就拿到了四个共享信息:Client RandomServer RandomClient ParamsServer Params ,两边就可以各自用 ECDHE 算出 Pre-Master ,再用 HKDF 生成主密钥 Master Secret ,效率比 TLS1.2 提高了一大截。

这里 TLS1.3 还有一个安全强化措施,多了个 Certificate Verify 消息,用服务器的私钥把前面的曲线、套件、参数等握手数据加了签名,作用和 Finished 消息差不多。但由于是私钥签名,所以强化了身份认证和和防窜改。

这两个 Hello 消息之后,客户端验证服务器证书,再发 Finished 消息,就正式完成了握手,开始收发 HTTP 报文。

HTTP/2

HTTP/1.x 缺陷

HTTP/1.x 实现简单是以牺牲性能为代价的:

  • 客户端需要使用多个连接才能实现并发和缩短延迟;
  • 不会压缩请求和响应首部,从而导致不必要的网络流量;
  • 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。

头部压缩

HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。

HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的头字段表,从而避免了重复传输。

HTTP/2 并没有使用传统的压缩算法,而是开发了专门的 HPACK 算法,在客户端和服务器两端建立「字典」,用索引号表示重复的字符串,还釆用 Huffman 编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。

二进制帧

HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。

在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。

  • 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。
  • 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。
  • 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

服务端推送

HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。

CORS跨域

当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。 例如,XMLHttpRequestFetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非使用CORS头文件。

(译者注:跨域并不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。)

隶属于 W3C 的 Web 应用工作组推荐了一种新的机制,即跨源资源共享(Cross-Origin Resource Sharing ) CORS。这种机制让Web应用服务器能支持跨站访问控制,从而使得安全地进行跨站数据传输成为可能。需要特别注意的是,这个规范是针对API容器的(比如说XMLHttpReques 或者 Fetch),以减轻跨域HTTP请求的风险。**CORS 需要客户端和服务器同时支持。目前,所有浏览器都支持该机制。 **

跨域资源共享标准( cross-origin sharing standard )允许在下列场景中使用跨域 HTTP 请求:

  • 前文提到的由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
  • Web 字体 (CSS 中通过 @font-face 使用跨域字体资源), 因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
  • WebGL 贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas
  • 样式表(使用 CSSOM)
  • Scripts (未处理的异常)

把CORS分为:简单请求、预请求和附带凭证信息的请求。

1. 简单请求

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。若请求满足所有下述条件,则该请求可视为“简单请求”:

  1. 使用下列方法之一:
  • GET
  • HEAD
  • POST
  1. Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (需要注意额外的限制)
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  1. Content-Type 的值仅限于下列三者之一:
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded
  1. 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

  2. 请求中没有使用 ReadableStream 对象。

简单来说,重点需要记住的就是两点:

  1. 只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-datatext/plain中的一种。
  2. 不会使用自定义请求头(类似于 X-Modified 这种)。

举例:

//比如说,假如站点 http://foo.example 的网页应用想要访问 http://bar.other 的资源。以下的 JavaScript 代 
//码应该会在 foo.example 上执行:    
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
function callOtherDomain() {
  if(invocation) {    
    invocation.open('GET', url, true);
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

//让我们看看,在这个场景中,浏览器会发送什么的请求到服务器,而服务器又会返回什么给浏览器:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 
Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example //该请求来自于 http://foo.exmaple。
//以上是浏览器发送请求

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61 
Access-Control-Allow-Origin: * //这表明服务器接受来自任何站点的跨站请求。如果设置为http://foo.example。其它站点就不能跨站访问 http://bar.other 的资源了。
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
//以上是服务器返回信息给浏览器

以下情况,请求会返回相关响应信息:

  • 如果资源是允许公开访问的(就像任何允许GET访问的 HTTP资源),返回 Access-Control-Allow-Origin:* 头信息就足够了,除非是一些需要Cookies和HTTP身份验证信息的请求。

  • 如果资源访问被限制基于相同的域名,或者如果要访问的资源需要凭证(或设置凭证),那么就有必要对请求头信息中的ORIGIN进行过滤,或者至少响应请求的来源(例如 Access-Control-Allow-Origin: http://arunranga.com)。

    另外,将发送 Access-Control-Allow-Credentials:true 头信息,这在后续部分将进行讨论。

2. 预请求

与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

当请求满足下述任一条件时,即应首先发送预检请求:

  1. 使用了下面任一 HTTP 方法:
  • PUT
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH
  1. 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (but note the additional requirements below)
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  1. Content-Type 的值不属于下列之一:
  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain
  1. 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。
  2. 请求中使用了ReadableStream对象。

不同于上面讨论的简单请求,“预请求”要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。这样做,是因为跨站请求可能会对目的站点的数据造成破坏。 当请求具备以下条件,就会被当成预请求处理:

  1. 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml 的 XML 数据的请求。
  2. 使用自定义请求头(比如添加诸如 X-PINGOTHER)。

举个例子:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}Arun';
function callOtherDomain(){
  if(invocation){
    invocation.open('POST', url, true);
    invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
    invocation.setRequestHeader('Content-Type', 'application/xml');
    invocation.onreadystatechange = handler;
    invocation.send(body); 
  }
}

如上,以 XMLHttpRequest 创建了一个 POST 请求,为该请求添加了一个自定义请求头(X-PINGOTHER: pingpong),并指定数据类型为 application/xml。所以,该请求是一个“预请求”形式的跨站请求。浏览器使用一个 OPTIONS 发送了一个“预请求”。Firefox 3.1 根据请求参数,决定需要发送一个“预请求”,来探明服务器端是否接受后续真正的请求。 OPTIONS 是 HTTP/1.1 里的方法,用来获取更多服务器端的信息,是一个不应该对服务器数据造成影响的方法。 随同 OPTIONS 请求,以下两个请求头一起被发送:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

假设服务器成功响应返回部分信息如下:

Access-Control-Allow-Origin: http://foo.example //表明服务器允许http://foo.example的请求
Access-Control-Allow-Methods: POST, GET, OPTIONS //表明服务器可以接受POST, GET和 OPTIONS的请求方法
Access-Control-Allow-Headers: X-PINGOTHER //传递一个可接受的自定义请求头列表。服务器也需要设置一个与浏览器对应。否则会报 Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers in preflight response 的错误
Access-Control-Max-Age: 1728000 //告诉浏览器,本次“预请求”的响应结果有效时间是多久。在上面的例子里,1728000秒代表着20天内,浏览器在处理针对该服务器的跨站请求,都可以无需再发送“预请求”,只需根据本次结果进行判断处理。

3. 附带凭证信息的请求

Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。

本例中,foo.example 的某脚本向 bar.other 发起一个GET 请求,并设置 Cookies:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
    
function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

第 7 行将 XMLHttpRequest 的 withCredentials 标志设置为 true,从而向服务器发送 Cookies。因为这是一个简单 GET 请求,所以浏览器不会对其发起“预检请求”。

但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。

假设服务器成功响应返回部分信息如下:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT

如果 bar.other 的响应头里没有 Access-Control-Allow-Credentials:true,则响应会被忽略。

特别注意: 给一个带有 withCredentials 的请求发送响应的时候,服务器端必须指定允许请求的域名,不能使用“*”。

上面这个例子中,如果响应头是这样的 Access-Control-Allow-Origin:* ,则响应会失败。在这个例子里,因为Access-Control-Allow-Origin的值是 foo.example 这个指定的请求域名,所以客户端把带有凭证信息的内容被返回给了客户端。另外注意,更多的cookie信息也被创建了。

CORS 和 JSONP 对比

  • JSONP 只能实现 GET 请求,而 CORS 支持所有类型的 HTTP 请求。
  • 使用 CORS,开发者可以使用普通的 XMLHttpRequest 发起请求和获得数据,比起 JSONP 有更好的错误处理。
  • JSONP 主要被老的浏览器支持,它们往往不支持 CORS,而绝大多数现代浏览器都已经支持了 CORS)。
  • CORS 与 JSONP 相比,无疑更为先进、方便和可靠。

参考

OSI七层模型详解

透视HTTP协议

MDN HTTP教程

硬核!30 张图解 HTTP 常见的面试题

HTTP

图解HTTP缓存

HTTP缓存控制小结

浏览器缓存机制剖析

彻底弄懂浏览器缓存策略

HTTP 缓存

变态的静态资源缓存与更新

HTTPS 详解

TLS & DTLS Heartbeat Extension

HTTP 基础概述