浏览器相关概念
进程与线程
概念
进程: 进程有独立的地址空间,操作系统会为进程单独创建一块内存,用于存放代码、运行中的数据和一个执行任务的主线程
线程: 线程是程序执行的最小单位,是进程的一个执行流,线程依附于进程而存在
进程、线程之间的关系
- 进程中任何一个线程执行出错,都会导致整个进程崩溃。
- 线程之间共享进程中的数据
- 当一个进程关闭后,操作系统会回收进程所占用的内存
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,进程之间的内容相互隔离,需要使用IPC通信才可以访问对方数据
- 一个进程至少拥有一个线程,一个线程只能属于一个进程
浏览器多进程架构
浏览器从 不稳定、不流畅 和 不安全 的单进程架构进化到多进程架构。多进程架构能够解决单进程的痛点。进程之间的相互隔离以及进程作为一个计算机系统资源的基本单位,可利用操作系统使用安全沙箱对进程进行安全隔离,提升了浏览器的 稳定性、流程性 和 安全性。但是同时也带来了一些问题: 浏览器更高的资源占用、更复杂的体系架构。目前多进程架构浏览器主要的进程有如下几个:
-
浏览器进程: 主要负责界面显示。用户交互、子进程管理,同时提供存储(
Cookie等存储)等功能。 -
渲染进程: 核心任务是将HTML、CSS和JavaScript解析成为用户可交互的网页,默认情况下,一个Tab标签会对应一个渲染进程(
同站点除外)。渲染进程中的数据所有内容都是来通过网络获取的,渲染进程中的代码都是不可信任的,处于安全考虑,渲染进程都是运行在安全沙箱模式下 -
网络进程: 主要是负责页面的网络资源加载
-
GPU进程: GPU进程的初衷是为了实现3D CSS效果,随着技术发展,网页以及UI界面都选择了GPU来绘制
-
插件进程: 主要负责插件的运行,因插件容易崩溃,所以通过插件进程来隔离,确保插件崩溃不会对浏览器和页面造成影响。
导航流程
导航流程图
一个经典的面试题: 输入url到加载完页面发生了什么?从图中可以看出从输入URL到界面展示发生了什么。
整体流程概括<URL请求渲染界面流程>:
-
浏览器进程接收到用户输入的URL请求,浏览器进程将该URL通过IPC转发给网络进程
-
网络进程接收到URL请求信息后,向服务器端发起真正的URL请求
-
网络进程接收到服务器端返回的响应头数据后,解析响应头数据,并将数据转发给浏览器进程
-
浏览器进程接收到响应头信息后,根据响应头的状态码进行不同的操作,如果接收的是
301/302/307状态码,则会重新进行网络请求。正常流程会发送"提交导航"给渲染进程 -
渲染进程接收到
"提交导航"的消息后,会通过IPC与网络进程创建数据管道,从网络进程接收HTML等数据 -
渲染进程向浏览器进程发送
确认提交信息,准备解析页面数据 -
浏览器进程接收到
确认提交信息后,开始移除旧文档(首次渲染白屏),然后更新浏览器进程中的页面信息其中
渲染流程在后续章节详细描述。在看完图后,提出以下问题: 带着问题再去看详细的导航流程 加深对浏览器导航流程的各个环节的理解。- 如果浏览器导航栏中不是输入URL怎么办?
- 强缓存和协商缓存是什么(
后续章节单独描述)? - DNS查询是什么?
- 浏览器最多为一个域名分配多少个TCP连接?
- 状态码有哪些?各自有什么含义?
导航流程
用户输入
浏览器根据用户输入的信息进行校验,根据校验结果分情况处理
- 如果是合法URL,则与网络进程进行IPC通信,将url请求转发给网络进程
- 如果是非法URL,则当做正常的查询处理,将其拼接到浏览器默认的搜索引擎地址后 进行请求, 然后再将请求发送给网络进程
当用户输入按下回车键后,就意味着当前界面需要被新的界面替换。但浏览器在离开当前页面时,给了当前界面一个 beforeunload事件的机会。主要是用于提示用户即将离开,是否需要保存界面信息等。如果没有beforeunload事件或者该事件已经执行完毕,浏览器的标签页图标会进入到一个加载状态。
直到整个导航流程走到渲染流程提交【确定提交】信息,才会直接被新界面替换。如果渲染流程的初次渲染很耗时,即浏览器进程接收【确认提交】和【DrawQuad】命令之间的时间间隔很长,这将导致浏览器长时间等待渲染数据,出现白屏
URL请求过程
构建请求
网络进程接收到URL请求之后,网络进程首先会构建HTTP请求行,准备好后发起网络请求。
// 请求方法 请求目标 HTTP版本号
GET /index.html HTTP1.1
查找缓存
网络进程首先会查找本地缓存(强缓存)是否缓存了该资源。如果浏览器若发现本地有该请求资源的有效副本,则会拦截请求 返回资源副本并结束请求。不再像服务器重新下载
性能优化: 缓解服务器端压力,提升页面性能;不需要像服务端访问,直接从本地获取 快速加载资源,提高访问速度。可使用 SW缓存接口资源、图片、文字等信息
DNS解析
HTTP基于TCP/IP,HTTP请求时,浏览器需要先通过TCP连接与服务器建立连接。向服务器发起真正请求需要通过IP进行访问。但HTTP请求发起的一般都是类似github.com的域名,此时需要DNS域名系统协助解析将域名转换成IP地址。
性能优化项: 若一直通过核心DNS系统解析,这将导致访问速度很慢,主要手段是通过DNS缓存:运营商DNS缓存,操作系统DNS解析缓存,host文件DNS缓存等
TCP连接
通过DNS解析拿到域名对应的IP地址后,客户端开始与服务器端尝试建立TCP连接。
在同一个域名下,最多可以同时建立6个TCP连接,如果当前TCP连接已经达到了6个,则需要先排队等待TCP。
TCP通过三次握手与服务器端建立连接, "请求-应答-应答其应答" 模式保证服务器与客户端都有接收和发送的信息的能力。
为什么TCP连接不是2次或者4次
女方<客户端>: 我生气了
男方<服务器>:对不起,亲爱的
以男女吵架为例,如果两次连接为例,如果女方<客户端>没有保证能够接受到信息的能力,女方<客户端>会以为男方<服务器>从始至终没有回应 卒 分手
女方<客户端>: 我生气了
男的<服务器>:对不起,亲爱的
女方<客户端>: 好的,和好了
男方<服务器>: 亲爱的你真好
以男女吵架为例,如果四次连接为例,女方<客户端>收到了男方<服务器>的回应已和好。第四次男方表忠心时,是对吵架后的一次保证,但是对整个和好过程来说 可有可无,多一次应答 会造成网络资源的浪费,前三次已可保证整个流程完整
性能优化项:浏览器最多为一个域名分配6个TCP连接,可能会造成HTTP请求等待TCP的情况,可使用域名分片技术 在同一站点下挂多个域名,增加TCP连接数
发送 HTTP 请求
TCP连接建立好之后,浏览器就可以与服务器端正式进行通信。浏览器会向服务器发送拼好的报文,等待服务器端响应。报文信息如下:
HTTP响应数据处理
服务器端收到请求报文后,解析请求报文,根据报文信息进行具体的业务操作。再发送一个响应报文给浏览器。
响应报文由响应头加响应体数据组成,响应头又由状态行和头字段构成。整体形式与请求报文类似。
其中状态码的信息很重要。如果状态码是 301/302/307 ,浏览器会进行重定向操作,重新开始构建请求,相当于用户在导航栏重新输入了一个URL地址发起请求如果状态码为 304 则表示命中协商缓存,直接从浏览器本地缓存中读取数据即可。具体各种状态码的含义往后看
响应数据类型也是一个比较重要的项。浏览器会根据Content-Type字段的值来判定是否需要下载(application/octet-stream)、或者正常回显(text/html)等
性能优化项: 若是静态资源,每次都去目标服务器访问则会整个请求流程很慢,可以采用CDN“就近访问”,加快网络下载速度。也可使用各种代理、服务器端缓存技术加快资源下载速度
准备渲染进程
如果HTTP接收的事HTML信息,浏览器则会继续进行导航流程。浏览器会开始准备渲染进程。默认情况下一个tab页签对应一个渲染进程。但是如果新打开的tab页面,与已打开的tab页签是同一个站点(即协议、根域名一致),则会复用现有的渲染进程。
总结来说,打开一个新页面采用的渲染进程策略就是:
- 通常情况下,打开新的页面都会使用单独的渲染进程;
- 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话 ,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程
提交文档
所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:
- 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
- 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
其中,当渲染进程确认提交之后,更新内容如下图所示,旧文档被移除 等待渲染进程新文档的提交。这个过程就是首次渲染白屏时间,整体界面重点优化与此相关。
渲染阶段
文档被提交之后,渲染进程就开始与网络进程建立通信管道,从网络进程那获取到数据开始解析HTML,至此 整个导航流程就完成了。
渲染流程
渲染流程图
渲染流程
整个渲染流程有多个子阶段,从网络接收到字节流开始到渲染完成输出像素,这整一个处理流程叫做 渲染流水线。流水线分为几个子阶段: 构建DOM树、样式计算(Recalculate Style)、计算布局(Layout Tree)、分层(Layer Tree)、绘制(Paint)、分块、光栅化、合成。需要重点关注的三个点
- 每个子节点都有其输入的内容
- 每个子阶段都有其处理过程
- 每个子阶段都有其输出的内容
DOM解析
渲染进程从网络进程接收到HTML开始,就开始解析HTML,边从网络进程获取HTML资源边使用HTML解析器解析HTML。DOM解析阶段主要分为三个阶段: 分词器Token解析、生成Node节点、生成DOM树
样式计算(Recalculate Style)
在没有JS文件的干扰情况下,正常渲染流水线,渲染进程会预解析CSS,在解析DOM的同时对CSS进行解析。CSS样式来源有:
- link引用的外部css文件
- <style>标记内的css
- 元素的style属性内嵌的css
浏览器无法直接理解纯文本的CSS样式,渲染进程需要将这三者来源进行整合,将其解析成浏览器能够理解的结构 -styleSheets(
助理解 CSSOM)。 样式也分为几个阶段: - 把 CSS 转换为浏览器能够理解的结构
- 转换样式表中的属性值,使其标准化、
- 算出 DOM 树中每个节点的具体样式
把 CSS 转换为浏览器能够理解的结构
HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。并且该结构同时具备了查询和修改功能,这会为后面的样式操作提供基础
转换样式表中的属性值,使其标准化
CSS文本虽然转化成了浏览器可以理解的结构,但是CSS很多属性值并不标准,难被渲染引擎理解,此时需要将所有的值转化为渲染引擎容易理解的、标准化的计算值,使所有的属性标准化。方便后续布局树以及图层树的构建。
计算出 DOM 树中每个节点的具体样式
样式属性被标准化后,需要计算DOM树中每个节点的样式属性,主要涉及CSS的继承和层叠规则。
- 继承: CSS 继承就是每个 DOM 节点都包含有父节点的样式
- 层叠: 定义了如何合并来自多个源的属性值的算法
计算布局(LayoutTree)
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
创建布局树
构建完DOM和样式计算之后,虽然分别生成了DOM树和带有元素样式属性的DOM树。但是对于带有元素样式属性属性: display: none等这类节点是不需要显示在浏览器界面上的。此时需要综合DOM树以及DOM的样式属性树(CSSOM)结合生成布局树(LayoutTree)
布局计算
现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容.
创建图层树(LayerTree)
虽然有了布局树,但是布局树(LayoutTree)还存在一些缺陷,页面中的一些复杂3D变化,页面滚动等效果都无法方便的实现处理。渲染引擎还需要为特定的节点生成专门的图层,并生成一棵对应的图层树(Layer Tree)。通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层
要想直观地理解什么是图层,你可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况。
那么需要满足什么条件,渲染引擎才会为特定的节点创建新的图层呢
显示合成
一、层叠上下文属性的元素
层叠上下文也基本上是有一些特定的CSS属性创建的,一般有以下情况:
- HTML根元素本身就具有层叠上下文。
- 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
- 元素的 opacity 值不是 1
- 元素的 transform 值不是 none
- 元素的 filter 值不是 none
- 元素的 isolation 值是isolate
- will-change指定的属性值为上面任意一个。
二、 需要剪裁的地方
比如一个div,你只给他设置 100 * 100 像素的大小,而你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条会被单独提升为一个图层
隐式合成
接下来是隐式合成,简单来说就是层叠等级低的节点被提升为单独的图层之后,那么所有层叠等级比它高的节点都会成为一个单独的图层。
这个隐式合成其实隐藏着巨大的风险,如果在一个大型应用中,当一个z-index比较低的元素被提升为单独图层之后,层叠在它上面的的元素统统都会被提升为单独的图层,可能会增加上千个图层,大大增加内存的压力,甚至直接让页面崩溃。这就是层爆炸的原理
合成层的优点:
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint
绘制(Paint)
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表
也可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表
分块
浏览器显示在显示屏上可见区域叫做视口,但是有的页面会比视口大很多,用户只使用滚动条滚动查看,但每次只能通过视口看到其中的一部分。所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要,基于这个原因,合成线程会将图层划分为图块,这些图块的大小通常是 256x256 或者 512x512
光栅化
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中
提交渲染
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上
浏览器流程问题详解
浏览器缓存
浏览器缓存就是把一个请求过的web资源(html页面,图片,js等)拷贝一份放在浏览器中。浏览器会根据请求保存输入内容的副本,当下一个请求来时,如果是相同的URL,浏览器会根据缓存机制决定直接使用副本响应请求还是向服务器再次发起请求。也可以与服务器交互进行协商,使用特定的浏览器缓存信息。HTTP缓存参考链接
使用浏览器的缓存的原因:
- 减少网络带宽消耗
- 降低服务器压力
- 减少网络延迟,加快页面打开速度,优化用户体验
浏览器缓存机制
当浏览器向服务器请求某个资源时
- 首次时访问时:对于强缓存,服务器会带上响应头Cache-Control或Expires字段。 对于协商缓存,服务器会根据该资源计算一个哈希值,并通过E-Tag和资源传给浏览器缓存在本地,同时服务器也会为该资源返回一个Last-Modify字段,记录该资源最后修改时间。
- 当浏览器再次请求该资源时,会在请求头上带上
If-Modified-Since: 资源最后修改时间,If-None-Match: 缓存哈希值传给服务器。服务器根据请求头信息判断该资源是否已过期,若未过期则返回一个304状态码。若过期则与首次访问一致 返回新的数据与资源给浏览器。
强缓存
强缓存是指用户发送的请求,直接从客户端缓存中获取。不需要再将请求发送给服务器端进行交互。强缓存是利用http头的Expires和Cache-Control来控制的,浏览器会根据这两个值来判定目标资源是否“命中”强缓存,再与服务器端交互。
-
Expries
Expires响应头包含日期/时间, 即在此时候之后,响应过期。因为Expires字段由服务器端传给客户端,最后由客户端拿这个字段的值与客户端本地时间去对比。但当前存在的问题是,服务器端与客户端时间不一致时,则会出现缓存使用错误,或者没有使用缓存的情况。如果在
Cache-Control响应头设置了 "max-age" 或者 "s-max-age" 指令,那么Expires头会被忽略。 -
Cache-Control
Cache-Control通用消息头字段,被用于在http请求和响应中,通过指定指令来实现缓存机制。缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中。Cache-Control中的“max-age”使用过期时长来优化Expires时间点,Cache-Control的指令特别多,用途也很广泛。以下常用关键的指令:
| 指令 | 类型 | 具体含义 |
|---|---|---|
public | 可缓存性 | 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容 |
private | 可缓存性 | 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器 |
no-cache | 可缓存性 | 在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证) |
no-store | 可缓存性 | 缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存 |
max-age | 到期 | 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间 |
s-maxage | 到期 | 覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它 |
must-revalidate | 重新验证 | 一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求 |
协商缓存
用户发送请求后,服务器收到根据收到的报文信息来判定这个请求的资源,是否可以从浏览器缓存中获取。协商缓存的响应头字段也有两个: Last-Modified和If-Modified-Since、 E-Tag和If-None-Match
- Last-Modified和If-Modified-Since
浏览器第一次请求一个资源的时候,服务器返回的Header中会加上
Last-Modify,Last-Modify是一个时间标识该资源的最后修改的时间,例如Last-Modify:Thu,31 Dec 2037 23:59:59 GMT
缺点:针对一些修改特别频繁的文件,秒级以下的。If-Modified-Since能检查到的粒度是s级的,这种修改无法判断。可能会导致拿不到最新的文件
- E-Tag和If-None-Match
E-Tag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Etag/If-None-Match返回的是一个校验码。Etag可以保证每一个资源都是唯一的,资源变化会导致ETag变化。服务器会根据浏览器上的If-None-Match来判断是否命中缓存
状态码
- 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
- 2××:成功,报文已经收到并被正确处理;
- 3××:重定向,资源位置发生变动,需要客户端重新发送请求;
- 4××:客户端错误,请求报文有误,服务器无法处理;
- 5××:服务器错误,服务器在处理请求时内部发生了错误。
其中一些重要的状态码单独列出,在日常与服务器端打交道的过程中,需要正确理解状态码的意义,且需要按规范返回状态码。
| 状态码 | 含义 |
|---|---|
| 200 OK | 成功状态码,如果是非HEAD请求,响应头一般都会有body数据 |
| 204 No Content | 成功状态码,但是响应头后没有body数据 |
| 206 Partial Content | 成功状态码,HTTP 分块下载或断点续传的基础,有body数据 但只是资源的一部分 |
| 301 Moved Permanently | 永久重定向”,含义是此次请求的资源已经不存在了,需要改用新的 URI 再次访问,会返回一个 Location字段用于跳转 |
| 302 Found | “临时重定向”,下次可继续访问 可能在网站升级 会返回一个 Location字段用于跳转 |
| 304 Not Modified | 配合请求头,用户缓存控制 协商缓存 |
| 304 Not Modified | 临时重定向,不可改变请求方法 |
| 403 Forbidden | 服务器禁止访问资源 |
| 404 Not Found | 资源在本服务器上未找到,所以无法提供给客户端 |
| 502 Bad Gateway | 客户端请求的功能还不支持 |
| 503 Service Unavailable | 表示服务器当前很忙,暂时无法响应服务,临时状态 |
JavaScript阻塞DOM树构建
CSS间接阻塞DOM树构建
浏览器安全
web页面安全
在没有安全保障的 Web 世界中,我们是没有隐私的,因此需要安全策略来保障我们的隐私和数据的安全。这就引出了页面中最基础、最核心的安全策略:同源策略(Same-origin policy)
同源策略
如果两个URL的协议、域名和端口都相同,我们称这两个URL同源。浏览器默认两个相同的源之间是可以互相访问资源和操作DOM的。同源策略主要表现在DOM层面、Web数据、网络层面这三个层面
DOM层面: 同源策略限制了来自不同源的JavaScript脚本对当前DOM对象的读和写的操作。
Web数据:同源策略限制了不同源站点读取当前站点的Cookie、IndexDB、LocalStorage等数据
网络层面: 同源策略限制了通过XMLHttpRequest、fetch等方式将站点的数据发送给不同源的站点。
安全和便利性的权衡
如果让不同源之间绝对隔离,虽然这个措施非常安全,但是这也会是Web项目难以开发和使用,比如 目前script引入一些第三方插件等将会无法生效。为了能让Web依然是灵活、开发的。浏览器让出了一些安全性来满足灵活性,这也带来了很多安全问题,比如:XSS攻击、CSRF攻击
DOM层面出让:
出让原因: 实际应用中,经常需要两个不同源的DOM之间进行通信出让措施: 浏览器加入了跨文档消息机制,可以通过window.postMessage的JavaScript接口和不同源的DOM进行通信导致的问题
Web数据出让
-
出让原因:同源策略要求页面所有资源来自一个源,但是对于不同资源部署到CDN上等问题就有一个很大的限制,此时就需要出让一些安全性: -
出让措施: 支持使用<script>引入外部资源 -
导致问题: XSS攻击:外部脚本恶意读取Cookie信息,进行恶意攻击 -
防范措施:浏览器引入的内容安全策略CSP,CSP让服务器决定浏览器可以加载哪些资源,决定浏览器是否能够执行内联JavaScript代码, Cookie设置httpOnly
网络层面出让
-
出让原因: 比较常见的,在前后端联调过程中,浏览器和服务器不在一个源上,但仍需要进行请求访问 -
出让措施: 浏览器引入了跨域资源共享,使用该机制进行跨域访问控制,使得跨域数据得以安全进行 -
导致的问题: CSRF攻击,CSRF盗用cookie 跨域进行请求等 -
防范措施:服务器进行referer检查、Cookie设置same-site、CSRF token
跨域处理
- JSONP: 使用浏览器对web数据方面的安全性出让,可以使用内部script标签包裹get请求
- CORS: 需要浏览器配合
- 反向代理
浏览器网络安全
使用 HTTP 传输的内容很容易被中间人窃取、伪造和篡改,通常我们把这种攻击方式称为中间人攻击。在将 HTTP 数据提交给 TCP 层之后,数据会经过用户电脑、WiFi 路由器、运营商和目标服务器,在这中间的每个环节中,数据都有可能被窃取或篡改。比如用户电脑被黑客安装了恶意软件,那么恶意软件就能抓取和篡改所发出的 HTTP 请求的内容。或者用户一不小心连接上了 WiFi 钓鱼路由器,那么数据也都能被黑客抓取或篡改
鉴于 HTTP 的明文传输使得传输过程毫无安全性可言,且制约了网上购物、在线转账等一系列场景应用,于是倒逼着我们要引入加密方案。从 HTTP 协议栈层面来看,我们可以在 TCP 和 HTTP 之间插入一个安全层,所有经过安全层的数据都会被加密或者解密
浏览器系统安全
渲染进程需要执行 DOM 解析、CSS 解析、网络图片解码等操作,如果渲染进程中存在系统级别的漏洞,那么以上操作就有可能让恶意的站点获取到渲染进程的控制权限,进而又获取操作系统的控制权限,这对于用户来说是非常危险的
因为网络资源的内容存在着各种可能性,所以浏览器会默认所有的网络资源都是不可信的,都是不安全的。基于以上原因,我们需要在渲染进程和操作系统之间建一道墙,即便渲染进程由于存在漏洞被黑客攻击,但由于这道墙,黑客就获取不到渲染进程之外的任何操作权限。将渲染进程和操作系统隔离的这道墙就是我们要聊的安全沙箱,限制渲染进程的权限
持久存储
由于安全沙箱需要负责确保渲染进程无法直接访问用户的文件系统,但是在渲染进程内部有访问 Cookie 的需求、有上传文件的需求,为了解决这些文件的访问需求,所以现代浏览器将读写文件的操作全部放在了浏览器内核中实现,然后通过 IPC 将操作结果转发给渲染进程。
- Cookie数据、localStorage、sessionStorage的读取
- 缓存文件的读取,比如网络文件缓存
网络访问
同样有了安全沙箱的保护,在渲染进程内部也是不能直接访问网络的,如果要访问网络,则需要通过浏览器内核。不过浏览器内核在处理 URL 请求之前,会检查渲染进程是否有权限请求该 URL,比如检查 XMLHttpRequest 或者 Fetch 是否是跨站点请求,或者检测 HTTPS 的站点中是否包含了 HTTP 的请求
用户交互
渲染进程实现了安全沙箱,还影响到了一个非常重要的用户交互功能。由于渲染进程不能直接访问窗口句柄,所以渲染进程需要完成以下两点大的改变
-
渲染进程需要渲染出位图。了向用户显示渲染进程渲染出来的位图,渲染进程需要将生成好的位图发送到浏览器内核,然后浏览器内核将位图复制到屏幕上
-
操作系统没有将用户输入事件直接传递给渲染进程,而是将这些事件传递给浏览器内核。 然后浏览器内核再根据当前浏览器界面的状态来判断如何调度这些事件,如果当前焦点位于浏览器地址栏中,则输入事件会在浏览器内核内部处理;如果当前焦点在页面的区域内,则浏览器内核会将输入事件转发给渲染进程
之所以这样设计,就是为了限制渲染进程有监控到用户输入事件的能力,所以所有的键盘鼠标事件都是由浏览器内核来接收的,然后浏览器内核再通过 IPC 将这些事件发送给渲染进程
安全问题
当页面被注入了恶意 JavaScript 脚本时,浏览器无法区分这些脚本是被恶意注入的还是正常的页面内容,所以恶意注入 JavaScript 脚本也拥有所有的脚本权限。下面我们就来看看,如果页面被注入了恶意 JavaScript 脚本,恶意脚本都能做哪些事情。
-
可以窃取 Cookie 信息。恶意 JavaScript 可以通过“document.cookie”获取 Cookie 信息,然后通过 XMLHttpRequest 或者 Fetch 加上 CORS 功能将数据发送给恶意服务器;恶意服务器拿到用户的 Cookie 信息之后,就可以在其他电脑上模拟用户的登录,然后进行转账等操作
-
可以监听用户行为。恶意 JavaScript 可以使用“addEventListener”接口来监听键盘事件,比如可以获取用户输入的信用卡等信息,将其发送到恶意服务器
-
可以通过修改 DOM 伪造假的登录窗口,用来欺骗用户输入用户名和密码等信息
-
还可以在页面内生成浮窗广告,这些广告会严重地影响用户体验
XSS攻击
XSS 全称是 Cross Site Scripting,为了与“CSS”区分开来,故简称 XSS,翻译过来就是“跨站脚本”。XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。主要有存储型 XSS 攻击、反射型 XSS 攻击和基于 DOM 的 XSS 攻击三种方式来注入恶意脚本。
无论是何种类型的 XSS 攻击,它们都有一个共同点,那就是首先往浏览器中注入恶意脚本,然后再通过恶意脚本将用户信息发送至黑客部署的恶意服务器上
攻击类型
- 存储型 XSS 攻击:
存储型,顾名思义就是将恶意脚本存储了起来,确实,存储型的 XSS 将脚本存储到了服务端的数据库,然后在客户端执行这些脚本,从而达到攻击的效果 - 反射型 XSS攻击:
反射型XSS指的是恶意 JavaScript 脚本属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript 脚本返回给用户。当恶意 JavaScript 脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作 - 基于DOM的 XSS攻击: 黑客通过各种手段将恶意脚本注入用户的页面中。比如通过网络劫持在页面传输过程中修改 HTML 页面的内容
防范手段
- 服务器对输入脚本进行过滤或转码
- 充分利用 CSP: 限制第三方资源脚本的执行
- 使用 HttpOnly 属性:由于 JavaScript 无法读取设置了 HttpOnly 的 Cookie 数据,所以即使页面被注入了恶意 JavaScript 脚本,也是无法获取到设置了 HttpOnly 的数据。
CSRF攻击
CSRF 英文全称是 Cross-site request forgery,所以又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。
和 XSS 不同的是,CSRF 攻击不需要将恶意代码注入用户的页面,仅仅是利用服务器的漏洞和用户的登录状态来实施攻击。
攻击类型
- 自动发起 Get 请求: 比如:黑客将转账的请求接口隐藏在 img 标签内,欺骗浏览器这是一张图片资源
- 自动发起 POST 请求: 使用构建自动提交表单这种方式,就可以自动实现跨站点 POST 数据提交
- 引诱用户点击链接: 诱惑用户点击黑客站点上的链接,这种方式通常出现在论坛或者恶意邮件上
防范手段
- 充分利用好 Cookie 的 SameSite 属性:限制其他域读取Cookie信息
- 验证请求的来源站点: 在服务器端验证请求来源的站点,验证请求头中的“Referer”、“Origin”字段
- CSRF Token:服务器生成一个随机 CSRF Token。CSRF Token 其实就是服务器生成的字符串,然后将该字符串植入到返回的页面中
页面整体性能优化
网络层面
-
HTTP协议升级: 升级HTTP2,实现了头部压缩、流与多路复用
-
缓存: DNS缓存、代理缓存、网关缓存、CDN就近响应、浏览器缓存、使用负载均衡提高浏览器的响应速度、redis缓存等
-
域名分片: 浏览器限制TCP连接数,可使用多域名访问静态资源,增加TCP连接数
-
降低服务器带宽消耗: 文件压缩,细小文件合并降低请求个数
-
代码等层面: css in js原子化;精简代码,删除注释; 压缩代码,代码的复杂度;按需引入; tree-shaking(需要继续研究)等技术
webpack -
对一些防抖、节流,减少服务器请求次数
渲染流程层面
-
骨架屏,提高用户体验
-
图片懒加载,资源预加载,减少网络竞争
-
无关js使用defer async延迟执行,避免阻塞渲染进程
-
适当使用合成层,减少页面重绘 重排操作
-
css js引入,css尽量放头部让渲染进程同步预解析,多个css可按照对应的功能 比如横屏、竖屏 按需分开引入
-
js代码优化,减少布局抖动和强制布局