前端须要了解的HTTP网络协议

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)的应用层协议。javascript

它是为 Web 浏览器与 Web 服务器之间的通讯而设计的,但也能够用于其余目的。css

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

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

HTTP报文

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

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

请求报文

请求行(request line)

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

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

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

请求方法

目前 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 换行表示结束。

状态码

状态码 类别 含义
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 须要进行附加操做以完成请求
4XX Client Error(客户端错误状态码) 服务器没法处理请求
5XX Server 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 协议规定 30一、302 状态下重定向时不容许把 POST 方法改为 GET 方法,可是大多数浏览器都会在 30一、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 优先的语言(天然语言)
Authorization Web 认证信息
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-Agent HTTP 客户端程序的信息

响应头部字段

头部字段名 说明
Accept-Ranges 是否接受字节范围请求
Age 推算资源建立通过时间
ETag 资源的匹配信息
Location 令客户端重定向至指定 URI
Proxy-Authenticate 代理服务器对客户端的认证信息
Retry-After 对再次发起请求的时机要求
Server HTTP 服务器的安装信息
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,先决条件失败) 响应。

缓存头部对比

头部 优点和特色 劣势和问题
Expires 一、HTTP 1.0 产物,能够在HTTP 1.0和1.1中使用,简单易用。 二、以时刻标识失效时间。 一、时间是由服务器发送的(UTC),若是服务器时间和客户端时间存在不一致,可能会出现问题。 二、存在版本问题,到期以前的修改客户端是不可知的。
Cache-Control 一、HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。 二、比Expires多了不少选项设置。 一、HTTP 1.1 才有的内容,不适用于HTTP 1.0 。 二、存在版本问题,到期以前的修改客户端是不可知的。
Last-Modified 一、不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间若是相同则返回304,不一样返回200以及资源内容。 一、只要资源修改,不管内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种状况下该资源包含的数据实际上同样的。 二、以时刻做为标识,没法识别一秒内进行屡次修改的状况。 三、某些服务器不能精确的获得文件的最后修改时间。
ETag 一、能够更加精确的判断资源是否被修改,能够识别一秒内屡次修改的状况。 二、不存在版本问题,每次请求都回去服务器进行校验。 一、计算ETag值须要性能损耗。 二、分布式服务器存储的状况下,计算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),密匙长度能够是12八、19二、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 种,经常使用的有 SHA22四、SHA25六、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 消息,把以前全部发送的数据作个摘要,再加密一下,让服务器作个验证。

  1. 服务器也是一样的操做,发 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请求。 例如,XMLHttpRequest和Fetch 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

(2). Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合以外的其余首部字段。该集合为:

Accept
Accept-Language
Content-Language
Content-Type (须要注意额外的限制)
DPR
Downlink
Save-Data
Viewport-Width
Width

(3). Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

(4). 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象能够使用 XMLHttpRequest.upload 属性访问。

(5). 请求中没有使用 ReadableStream 对象。

简单来讲,重点须要记住的就是两点:

**(1)只使用 GET, HEAD 或者 POST 请求方法。若是使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/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

(2). 人为设置了对 CORS 安全的首部字段集合以外的其余首部字段。该集合为:

Accept
Accept-Language
Content-Language
Content-Type (but note the additional requirements below)
DPR
Downlink
Save-Data
Viewport-Width
Width

(3). Content-Type 的值不属于下列之一:

application/x-www-form-urlencoded
multipart/form-data
text/plain

(4). 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。
(5). 请求中使用了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 的某个特殊标志位。

本例中,http://foo.example 的某脚本向 http://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的值是 http://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 基础概述

相关文章
相关标签/搜索