页面加载过程
总体来说分为以下几个过程:- DNS 解析:将域名解析成 IP 地址
- TCP 连接:TCP 三次握手
- 发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 断开连接:TCP 四次挥手
1. URL 到底是啥
URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。 比如 www.w3school.com.cn/html/index.…
scheme://host.domain:port/path/filename 各部分解释如下:
- scheme - 定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http,而 https 则是进行加密的网络传输。
- host - 定义域主机(http 的默认主机是 www)
- domain - 定义因特网域名,比如 w3school.com.cn
- port - 定义主机上的端口号(http 的默认端口号是 80)
- path - 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。
- filename - 定义文档/资源的名称
2. 域名解析(DNS)
在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。大家这里或许会有个疑问----计算机既可以被赋予 IP 地址,也可以被赋予主机名和域名。比如 www.hackr.jp。那怎么不一开始就赋予个 IP 地址?这样就可以省去解析麻烦。我们先来了解下什么是 IP 地址
2.1 IP 地址
IP 地址是指互联网协议地址,是 IP Address 的缩写。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。IP 地址是一个 32 位的二进制数,比如 127.0.0.1 为本机 IP。 域名就相当于 IP 地址乔装打扮的伪装者,带着一副面具。它的作用就是便于记忆和沟通的一组服务器的地址。用户通常使用主机名或域名来访问对方的计算机,而不是直接通过 IP 地址访问。因为与 IP 地址的一组纯数字相比,用字母配合数字的表示形式来指定计算机名更符合人类的记忆习惯。但要让计算机去理解名称,相对而言就变得困难了。因为计算机更擅长处理一长串数字。为了解决上述的问题,DNS 服务应运而生。
2.2.什么是域名解析
DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。
例如
baidu.com 220.114.23.56(服务器外网IP地址)80(服务器端口号)
2.3. 浏览器如何通过域名去查询 URL 对应的 IP 呢
- 浏览器缓存:浏览器会按照一定的频率缓存 DNS 记录。
- 操作系统缓存:如果浏览器缓存中找不到需要的 DNS 记录,那就去操作系统中找。
- 路由缓存:路由器也有 DNS 缓存。
- ISP 的 DNS 服务器:ISP 是互联网服务提供商(Internet Service Provider)的简称,ISP 有专门的 DNS 服务器应对 DNS 查询请求。
- 根服务器:ISP 的 DNS 服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS 服务器先问根域名服务器.com 域名服务器的 IP 地址,然后再问.baidu 域名服务器,依次类推)
2.4. 小结
浏览器通过向 DNS 服务器发送域名,DNS 服务器查询到与域名相对应的 IP 地址,然后返回给浏览器,浏览器再将 IP 地址打在协议上,同时请求参数也会在协议搭载,然后一并发送给对应的服务器。接下来介绍向服务器发送 HTTP 请求阶段,HTTP 请求分为三个部分:TCP 三次握手、http 请求响应信息、关闭 TCP 连接。
3. TCP 三次握手
在客户端发送数据之前会发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息。
3.1.TCP 三次握手的过程如下:
-
客户端发送一个带 SYN=1,Seq=X 的数据包到服务器端口(第一次握手,由浏览器发起,告诉服务器我要发送请求了)
-
服务器发回一个带 SYN=1, ACK=X+1, Seq=Y 的响应包以示传达确认信息(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)
-
客户端再回传一个带 ACK=Y+1, Seq=Z 的数据包,代表“握手结束”(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)
3.2.为啥需要三次握手
谢希仁著《计算机网络》中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。
4. 发送 HTTP 请求
TCP 三次握手结束后,开始发送 HTTP 请求报文。 请求报文由请求行(request line)、请求头(header)、请求体三个部分组成,如下图所示:
4.1 请求行包含请求方法、URL、协议版本
请求方法包含 8 种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE
。
URL 即请求地址,由 <协议>://<主机>:<端口>/<路径>?<参数>
组成
协议版本即 http 版本号
POST /chapter17/user.html HTTP/1.1
以上代码中“POST
”代表请求方法,“/chapter17/user.html
”表示 URL,“HTTP/1.1
”代表协议和协议的版本。现在比较流行的是 Http1.1 版本
4.2 请求头
请求头包含请求的附加信息,由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。
请求头部通知服务器有关于客户端请求的信息。它包含许多有关的客户端环境和请求正文的有用信息。其中比如:Host
,表示主机名,虚拟主机;Connection
,HTTP/1.1 增加的,使用 keepalive
,即持久连接,一个连接可以发多个请求;User-Agent
,请求发出者,兼容性以及定制化需求。
4.3 请求体
请求体,可以承载多个请求参数的数据,包含回车符、换行符和请求数据,并不是所有请求都具有请求数据。
name=tom&password=1234&realName=tomson
上面代码,承载着 name、password、realName 三个请求参数。
5. 服务器处理请求并返回 HTTP 报文
5.1. 服务器
服务器是网络环境中的高性能计算机,它侦听网络上的其他计算机(客户机)提交的服务请求,并提供相应的服务,比如网页服务、文件下载服务、邮件服务、视频服务。而客户端主要的功能是浏览网页、看视频、听音乐等等,两者截然不同。 每台服务器上都会安装处理请求的应用——web server。常见的 web server 产品有 apache、nginx、IIS 或 Lighttpd 等。
web server 担任管控的角色,对于不同用户发送的请求,会结合配置文件,把不同请求委托给服务器上处理相应请求的程序进行处理(例如 CGI 脚本,JSP 脚本,servlets,ASP 脚本,服务器端 JavaScript,或者一些其它的服务器端技术等),然后返回后台程序处理产生的结果作为响应。
5.2. MVC 后台处理阶段
后台开发现在有很多框架,但大部分都还是按照 MVC 设计模式进行搭建的。 MVC 是一个设计模式,将应用程序分成三个核心部件:模型(model)-- 视图(view)--控制器(controller),它们各自处理自己的任务,实现输入、处理和输出的分离。
1、视图(view)它是提供给用户的操作界面,是程序的外壳。
2、模型(model)
**模型主要负责数据交互。**在 MVC 的三个部件中,模型拥有最多的处理任务。一个模型能为多个视图提供数据。
3、控制器(controller)
它负责根据用户从"视图层"输入的指令,选取"模型层"中的数据,然后对其进行相应的操作,产生最终结果。控制器属于管理者角色,从视图接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示模型处理返回的数据。
这三层是紧密联系在一起的,但又是互相独立的,每一层内部的变化不影响其他层。每一层都对外提供接口(Interface),供上面一层调用。
至于这一阶段发生什么?简而言之,首先浏览器发送过来的请求先经过控制器,控制器进行逻辑处理和请求分发,接着会调用模型,这一阶段模型会获取 redis db 以及 MySQL 的数据,获取数据后将渲染好的页面,响应信息会以响应报文的形式返回给客户端,最后浏览器通过渲染引擎将网页呈现在用户面前。
5.3.http 响应报文
响应报文由响应行(request line)、响应头部(header)、响应主体三个部分组成。如下图所示:
(1) 响应行包含:协议版本,状态码,状态码描述状态码规则如下:
- 1xx:指示信息--表示请求已接收,继续处理。
- 2xx:成功--表示请求已被成功接收、理解、接受。
- 3xx:重定向--要完成请求必须进行更进一步的操作。
- 4xx:客户端错误--请求有语法错误或请求无法实现。
- 5xx:服务器端错误--服务器未能实现合法的请求。
(2) 响应头部包含响应报文的附加信息,由 名/值 对组成
(3) 响应主体包含回车符、换行符和响应返回数据,并不是所有响应报文都有响应数据
6. 浏览器解析渲染页面
浏览器拿到响应文本 HTML 后,接下来介绍下浏览器渲染机制
浏览器解析渲染页面分为一下五个步骤:- 根据 HTML 解析出 DOM 树
- 根据 CSS 解析生成 CSS 规则树
- 结合 DOM 树和 CSS 规则树,生成渲染树
- 根据渲染树计算每一个节点的信息
- 根据计算好的信息绘制页面
7. 断开连接
当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手。
- 发起方向被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。(第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)
- 被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。(第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)
- 被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。(第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧)
- 发起方向被动方发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。(第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)
TCP/UDP
TCP/IP网络模型
计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。
TCP/IP 是互联网相关的各类协议族的总称,比如:TCP,UDP,IP,FTP,HTTP,ICMP,SMTP 等都属于 TCP/IP 族内的协议。
TCP/IP模型是互联网的基础,它是一系列网络协议的总称。这些协议可以划分为四层,分别为链路层、网络层、传输层和应用层。
- 链路层:负责封装和解封装IP报文,发送和接受ARP/RARP报文等。
- 网络层:负责路由以及把分组报文发送给目标网络或主机。
- 传输层:负责对报文进行分组和重组,并以TCP或UDP协议格式封装报文。
- 应用层:负责向用户提供应用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。
UDP
定义
UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
特点:
1. 面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
具体来说就是:
在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
2. 有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
3. UDP是面向报文的
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
4. 不可靠性
首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
5. 头部开销小,传输数据报文时是很高效的。
UDP 头部包含了以下几个数据:- 两个十六位的端口号,分别为源端口(可选字段)和目标端口
- 整个数据报文的长度
- 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
因此 UDP 的头部开销小,只有八字节,相比 TCP的至少二十字节要少得多,在传输数据报文时是很高效的
TCP
当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。
TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。
1. TCP连接过程
如下图所示,可以看到建立一个TCP连接的过程为(三次握手的过程):
第一次握手客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。
第二次握手
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。
第三次握手
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
这里可能大家会有个疑惑:为什么 TCP 建立连接需要三次握手,而不是两次?这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。
2. TCP断开链接
TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。第一次握手
若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
第二次握手
B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。
第三次握手
B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
第四次握手
A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。
特点
1. 面向连接
面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
2. 仅支持单播传输
每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。
3.面向字节流
TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
4.可靠传输
对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
5.提供拥塞控制
当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞
6.TCP提供全双工通信
TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)
TCP与UDP区别总结:
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
- 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
- TCP首部开销20字节;UDP的首部开销小,只有8个字节
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
http1.1和http2.0的区别
对比1.1,2.0新增了多路复用,做到同一个连接并发处理多个请求,并且当我们对支持HTTP2.0的web server请求数据的时候,服务器会顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。整体来说,减少了http请求,提高了效率。
HTTPS
基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护
特点:
- 内容加密:采用混合加密技术,中间者无法直接查看明文内容
- 验证身份:通过证书认证客户端访问的是自己的服务器
- 保护数据完整性:防止传输的内容被中间人冒充或者篡改
非对称加密过程需要用到公钥进行加密,那么公钥从何而来?其实公钥就被包含在数字证书中,数字证书通常来说是由受信任的数字证书颁发机构CA,在验证服务器身份后颁发,证书中包含了一个密钥对(公钥和私钥)和所有者识别信息。数字证书被放到服务端,具有服务器身份验证和数据传输加密功能。
实现原理
SSL建立连接过程
- client向server发送请求https://baidu.com,然后连接到server的443端口,发送的信息主要是随机值1和客户端支持的加密算法。
- server接收到信息之后给予client响应握手信息,包括随机值2和匹配好的协商加密算法,这个加密算法一定是client发送给server加密算法的子集。
- 随即server给client发送第二个响应报文是数字证书。服务端必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面,这套证书其实就是一对公钥和私钥。传送证书,这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间、服务端的公钥,第三方证书认证机构(CA)的签名,服务端的域名信息等内容。
- 客户端解析证书,这部分工作是由客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值(预主秘钥)。
- 客户端认证证书通过之后,接下来是通过随机值1、随机值2和预主秘钥组装会话秘钥。然后通过证书的公钥加密会话秘钥。
- 传送加密信息,这部分传送的是用证书加密后的会话秘钥,目的就是让服务端使用秘钥解密得到随机值1、随机值2和预主秘钥。
- 服务端解密得到随机值1、随机值2和预主秘钥,然后组装会话秘钥,跟客户端会话秘钥相同。
- 客户端通过会话秘钥加密一条消息发送给服务端,主要验证服务端是否正常接受客户端加密的消息。
- 同样服务端也会通过会话秘钥加密一条消息回传给客户端,如果客户端能够正常接受的话表明SSL层连接建立完成了。
websocket和http区别
http
http协议是用在应用层的协议,他是基于tcp协议的,http协议建立链接也必须要有三次握手才能发送信息。
http链接分为短链接,长链接,短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response。长链接是在一定的期限内保持链接。保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的。
WebSocket
WebSocket是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮训问题而生的,他实现了多路复用,他是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息。
建立了WebSocket之后服务器不必在浏览器发送request请求之后才能发送信息到浏览器。这时的服务器已有主动权想什么时候发就可以发送信息到服务器。而且信息当中不必在带有head的部分信息了,与http的长链接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。
HTTP的长连接与websocket的持久连接
HTTP1.1的连接默认使用长连接(persistent connection), 即在一定的期限内保持链接,客户端会需要在短时间内向服务端请求大量的资源,保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的。 在一个TCP连接上可以传输多个Request/Response消息对,所以本质上还是Request/Response消息对,仍然会造成资源的浪费、实时性不强等问题。
如果不是持续连接,即短连接,那么每个资源都要建立一个新的连接,HTTP底层使用的是TCP,那么每次都要使用三次握手建立TCP连接,即每一个request对应一个response,将造成极大的资源浪费。 长轮询,即客户端发送一个超时时间很长的Request,服务器hold住这个连接,在有新数据到达时返回Response
websocket的持久连接,只需建立一次Request/Response消息对,之后都是TCP连接,避免了需要多次建立Request/Response消息对而产生的冗余头部信息。
网络安全
1.XSS
XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS重叠,所以只能叫 XSS。跨站脚本攻击是指通过存在安全漏洞的Web网站注册用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击。
跨站脚本攻击有可能造成以下影响:
- 利用虚假输入表单骗取用户个人信息。
- 利用脚本窃取用户的Cookie值,被害者在不知情的情况下,帮助攻击者发送恶意请求。
- 显示伪造的文章或图片。
XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。
XSS 的攻击方式千变万化,但还是可以大致细分为几种类型。
1.1.非持久型 XSS(反射型 XSS )
非持久型 XSS 漏洞,一般是通过给别人发送带有恶意脚本代码参数的 URL,当 URL 地址被打开时,特有的恶意代码参数被 HTML 解析、执行。
举一个例子,比如页面中包含有以下代码:
<select>
<script>
document.write(''
+ '<option value=1>'
+ location.href.substring(location.href.indexOf('default=') + 8)
+ '</option>'
);
document.write('<option value=2>English</option>');
</script>
</select>
攻击者可以直接通过 URL (类似:
https://xxx.com/xxx?default=<script>alert(document.cookie)</script>
) 注入可执行的脚本代码。不过一些浏览器如Chrome其内置了一些XSS过滤器,可以防止大部分反射型XSS攻击。
非持久型 XSS 漏洞攻击有以下几点特征:
- 即时性,不经过服务器存储,直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击,拿到用户隐私数据。
- 攻击者需要诱骗点击,必须要通过用户点击链接才能发起
- 反馈率低,所以较难发现和响应修复
- 盗取用户敏感保密信息
为了防止出现非持久型 XSS 漏洞,需要确保这么几件事情:
- Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。
- 尽量不要从 URL,document.referrer,document.forms 等这种 DOM API 中获取数据直接渲染。
- 尽量不要使用 eval, new Function(),document.write(),document.writeln(),window.setInterval(),window.setTimeout(),innerHTML,document.createElement() 等可执行字符串的方法。
- 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。
- 前端渲染的时候对任何的字段都需要做 escape 转义编码。
1.2.持久型 XSS(存储型 XSS)
持久型 XSS 漏洞,一般存在于 Form 表单提交等交互功能,如文章留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。
举个例子,对于评论功能来说,就得防范持久型 XSS 攻击,因为我可以在评论中输入以下内容
主要注入页面方式和非持久型 XSS 漏洞类似,只不过持久型的不是来源于 URL,referer,forms 等,而是来源于后端从数据库中读出来的数据 。持久型 XSS 攻击不需要诱骗点击,黑客只需要在提交表单的地方完成注入即可,但是这种 XSS 攻击的成本相对还是很高。
攻击成功需要同时满足以下几个条件:
- POST 请求提交表单后端没做转义直接入库。
- 后端从数据库中取出数据没做转义直接输出给前端。
- 前端拿到后端数据没做转义直接渲染成 DOM。
持久型 XSS 有以下几个特点:
- 持久性,植入在数据库中
- 盗取用户敏感私密信息
- 危害面广
1.3.如何防御
对于 XSS 攻击来说,通常有两种方式可以用来防御。
1.CSP
CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。
通常可以通过两种方式来开启 CSP:
设置 HTTP Header 中的 Content-Security-Policy 设置 meta 标签的方式 这里以设置 HTTP Header 来举例:
只允许加载本站资源
Content-Security-Policy: default-src 'self'
只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
允许加载任何来源框架
Content-Security-Policy: child-src 'none'
如需了解更多属性,请查看Content-Security-Policy
文档
对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。
2. 转义字符
用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义
function escape(str) {
str = str.replace(/&/g, '&')
str = str.replace(/</g, '<')
str = str.replace(/>/g, '>')
str = str.replace(/"/g, '&quto;')
str = str.replace(/'/g, ''')
str = str.replace(/`/g, '`')
str = str.replace(/\//g, '/')
return str
}
但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。
const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1><script>alert("xss");</script>
console.log(html)
以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。
3. HttpOnly Cookie。
这是预防XSS攻击窃取用户cookie最有效的防御手段。Web应用程序在设置cookie时,将其属性设为HttpOnly,就可以避免该网页的cookie被客户端恶意JavaScript窃取,保护用户cookie信息。
2.CSRF
CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的Web攻击,它利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作。
2.1.CSRF攻击的原理
下面先介绍一下CSRF攻击的原理:
完成 CSRF 攻击必须要有三个条件:
- 用户已经登录了站点 A,并在本地记录了 cookie
- 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。
- 站点 A 没有做任何 CSRF 防御
2.2.如何防御
防范 CSRF 攻击可以遵循以下几种规则:
-
Get 请求不对数据进行修改
-
不让第三方网站访问到用户 Cookie
-
阻止第三方网站请求接口
-
请求时附带验证信息,比如验证码或者 Token
- SameSite 可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。
- Referer Check
HTTP Referer是header的一部分,当浏览器向web服务器发送请求时,一般会带上Referer信息告诉服务器是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。可以通过检查请求的来源来防御CSRF攻击。正常请求的referer具有一定规律,如在提交表单的referer必定是在该页面发起的请求。所以通过检查http包头referer的值是不是这个页面,来判断是不是CSRF攻击。
但在某些情况下如从https跳转到http,浏览器处于安全考虑,不会发送referer,服务器就无法进行check了。若与该网站同域的其他网站有XSS漏洞,那么攻击者可以在其他网站注入恶意脚本,受害者进入了此类同域的网址,也会遭受攻击。出于以上原因,无法完全依赖Referer Check作为防御CSRF的主要手段。但是可以通过Referer Check来监控CSRF攻击的发生。
- Anti CSRF Token
目前比较完善的解决方案是加入Anti-CSRF-Token。即发送请求时在HTTP 请求中以参数的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token。服务器读取浏览器当前域cookie中这个token值,会进行校验该请求当中的token和cookie当中的token值是否都存在且相等,才认为这是合法的请求。否则认为这次请求是违法的,拒绝该次服务。
这种方法相比Referer检查要安全很多,token可以在用户登陆后产生并放于session或cookie中,然后在每次请求时服务器把token从session或cookie中拿出,与本次请求中的token 进行比对。由于token的存在,攻击者无法再构造出一个完整的URL实施CSRF攻击。但在处理多个页面共存问题时,当某个页面消耗掉token后,其他页面的表单保存的还是被消耗掉的那个token,其他页面的表单提交时会出现token错误。
-
验证码 应用程序和用户进行交互过程中,特别是账户交易这种核心步骤,强制用户输入验证码,才能完成最终请求。在通常情况下,验证码够很好地遏制CSRF攻击。但增加验证码降低了用户的体验,网站不能给所有的操作都加上验证码。所以只能将验证码作为一种辅助手段,在关键业务点设置验证码。
3.点击劫持
点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。
3.1. 特点
隐蔽性较高,骗取用户操作 "UI-覆盖攻击" 利用iframe或者其它标签的属性
3.2. 点击劫持的原理
用户在登陆 A 网站的系统后,被攻击者诱惑打开第三方网站,而第三方网站通过 iframe 引入了 A 网站的页面内容,用户在第三方网站中点击某个按钮(被装饰的按钮),实际上是点击了 A 网站的按钮。 接下来我们举个例子:我在优酷发布了很多视频,想让更多的人关注它,就可以通过点击劫持来实现
iframe {
width: 1440px;
height: 900px;
position: absolute;
top: -0px;
left: -0px;
z-index: 2;
-moz-opacity: 0;
opacity: 0;
filter: alpha(opacity=0);
}
button {
position: absolute;
top: 270px;
left: 1150px;
z-index: 1;
width: 90px;
height:40px;
}
</style>
......
<button>点击脱衣</button>
<img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/2/1730eb15db22c572~tplv-t2oaga2asx-image.image">
<iframe src="http://i.youku.com/u/UMjA0NTg4Njcy" scrolling="no"></iframe>
从上图可知,攻击者通过图片作为页面背景,隐藏了用户操作的真实界面,当你按耐不住好奇点击按钮以后,真正的点击的其实是隐藏的那个页面的订阅按钮,然后就会在你不知情的情况下订阅了。
3.3. 如何防御
1)X-FRAME-OPTIONS X-FRAME-OPTIONS是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。
该响应头有三个值可选,分别是
DENY,表示页面不允许通过 iframe 的方式展示 SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示 ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示 2)JavaScript 防御 对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。
<head>
<style id="click-jack">
html {
display: none !important;
}
</style>
</head>
<body>
<script>
if (self == top) {
var style = document.getElementById('click-jack')
document.body.removeChild(style)
} else {
top.location = self.location
}
</script>
</body>
以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。
4.URL跳转漏洞
4.1. 定义
借助未验证的URL跳转,将应用程序引导到不安全的第三方区域,从而导致的安全问题。
4.2.URL跳转漏洞原理
黑客利用URL跳转漏洞来诱导安全意识低的用户点击,导致用户信息泄露或者资金的流失。其原理是黑客构建恶意链接(链接需要进行伪装,尽可能迷惑),发在QQ群或者是浏览量多的贴吧/论坛中。 安全意识低的用户点击后,经过服务器或者浏览器解析后,跳到恶意的网站中。
恶意链接需要进行伪装,经常的做法是熟悉的链接后面加上一个恶意的网址,这样才迷惑用户。
诸如伪装成像如下的网址,你是否能够识别出来是恶意网址呢?
http://gate.baidu.com/index?act=go&url=http://t.cn/RVTatrd
http://qt.qq.com/safecheck.html?flag=1&url=http://t.cn/RVTatrd
http://tieba.baidu.com/f/user/passport?jumpUrl=http://t.cn/RVTatrd
4.3.实现方式:
- Header头跳转
- Javascript跳转
- META标签跳转
这里我们举个Header头跳转实现方式:
<?php
$url=$_GET['jumpto'];
header("Location: $url");
?>
http://www.wooyun.org/login.php?jumpto=http://www.evil.com
这里用户会认为www.wooyun.org
都是可信的,但是点击上述链接将导致用户最终访问www.evil.com
这个恶意网址。
4.4.如何防御
1)referer的限制 如果确定传递URL参数进入的来源,我们可以通过该方式实现安全限制,保证该URL的有效性,避免恶意用户自己生成跳转链接
2)加入有效性验证Token 我们保证所有生成的链接都是来自于我们可信域的,通过在生成的链接里加入用户不可控的Token对生成的链接进行校验,可以避免用户生成自己的恶意链接从而被利用,但是如果功能本身要求比较开放,可能导致有一定的限制。
5.SQL注入
SQL注入是一种常见的Web安全漏洞,攻击者利用这个漏洞,可以访问或修改数据,或者利用潜在的数据库漏洞进行攻击。
5.1.SQL注入的原理
我们先举一个万能钥匙的例子来说明其原理:
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
后端的 SQL 语句可能是如下这样的:
let querySQL = `
SELECT *
FROM user
WHERE username='${username}'
AND psw='${password}'
`;
// 接下来就是执行 sql 语句...
这是我们经常见到的登录页面,但如果有一个恶意攻击者输入的用户名是 admin' --,密码随意输入,就可以直接登入系统了。why! ----这就是SQL注入
我们之前预想的SQL 语句是:
SELECT * FROM user WHERE username='admin' AND psw='password'
但是恶意攻击者用奇怪用户名将你的 SQL 语句变成了如下形式:
SELECT * FROM user WHERE username='admin' --' AND psw='xxxx'
在 SQL 中,' --是闭合和注释的意思,-- 是注释后面的内容的意思,所以查询语句就变成了:
SELECT * FROM user WHERE username='admin'
所谓的万能密码,本质上就是SQL注入的一种利用方式。
5.2. 一次SQL注入的过程包括以下几个过程:
- 获取用户请求参数
- 拼接到代码当中
- SQL语句按照我们构造参数的语义执行成功
5.3. SQL注入的必备条件:
- 可以控制输入的数据
- 服务器要执行的代码拼接了控制的数据。
我们会发现SQL注入流程中与正常请求服务器类似,只是黑客控制了数据,构造了SQL查询,而正常的请求不会SQL查询这一步,SQL注入的本质:数据和代码未分离,即数据当做了代码来执行。
5.4. 危害
- 获取数据库信息
- 管理员后台用户名和密码
- 获取其他数据库敏感信息:用户名、密码、手机号码、身份证、银行卡信息……
- 整个数据库:脱裤
- 获取服务器权限
- 植入Webshell,获取服务器后门
- 读取服务器敏感文件
5.5 如何防御
-
严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害
-
后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。
-
对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。
-
所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。
6. OS命令注入攻击
OS命令注入和SQL注入差不多,只不过SQL注入是针对数据库的,而OS命令注入是针对操作系统的。OS命令注入攻击指通过Web应用,执行非法的操作系统命令达到攻击的目的。只要在能调用Shell函数的地方就有存在被攻击的风险。倘若调用Shell时存在疏漏,就可以执行插入的非法命令。
命令注入攻击可以向Shell发送命令,让Windows或Linux操作系统的命令行启动程序。也就是说,通过命令注入攻击可执行操作系统上安装着的各种程序。
6.1.原理
黑客构造命令提交给web应用程序,web应用程序提取黑客构造的命令,拼接到被执行的命令中,因黑客注入的命令打破了原有命令结构,导致web应用执行了额外的命令,最后web应用程序将执行的结果输出到响应页面中。
我们通过一个例子来说明其原理,假如需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去返回一个结果给用户
// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};
exec(`git clone ${params.repo} /some/path`);
如果 params.repo 传入的是 https://github.com/admin/admin.github.io.git
确实能从指定的 git repo 上下载到想要的代码。
但是如果 params.repo 传入的是
https://github.com/xx/xx.git && rm -rf /* &&
恰好你的服务是用 root 权限起的就糟糕了。
6.2.如何防御
后端对前端提交内容进行规则限制(比如正则表达式)。 在调用系统命令前对所有传入参数进行命令行参数转义过滤。 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm包
session,cookie和token
Cookie
cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。
cookie由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的cookie数量是有限的。
Session
session 从字面上讲,就是会话。这个就类似于你和一个人交谈,你怎么知道当前和你交谈的是张三而不是李四呢?对方肯定有某种特征(长相等)表明他就是张三。
session 也是类似的道理,服务器要知道当前发请求给自己的是谁。为了做这种区分,服务器就要给每个客户端分配不同的“身份标识”,然后客户端每次向服务器发请求的时候,都带上这个“身份标识”,服务器就知道这个请求来自于谁了。至于客户端怎么保存这个“身份标识”,可以有很多种方式,对于浏览器客户端,大家都默认采用 cookie 的方式。
服务器使用session把用户的信息临时保存在了服务器上,用户离开网站后session会被销毁。这种用户信息存储方式相对cookie来说更安全,可是session有一个缺陷:如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失。
Token
在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,tokens 是多用户下处理认证的最佳方式。
以下几点特性会让你在程序中使用基于Token的身份验证
- 无状态、可扩展
- 支持移动设备
- 跨程序调用
- 安全
Token起源
在介绍基于Token的身份验证的原理与优势之前,不妨先看看之前的认证都是怎么做的。
基于服务器的验证
我们都是知道HTTP协议是无状态的,这种无状态意味着程序需要验证每一次请求,从而辨别客户端的身份。
在这之前,程序都是通过在服务端存储的登录信息来辨别请求的。这种方式一般都是通过存储Session来完成。
随着Web,应用程序,已经移动端的兴起,这种验证的方式逐渐暴露出了问题。尤其是在可扩展性方面。
基于服务器验证方式暴露的一些问题
1.Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
2.可扩展性:在服务端的内存中使用Seesion存储登录信息,伴随而来的是可扩展性问题。
3.CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
4.CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
在这些问题中,可扩展行是最突出的。因此我们有必要去寻求一种更有行之有效的方法。
基于Token的验证原理
基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。
这种概念解决了在服务端存储信息时的许多问题
NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录。
基于Token的身份验证的过程如下:
-
用户通过用户名和密码发送请求。
-
程序验证。
-
程序返回一个签名的token 给客户端。
-
客户端储存token,并且每次用于每次发送请求。
-
服务端验证token并返回数据。
每一次请求都需要token。token应该在HTTP的头部发送从而保证了Http请求无状态。我们同样通过设置服务器属性Access-Control-Allow-Origin:* ,让服务器能接受到来自所有域的请求。
需要主要的是,在ACAO头部标明(designating)*时,不得带有像HTTP认证,客户端SSL证书和cookies的证书。
实现思路:
-
用户登录校验,校验成功后就返回Token给客户端。
-
客户端收到数据后保存在客户端
-
客户端每次访问API是携带Token到服务器端。
-
服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
当我们在程序中认证了信息并取得token之后,我们便能通过这个Token做许多的事情。
我们甚至能基于创建一个基于权限的token传给第三方应用程序,这些第三方程序能够获取到我们的数据(当然只有在我们允许的特定的token)
Token的优势
1.无状态、可扩展
在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。
如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性)。用户量大时,可能会造成一些拥堵。
但是不要着急。使用tokens之后这些问题都迎刃而解,因为tokens自己hold住了用户的验证信息。
2.安全性
请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。
token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效。
3.可扩展性
Tokens能够创建与其它程序共享权限的程序。例如,能将一个随便的社交帐号和自己的大号(Fackbook或是Twitter)联系起来。当通过服务登录Twitter(我们将这个过程Buffer)时,我们可以将这些Buffer附到Twitter的数据流上(we are allowing Buffer to post to our Twitter stream)。
使用tokens时,可以提供可选的权限给第三方应用程序。当用户想让另一个应用程序访问它们的数据,我们可以通过建立自己的API,得出特殊权限的tokens。
4.多平台跨域
我们提前先来谈论一下CORS(跨域资源共享),对应用程序和服务进行扩展的时候,需要介入各种各种的设备和应用程序。
Having our API just serve data, we can also make the design choice to serve assets from a CDN. This eliminates the issues that CORS brings up after we set a quick header configuration for our application.
只要用户有一个通过了验证的token,数据和资源就能够在任何域上被请求到。
Access-Control-Allow-Origin: *
基于标准创建token的时候,你可以设定一些选项。我们在后续的文章中会进行更加详尽的描述,但是标准的用法会在JSON Web Tokens体现。
最近的程序和文档是供给JSON Web Tokens的。它支持众多的语言。这意味在未来的使用中你可以真正的转换你的认证机制。
缓存
缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。
对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
接下来的内容中我们将通过缓存位置、缓存策略以及实际场景应用缓存策略来探讨浏览器缓存机制。
缓存位置
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
1.Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch函数获取数据。也就是说,如果我们没有在 Service Worker命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
2.Memory Cache
Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓。
内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">
)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。
需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。
3.Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。
浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?
关于这点,网上说法不一,不过以下观点比较靠得住:
- 对于大文件来说,大概率是不存储在内存中的,反之优先
- 当前系统内存使用率高的话,文件优先存储进硬盘
4.Push Cache
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。这里推荐阅读Jake Archibald的 HTTP/2 push is tougher than I thought 这篇文章,文章中的几个结论:
- 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
- 可以推送 no-cache 和 no-store 的资源
- 一旦连接被关闭,Push Cache 就被释放
- 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
- Push Cache 中的缓存只能被使用一次
- 浏览器可以拒绝接受已经存在的资源推送
- 你可以给其他域名推送资源
如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。
那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
缓存过程分析
浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求,那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。具体过程如下图:
由上图我们可以知道:
-
浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
-
浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中
以上两点结论就是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,本文也将围绕着这点进行详细分析。为了方便大家理解,这里我们根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强缓存和协商缓存。
强缓存
强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
1.Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT
表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
2.Cache-Control
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300
时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:
public:所有内容都将被缓存(客户端和代理服务器都可缓存)。具体来说响应可被任何中间节点缓存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中间的proxy可以缓存资源,比如下次再请求同一资源proxy1直接把自己缓存的东西给 Browser 而不再向proxy2要。
private:所有内容只有客户端可以缓存,Cache-Control的默认取值。具体来说,表示中间节点不允许缓存,对于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 会老老实实把Server 返回的数据发送给proxy1,自己不缓存任何数据。当下次Browser再次请求时proxy会做好请求转发而不是自作主张给自己缓存的数据。
no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存。需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。
no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
max-age:max-age=xxx (xxx is numeric)表示缓存内容将在xxx秒后失效
s-maxage(单位为s):同max-age作用一样,只在代理服务器中生效(比如CDN缓存)。比如当s-maxage=60时,在这60秒中,即使更新了CDN的内容,浏览器也不会进行请求。max-age用于普通缓存,而s-maxage用于代理缓存。s-maxage的优先级高于max-age。如果存在s-maxage,则会覆盖掉max-age和Expires header。
max-stale:能容忍的最大过期时间。max-stale指令标示了客户端愿意接收一个已经过期了的响应。如果指定了max-stale的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何age的响应(age表示响应由源站生成或确认的时间与当前时间的差值)。
min-fresh:能够容忍的最小新鲜度。min-fresh标示了客户端不愿意接受新鲜度不多于当前的age加上min-fresh设定的时间之和的响应。
从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。
3.Expires和Cache-Control两者对比
其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。 强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。
协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
- 协商缓存生效,返回304和Not Modified
- 协商缓存失效,返回200和请求结果
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
1.Last-Modified和If-Modified-Since
浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;
Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
浏览器下一次请求这个资源,浏览器检测到有 Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200
但是 Last-Modified 存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
- 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag
和If-None-Match
2.ETag和If-None-Match
Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。
3.两者之间对比:
-
首先在精确度上,Etag要优于Last-Modified。
- Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
-
第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
-
第三在优先级上,服务器校验优先考虑Etag
缓存机制
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。具体流程图如下:
看到这里,不知道你是否存在这样一个疑问:如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
实际场景应用缓存策略
1.频繁变动的资源
Cache-Control: no-cache
对于频繁变动的资源,首先需要使用Cache-Control: no-cache
使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。
2.不常变化的资源
Cache-Control: max-age=31536000
通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000
(一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery-3.3.1.min.js
, lodash.min.js
等) 均采用这个模式。
用户行为对浏览器缓存的影响
所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:
- 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
- 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有
Cache-control: no-cache
(为了兼容,还带了Pragma: no-cache
),服务器直接返回 200 和最新内容。
文章内容转自一下地址:
大牛博客推荐浪里行舟