0%

浏览器相关知识

1. 浏览器缓存

强缓存&协商缓存

强缓存:不需要发送HTTP请求

协商缓存:需要发送HTTP请求

强缓存

不需要发送HTTP请求。

HTTP/1.0:Expires

HTTP/1.1:Cache-Control

Expires

Expires即过期时间,存在于服务端返回的响应头中,告诉浏览器这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。

image-20220315094313721

image-20220315095603481

表示资源过期的时间。

问题是服务器的时间可能和浏览器的时间不一致,那么服务器返回的这个时间可能是不准确的。

在HTTP/1.1被抛弃

Cache-Control

与Expires的区别就是它没有采用具体的过期时间点的形式,而是采用过期时长来控制缓存。max-age

image-20220315095013941

可以组合非常多的指令,完成更多场景的缓存判断:

public

客户端和代理服务器都可以缓存。因为一个请求要经过不同的代理服务器才能到达目标服务器,所以不仅仅浏览器,中间的任何代理节点都可以缓存。

image-20220315100135600

private

只有浏览器能缓存,中间的代理服务器不能缓存。

image-20220315100614544

no-cache

跳过当前强缓存,直接发送HTTP请求,进入协商缓存阶段。

image-20220315100858357

s-maxage

针对代理服务器的缓存时间。

当Expires和Cache-Control同时存在的时候,Cache-Control会优先考虑。

强缓存失效了,就会进入协商缓存。

协商缓存

强缓存失效之后,浏览器在请求头中携带相应的缓存tag来向服务器发送请求,由服务器根据tag来决定是否使用缓存,这就是协商缓存。

缓存tag分为两种:Last-Modified和ETag

image-20220315102212062

Last-Modified

最后修改时间。浏览器第一次向服务器发送请求后,服务器会在响应头中加上这个字段。

浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。

服务器拿到请求头中的If-Modified-Since字段后,会和这个服务器中该资源最后修改时间对比:

如果这个值小于最后修改时间,说明是时候更新了,返回新的资源,和常规的HTTP请求流程一样。

否则返回304,告诉浏览器直接用缓存。

ETag

ETag是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值传给浏览器。

浏览器收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。

服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:

如果两者不一样,说明要更新了。返回新的资源,和常规的HTTP请求响应的流程一样。

否则返回304,告诉浏览器直接用缓存。

两者对比
  1. 在精准度上,ETag优于Last-Modified。优于ETag是按照内容给资源上标识,因此能准确感知资源的变化。而Last-Modified就不一样了,在以下情况不能准确感知变化:
    • 编辑了资源文件,但是文件内容没有更改,会造成缓存失效
    • Last-Modified能够感知的时间是秒,如果文件在1秒内改变了多次,那么这时候的Last-Modified就无法体现修改。
  2. 在性能上,Last-Modified优于ETag,也很简单理解,Last-Modified仅仅只是记录一个时间点,而ETag需要根据具体的文件内容生成哈希值。

如果两种方式都支持,服务器优先考虑ETag。

缓存位置

当强缓存命中或者协商缓存服务器返回304,直接从缓存的位置获取资源。

Service Worker、Memory Cache、Disk Cache、Push Cache

Service Worker

image-20220315135819839

Service Worker借鉴了Web Worker的思路,即让JS运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。可以完成离线缓存、消息推送和网络代理的功能。其中离线缓存靠的是Service Worker Cache。离线缓存就是Service Worker Cache。

也是PWA的重要实现机制。

image-20220315140655167

Memory Cache和Disk Cache

Memory Cache

内存缓存,效率上最快。存活时间最短,当渲染进程结束后,内存缓存不存在了。

Disk Cache

是磁盘缓存,存取效率慢,优势在存储容量和时长。

使用策略

比较大的js、css放入磁盘,否则放入内存

内存占用率高的时候,放入磁盘

Push Cache

推送缓存,是HTTP/2的内容

总结

  1. 首先通过Cache-Control验证强缓存是否可用
    1. 如果强缓存可用直接使用
    2. 否则进入协商缓存,发送HTTP请求,服务器通过请求头中的If-Modified-Since或者If-None-Match检查资源是否更新
      1. 如果资源更新返回200,和资源
      2. 否则返回304告诉浏览器直接从缓存获取

2. 浏览器本地存储

主要分为Cookie、WebStorage(localStorage和sessionStorage)和indexedDB。

Cookie最开始不是做本地存储的,而是为了弥补HTTP在状态管理上的不足。

HTTP协议是一个无状态协议,客户端向服务器发请求,服务器返回响应,下次发请求如何让服务端知道客户端是谁?

因此就产生了Cookie。

Cookie本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对存储。

向同一个域名下发送请求,都会携带相同的Cookie,服务器拿到Cookie进行解析,就能拿到客户端的状态。

Cookie的缺陷

  1. 容量缺陷。容量只有4KB,只能存储少量信息。

  2. 性能缺陷。不管域名下面的某个地址是否需要这个Cookie,请求都会携带完整的Cookie,因此请求数增多会造成巨大的性能浪费。

  3. 安全缺陷。以纯文本形式传递,很容易被非法用户截获并篡改,相当危险。在HttpOnly为false情况下,Cookie信息能够通过js脚本直接读取。

localStorage

localStorage和Cookie的异同点

和Cookie一样,对同一个同一个域名都会有相同的localStorage。

区别:

  1. 容量。上限5M,比Cookie的4K大。5M针对某个域名,永久存储。

  2. 只存在客户端,默认不参与服务端的通信。避免了Cookie带来的性能和安全问题。

  3. 接口封装。通过localStorage暴露在全局,通过setItem和getItem进行操作,非常方便。

操作方式

接着进入相同的域名时就能拿到相应的值:

localStorage存储的都是字符串,如果是存储对象需要调用JSON的stringfy方法,并且用JSON.parse解析成对象。

应用场景

利用localStorage的较大容量和持久特性,可以利用localStorage存储一些内容稳定的资源,比如官网的logo,存储Base64格式的图片资源。

sessionStorage

sessionStorage和localStorage的异同

一致:

  1. 容量。上限也是5M。

  2. 只存在客户端,不参与服务端通信。

  3. 接口封装。除了sessionStorage名字有所变化,存储方式、操作方式均和localStorage一样。

区别:

sessionStorage只是会话级别的存储,不是持久化的存储。会话结束,也就是页面关闭,这部分sessionStorage也就不存在了。

应用场景

  1. 可以用它对表单信息进行维护,将表单信息存储在里面,保证页面即使刷新也不会让之前的表单信息丢失。

  2. 可以用它存储本地浏览记录。如果关闭页面后不需要,那么sessionStorage就很合适。

IndexedDB

IndexedDB是运行在浏览器中的非关系型数据库,本质上是数据库,理论上容量没有上限。

IndexedDB重要特性,除了拥有数据库本身的特性,除了拥有数据库本身的特性,比如支持事务、存储二进制数据,还有这些特性:

  1. 键值对存储。内部采用对象仓库存放数据,在这个对象仓库中数据采用键值对的方式来存储。

  2. 异步操作。数据库的读写属于I/O操作,浏览器中对异步I/O提供了支持。

  3. 受同源策略限制,即无法访问跨域的数据库。

总结

  1. cookie并不适合存储,而且存在非常多的缺陷。

  2. Web Storage包括localStorage和sessionStorage,默认不会参与和服务器的通信。

  3. IndexedDB为运行在浏览器上的非关系型数据库,为大型数据的存储提供了接口。

3. 从输入URL到页面呈现发生了什么?

在浏览器地址栏输入百度地址:https://www.baidu.com/

构建请求

浏览器会构建请求行:

1
2
// 请求方法是GET,路径为根路径,HTTP协议版本为1.1
GET / HTTP/1.1

查找强缓存

先检查强缓存,如果命中直接使用,否则进入下一步。强缓存见前面。

DNS解析

由于输入的是域名,数据包是通过IP地址传给对方。需要得到域名对应的IP地址。这个过程需要依赖一个服务系统,这个系统将域名和IP一一映射,这个系统就叫DNS(域名系统,domain name system)。得到具体IP的过程就是DNS解析。

浏览器提供了DNS数据缓存功能。即一个域名如果解析过,那会把解析的结果缓存下来,下次处理直接走缓存,不需要经过DNS解析。

如果不指定端口的话,默认采用对应IP的80端口。

建立TCP连接

Chrome在同一个域名下要求同时最多有6个TCP连接,超过6个的话剩下的请求就得等待。

假如现在不需要等待,进入了TCP连接的建立阶段。首先解释一下什么是TCP:

TCP(transmission control protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

建立TCP连接经历了下面三个阶段:

  1. 通过3次握手(总共发送3个数据包确认已经建立连接)建立客户端和服务器之间的连接。

  2. 进行数据传输。这里有一个重要的机制,接收方接收到数据包之后必须要向发送方确认,如果发送方没有接到这个确认的消息,就判定为数据包丢失,并重新发送该数据包。发送的过程还有一个优化的策略,把大的数据包拆成一个个小的数据包,一次传输到接收方,接收方按照这个小包的顺序把它们组装成完整数据包。

  3. 断开连接的阶段。数据传输完成,靠四次挥手来断开连接。

TCP保证数据传输的可靠性的手段:1. 三次握手确认连接;2. 数据包校验保证数据到达接收方;3. 四次挥手断开连接。

关于握手的深入研究:https://zhuanlan.zhihu.com/p/86426969

发送HTTP请求

TCP连接建立完毕,浏览器可以和服务器开始通信,开始发送HTTP请求。浏览器发HTTP请求要携带三样东西:请求行、请求头和请求体。

  1. 首先,浏览器会向服务器发送请求行,之前构建请求的时候就构建了。

    1
    2
    // 请求方法是GET,路径为根路径,HTTP协议版本为1.1
    GET / HTTP/1.1

    结构很简单,由请求方法、请求URI和HTTP版本协议组成。

  2. 同时也要带上请求头,比如Cache-Control、If-Modified-Since、If-None-Match都有可能放入请求头中作为缓存的标识信息。还有其他一些属性:

  3. 最后是请求体,只有在POST方法下存在,常见的场景是表单提交。

网络响应

HTTP请求到达服务器,服务器进行对应的处理。最后要把数据传给浏览器,也就是返回网络响应。

跟请求部分类似,网络响应具有三个部分:响应行、响应头和响应体。

响应行类似下面这样:

1
HTTP/1.1 200 OK

由HTTP协议版本、状态码和状态描述组成。

响应头包含了服务器及其返回数据的一些信息,服务器生成数据的时间、返回的数据类型以及对即将写入的Cookie信息。

举例如下:

响应完成之后TCP不一定断开。这时候要判断Connection字段,如果请求头或者响应头中含有Connection: Keep-Alive,表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。否则断开TCP连接,请求-响应流程结束。

总结

总结浏览器端的网络请求过程:

4. 从输入URL到页面呈现发生了什么?——解析算法篇

完成了网络请求和响应,如果响应头中Content-Type的值是text/html,那么接下来就是浏览器的解析和渲染工作了。

首先来介绍解析部分,主要分为以下几个步骤:构建DOM树、样式计算、生成布局树(Layout Tree)。

构建DOM树

由于浏览器无法直接理解HTML字符串,因此将这一系列的字节流转换为一种有意义并且方便操作的数据结构,这种数据结构就是DOM树。DOM树本质上是一个以document为根节点的多叉树。

HTML文法的本质

HTML的文法并不是上下文无关文法。

规范的HTML语法,是符合上下文无关文法的,能够体现它非上下文无关的是不标准的语法。

比如解析器扫描到form标签的时候,上下文无关文法的处理方式是直接创建对应form的DOM对象,而真实的HTML5场景中却不是这样,解析器会查看form标签的上下文,如果这个form标签的父标签也是form,那么直接跳过当前的form标签,否则才创建DOM对象。

常规的编程语言都是上下文无关的,而HTML却相反,也真是它非上下文无关的特性,决定了HTML Parser并不能使用常规编程语言的解析器来完成,需要另辟蹊径。

解析算法

HTML5规范详细地介绍了解析算法。这个算法分为两个阶段:标记化、建树。

对应的两个过程就是词法分析和语法分析。

标记化算法

这个算法输入为HTML文本,输出为HTML标记,也称为标记生成器。其中运用有限自动状态机来完成。即在当前状态下,接收一个或多个字符,就会更新到下一个状态。

建树算法

DOM树是一个以document为根节点的多叉树。因此解析器首先会创建一个document对象。标记生成器会把每个标记的信息发送给建树器。建树器接收到相应的标记时,会创建对应的DOM对象。创建这个DOM对象后会做两件事情:1. 将DOM对象加入DOM树中;2. 将对应标记压入存放开放(与闭合标签意思对应)元素的栈中。

容错机制

HTML5强大的容错策略,容错能力非常强。

样式计算

关于CSS样式,来源一般是三种:

  1. link标签引用

  2. style标签中的样式

  3. 元素的内嵌style属性

格式化样式表

浏览器是无法识别css文本的,渲染引擎接收到css文本之后第一件事情就是将其转化为一个结构化的对象,即styleSheets。

格式化的过程过于复杂,对于不同的浏览器会有不同的优化策略。

在浏览器控制台可以通过document.styleSheets来查看这个最终的结构。

标准化样式属性

有一些css样式的数值并不容易被渲染引擎所理解,因此需要在计算样式之前将他们标准化,如em->px,red->#ff0000,bold->700等。

计算每个节点的具体样式

样式已经被格式化和标准化,接下来可以计算每个节点的具体样式信息。

两个规则:继承和层叠。

每个子节点都会默认继承父节点的样式属性,如果父节点中没有找到,就会采用浏览器默认样式,也叫UserAgent样式。这就是继承规则,非常容易理解。

然后是层叠规则,css最大的特点在于它的层叠性,也就是最终的样式取决于各个属性共同作用的效果。

计算完样式后,所有样式会被挂载到window.getComputedStyle上,可以通过js获取。

生成布局树

现在已经生成了DOM树和DOM样式,接下来通过浏览器的布局系统确定元素的位置,生成一棵布局树。

布局树生成的大致工作:

  1. 遍历生成的DOM树节点,并把它们添加到布局树中。

  2. 计算布局树节点的坐标位置。

布局树只包含可见元素,对于head标签和设置了display:none的元素,将不会被放入其中。

Chrome现在已经没有生成Render Tree的功能。

从Chrome源码看浏览器如何layout布局从Chrome源码看浏览器如何layout布局 – 人人FED

总结

5. 从输入URL到页面呈现发生了什么?——渲染过程篇

渲染分为以下几个步骤:

  1. 建立图层树(Layer Tree)

  2. 生成绘制列表

  3. 生成图块并栅格化

  4. 显示器显示内容

建图层树

一些复杂的场景,比如3D动画如何呈现出变换效果,当元素含有层叠上下文时如何控制显示和隐藏。所以浏览器在构建完布局树之后,还会对特定的节点进行分层,构建一棵图层树(Layer Tree)。

一般情况下,节点的图层默认属于父亲节点的图层(称为合成层)。什么时候能提升成为一个单独的合成层呢?

有两种分别讨论的情况:显式合成、隐式合成。

显式合成

显式合成的情况:

  1. 拥有层叠上下文的节点。

    1. HTML根元素本身就具有层叠上下文。

    2. 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。

    3. 元素的opacity值不是1。

    4. 元素的transform值不是none。

    5. 元素的filter值不是none。

    6. 元素的isolation值是isolate。

    7. will-change指定的属性值为上面任意一个。

  2. 需要剪裁的地方。比如一个div只给他设置100*100的大小,而在里面放了非常多的文字,超出的文字部分就要被剪裁。出现了滚动条,滚动条会被单独提升为一个图层。

隐式合成

层叠等级低的节点被提升为单独的图层后,所有层叠等级高的节点都会成为一个单独的图层。

这个隐式合成隐藏着巨大的风险,当一个z-index较低的图层被提升为单独的图层后,层叠在上面的元素都会被提升为单独的图层,可能会增加上千个图层,增加内存压力,让页面崩溃。这就是层爆炸。

需要repaint时,只需要repaint本身,不会影响到其他层。

生成绘制列表

渲染引擎会将图层的绘制拆分成一个个绘制指令,比如先画背景再描绘边框。然后将这些指令按顺序组合成一个待绘制列表,相当于给后面的绘制操作做了一波计划。

生成图块和生成位图

渲染进程中绘制操作由专门的线程完成的,这个线程叫合成线程。绘制列表准备好之后,渲染进程的主线程会给合成线程发送commit消息,把绘制列表提交给合成线程。

当页面非常大需要滑动的时候,全部绘制出来很浪费性能,将页面分块可以加速页面的首屏展示。

即使绘制一个图块也会耗时,所以Chrome首次合成先合成低分辨率图片,然后正常图块绘制完毕后,将低分辨率的替换。

渲染过程中专门维护了一个栅格化线程池,专门负责把图块转换为位图数据。然后合成线程会选择视口附近图块交给栅格化线程池生成位图。

生成位图的过程实际上都会使用GPU进行加速,生成的位图最后发送给合成线程。

显示器显示内容

栅格化操作完成后,合成线程会生成一个绘制命令,即“DrawQuad”,并发送给浏览器进程。

浏览器的viz组件接收到命令,根据命令发送到内存,再发送到显卡。

屏幕都有一个固定的刷新频率。

总结

6. 对重绘和回流的理解

渲染流水线的流程:

回流

回流也叫重排。

触发条件

对DOM结构修改引发DOM几何尺寸变化的时候,会发生回流的过程。

以下的操作会触发回流:

  1. 一个DOM元素的几何属性变化,width、height、padding、margin、left、top、border。

  2. DOM节点发生增减或移动。

  3. 读写offset族、scroll族和client族属性的时候,浏览器为了获取这些值,需要进行回流操作。

  4. 调用window.getComputedStyle方法。

回流过程

触发回流的时候,如果DOM结构发生改变,重新渲染DOM树,然后将后面的流程全部走一遍。开销非常大。

重绘

触发条件

当DOM修改导致样式变化,并且没有影响几何属性的时候,会导致重绘(repaint)。

重绘过程

没有导致DOM几何属性的变化,元素的位置信息不需要更新,省去了布局的过程。流程如下:

跳过生成布局树和建图层树,直接绘制列表,然后继续进行分块生成位图等后面一系列操作。

重绘不一定导致回流,回流一定导致重绘。

合成

CSS3的transform、opacity、filter这些属性可以实现合成的效果,也就是GPU加速。

GPU加速的原因

在合成的情况下,直接跳过布局和绘制流程,直接进入非主线程处理的部分,直接交给合成线程处理。交给它处理的好处:

  1. 能够充分发挥GPU的优势。合成线程生成位图的过程中会调用线程池,并在其中使用GPU进行加速生成,GPU擅长处理位图数据。

  2. 没有占用主线程的资源,即使主线程卡住了,效果依然能够流畅地展示。

实践意义

  1. 避免频繁使用style,而是采用修改class的方式。

  2. 使用createDocumentFragment进行批量的DOM操作。

  3. 对resize、scroll进行防抖/节流处理。

  4. 添加will-change: tranform,让渲染引擎为其单独实现一个图层,当这些变换发生时,仅仅需要合成线程处理这些变换,而不牵扯到主线程,提高渲染效率。

7. XSS攻击

XSS攻击的定义

XSS全称是Cross Site Scripting(跨站脚本)。XSS攻击是指浏览器中执行恶意脚本(无论跨域还是同域),从而拿到用户的信息并进行操作。这些操作可以:1. 窃取cookie;2. 监听用户行为;比如输入账号密码后直接发送到服务器;3. 修改DOM伪造登录表单;4. 页面中生成浮窗广告。

XSS攻击的三种实现方式:存储型、反射型、文档型。

存储型

将恶意脚本存储到服务端的数据库,然后在客户端执行这些脚本,从而达到攻击的效果。

在留言评论区提交一段脚本代码,在页面渲染过程中直接执行,相当于直接执行一段未知逻辑的XSS代码。

反射型

恶意脚本作为网络请求的一部分。

1
http://sanyuan.com?q=<script>alert("你完蛋了")</script>

服务器端会拿到q参数,然后将内容返回到浏览器端,浏览器将这些内容作为HTML的一部分解析,发现是一个脚本,直接执行,这样就被攻击了。

恶意脚本通过作为网络请求的参数,经过服务器,反射到HTML文档中,执行解析。服务器并不会存储这些恶意脚本。

文档型

XSS攻击并不会经过服务端,而是作为中间人,传书过程中劫持数据包,修改里面的html文档。包括WIFI路由器劫持和本地恶意软件。

防范措施

  1. 不要相信任何用户的输入,无论在前端还是服务端都要对用户的输入进行转码或过滤。

    1
    2
    3
    <script>alert('你完蛋了')</script>
    // 转为
    <script>alert(&#39;你完蛋了&#39;)</script>

    也可以利用关键词过滤的方式,将script标签删除。

  2. 利用CSP,即浏览器中的内容安全策略。核心思想是服务器决定加载哪些资源。

    1. 限制其他域下的资源加载。

    2. 禁止向其他域提交数据。

    3. 提供上报机制,帮助我们及时发现XSS攻击。

  3. 利用HttpOnly。XSS攻击很多是窃取Cookie,设置Cookie的HttpOnly属性后,JS无法读取Cookie值,这样防范了攻击。

8. CSRF攻击

CSRF攻击的定义

CSRF(Cross-site request forgery),跨站请求伪造,黑客诱导用户打开链接,打开黑客的网站,然后利用用户目前的登录状态发起跨站请求。

攻击方式:

自动发GET请求

1
<img src="https://xxx.com/info?user=hhh&count=100">

进入页面后自动发送get请求,会自动带上xxx.com的cookie信息。假如服务器端没有相应的验证机制,可能认为发请求的是一个正常用户,因为携带了相应的cookie,然后进行相应的操作,可能是转账汇款相关的操作。

自动发POST请求

黑客可能自己填一个表单,写一段自动提交的脚本。

1
2
3
4
5
<form id='hacker-form' action="https://xxx.com/info" method="POST">
<input type="hidden" name="user" value="hhh" />
<input type="hidden" name="count" value="100" />
</form>
<script>document.getElementById('hacker-form').submit();</script>

同样也会携带相应的用户cookie信息,让服务器误以为是一个正常的用户在操作,让各种恶意的操作成为可能。

诱导点击发送GET请求

黑客的网站上可能放一个链接诱导你来点击:

1
<a href="https://xxx/info?user=hhh&count=100" taget="_blank">点击进⼊修仙世界</a>

点击后自动发get请求。然后和自动发GET请求部分同理。

和XSS攻击相比,CSRF并不要将恶意代码注入到用户当前页面的html中,而是跳转到新的页面,利用服务器的验证漏洞和用户之前的登录状态模拟用户操作。

防范措施

利用Cookie的SameSite属性

CRSF攻击重要的一环就是自动发送目标站点下的Cookie,这份Cookie模拟了用户的身份。

Cookie中有一个关键字段SameSite,可以对请求中Cookie携带做一些限制。

SameSite有三个值,Strict、Lax和None。

  1. Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com才能请求携带Cookie,在其他网站请求都不能。

  2. 在Lax模式,只有get方法提交表单或者a标签发送get请求的情况下可以携带Cookie,其他情况不能。

  3. None模式,默认模式,请求会自动携带Cookie。

验证来源站点

用到请求头的两个字段:Origin和Referer。

Origin只包含域名信息,Referer包含具体的URL路径。

这两者都可以伪造,通过Ajax自定义请求头,安全性略差。

CSRF Token

浏览器向服务器发送请求时,服务器生成一个字符串,植入到返回的页面中。浏览器如果要发送请求,就必须带上这个字符串,然后服务器验证是否合法,如果不合法不予响应。

这个字符串是CSRF Token,通过第三方站点无法拿到,会被服务器拒绝。

9. HTTPS为什么让数据传输更安全?

HTTP的特性是明文传输,传输的每个环节数据都可能被第三方窃取或者篡改。HTTP数据经过TCP层,经过WIFI路由器、运营商和目标服务器,都可能被中间人拿到数据篡改,就是中间人攻击。

新的加密方案HTTPS。

HTTPS不是一个新的协议,而是一个加强版的HTTP,在HTTP和TCP之间建立了一个中间层,不是直接通信而是经过中间层加密,将加密后的数据包传给TCP响应,然后TCP必须将数据包解密才能传给HTTP。中间层也叫安全层,安全层的核心就是对数据加解密。

对称加密和非对称加密

概念

对称加密是最简单的方式,指加密和解密用的同样的密钥。

非对称加密,如果有A、B两把密钥,A加密过的数据包只能用B解密,B加密过的数据包只能用A解密。

加解密过程

浏览器和服务器进行协商加解密的过程:

  1. 浏览器给服务器发送一个随机数client_random,和加密方法列表。

  2. 服务器接收后给浏览器返回另一个随机数server_random和加密方法。

  3. 现在浏览器和服务器有了相同的client_random、server_random和加密方法三个凭证。

  4. 用这个加密方法将两个随机数生成秘钥,就是浏览器和服务器通信的密钥。

各自应用的效果

用对称加密的方式,第三方可以在中间获取到client_random、server_random和加密方法,加密方法同时可以解密,中间人可以成功对暗号进行解密破解。

非对称加密中服务器有公钥,是公开的,有私钥,是只有服务器自己知道。

传输时浏览器把client_random和加密方法列表传过来,服务器接收到,把server_random、加密方法和公钥传给浏览器。

现在两者拥有相同的client_random、server_random和加密方法。浏览器用公钥将client_random、server_random加密,生成与服务器通信的暗号。

由于是非对称加密,公钥加密过的数据只能用私钥解密,中间人就算拿到了数据也无法解密。

服务器的数据只能用私钥进行加密,中间人一旦拿到公钥,就可以对服务端数据进行解密了。

对称加密和非对称加密结合

  1. 浏览器向服务器发送client_random和加密方法列表。

  2. 服务器接收到后返回server_random、加密方法和公钥。

  3. 浏览器接收,生成另一个随机数pre_random,用公钥加密,传给服务器。

  4. 服务器用私钥解密这个被加密的pre_random。

浏览器和服务器有三个相同的凭证client_random、server_random、pre_random。然后两者用相同的加密方法混合这三个随机数,生成最终的密钥。

浏览器和服务器用一样的密钥进行通信,就是对称加密。

最终的密钥很难被中间人拿到,因为中间人没有私钥,拿不到pre_random,无法生成最终的密钥。

改进是防止了私钥加密的数据外传。单独使用非对称加密,漏洞在于服务器传数据给浏览器只能用私钥加密,这是危险产生的根源。利用对称非对称结合可以防止。

添加数字证书

黑客采用DNS劫持,将目标地址替换成黑客服务器地址,然后黑客自己造公钥私钥,照样能进行数据传输。浏览器用户不知道自己正在访问一个危险的服务器。

HTTPS在上面结合对称和非对称的基础上,又添加了数字证书认证的步骤,目的是给服务器证明自己的身份。

传输过程

为了获取这个证书,服务器运营者需要向第三方认证机构获取授权,第三方认证机构也叫CA ( Certificate Authority ),认证通过后CA会给服务器颁发证书。

数字证书两个作用:服务器向浏览器证明自己的身份、把公钥传给浏览器。

当服务器传送server_random、加密方法的时候,顺便会带上数字证书(包含了公钥),浏览器接收之后就会开始验证数字证书。如果验证通过,后面的过程照常执行,否则拒绝执行。

认证过程

首先读取证书中的明文内容,CA进行数字证书的签名的时候会保存一个hash函数,这个函数计算明文内容得到信息A,公钥解密明文内容得到信息B,两份信息做比对,一致则代表合法。

有时候对于浏览器而言,不知道哪些CA值得信任的,会继续查找CA的上级CA,验证上级CA的合法性。一般根级的CA会内置在操作系统中,如果向上没有找到根级CA就会认为不合法。

总结

HTTPS不是一个新的协议,在HTTP和TCP的传输中建立了一个安全层,利用对称加密和非对称加密结合数字证书认证的方式,大大提高安全性。

10. 实现事件的防抖和节流

节流

在定时器的范围内再次出发则不予理睬,等当前定时器完成才能启动下一个定时器任务。比如十分钟一趟的公交车,十分钟内有没有人在等待不管,到了十分钟准时走。

1
2
3
4
5
6
7
8
9
10
11
12
function throttle(fn, interval) {
let flag = true;
return function(...args) {
let context = this;
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(context, args);
flag = true;
}, interval);
};
};
1
2
3
4
5
6
7
8
9
10
11
const throttle = function(fn, interval) {
let last = 0;
return function (...args) {
let context = this;
let now = +new Date();
// 还没到时间
if(now - last < interval) return;
last = now;
fn.apply(this, args)
}
}

防抖

每次事件触发删除原来的定时器,建立新的定时器,只认最后一次,从最后一次开始计时。

1
2
3
4
5
6
7
8
9
10
function debounce(fn, delay) {
let timer = null;
return function (...args) {
let context = this;
if(timer) clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
}

加强版节流

把防抖和节流放到一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function throttle(fn, delay) {
let last = 0, timer = null;
return function (...args) {
let context = this;
let now = new Date();
if(now - last < delay){
clearTimeout(timer);
setTimeout(function() {
last = now;
fn.apply(context, args);
}, delay);
} else {
// 这个时候表示时间到了,必须给响应
last = now;
fn.apply(context, args);
}
}
}

11. 实现图片的懒加载

方案一:clientHeight、scrollTop和offsetTop

首先给图片一个占位资源:

1
<img src="default.jpg" data-src="http://www.xxx.com/target.jpg" />

通过监听scroll事件判断图片是否到达视口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let img = document.getElementsByTagName("img");
let num = img.length;
let count = 0;//计数器,从第⼀张图⽚开始计
lazyload();//⾸次加载别忘了显示图⽚
window.addEventListener('scroll', lazyload);
function lazyload() {
let viewHeight = document.documentElement.clientHeight;//视⼝⾼度
let scrollTop = document.documentElement.scrollTop ||
document.body.scrollTop;//滚动条卷去的⾼度
for(let i = count; i <num; i++) {
// 元素现在已经出现在视⼝中
if(img[i].offsetTop < scrollHeight + viewHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}

最好对scroll做节流处理,以免频繁触发:

1
2
// throttle函数我们上节已经实现
window.addEventListener('scroll', throttle(lazyload, 200));

方案二:getBoundingClientRect

DOM元素的getBoundingClientRect API判断是否出现在视口。

上述的lazyload函数改成下面:

1
2
3
4
5
6
7
8
9
10
function lazyload() {
for(let i = count; i <num; i++) {
// 元素现在已经出现在视⼝中
if(img[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}

方案三: IntersectionObserver

浏览器内置的API,实现了监听window的scroll事件、判断是否在视口中以及节流三大功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let img = document.getElementsByTagName("img");
const observer = new IntersectionObserver(changes => {
//changes 是被观察的元素集合
for(let i = 0, len = changes.length; i < len; i++) {
let change = changes[i];
// 通过这个属性判断是否在视⼝中
if(change.isIntersecting) {
const imgElement = change.target;
imgElement.src = imgElement.getAttribute("data-src");
observer.unobserve(imgElement);
}
}
})
Array.from(img).forEach(item => observer.observe(item));

IntersectionObserver也可以用作其他资源的预加载。