浏览器
页面渲染流程
- 字节流解码。浏览器获得字节数据,根据字节编码将字节流解码,转换为代码。
- 输入流预处理。字符数据进行统一格式化。
- 令牌化。从输入流中提取可识别的子串和标记符号。可以理解为对HTML解析,进行词法分析,匹配标签生成令牌结构。
- 构建DOM树、构建CSSOM树。DOM树和CSSOM树的构建过程是同时进行的,在 HTML 解析过程中如果遇到 script 标签,解析会暂停并将执行权限交给 JavaScript 引擎,等到 JavaScript 脚本执行完毕后再交给渲染引擎继续解析。(补充:如果脚本中调用了改变 DOM 结构的 document.write() 函数,此时渲染引擎会回到第二步,将这些代码加入字符流,重新进行解析。)
- 构建渲染树。DOM树负责结构内容,CSSOM树负责样式规则,为了渲染,需要将它们合成渲染树。
- 布局。布局阶段根据渲染树的节点和节点的CSS定义以及节点从属关系,计算元素的大小和位置,将所有相对值转换为屏幕上的绝对像素。
- 绘制。绘制就是将渲染树中的每个节点转换成屏幕上的实际像素的过程。在绘制阶段,浏览器会遍历渲染树,调用渲染器的paint方法在屏幕上显示其内容。实际上,绘制过程是在多个层上完成的,这些层称为渲染层(RenderLayer)。
- 渲染层合成。多个绘制后的渲染层按照恰当的重叠顺序进行合并,而后生成位图,最终通过显卡展示到屏幕上。
数据变化过程:字节 → 字符 → 令牌 → 树 → 页面
回流、重绘
回流(Reflow)
在布局完成后,对DOM布局进行修改(比如大小或位置),会引起页面重新计算布局,这个过程称为“回流”。
重绘(Repaint)
对DOM进行不影响布局的修改引起的屏幕局部绘制(比如背景颜色、字体颜色),这个过程称为“重绘”。
小结
回流一定会引起重绘,而重绘不一定会引起回流。由于回流需要重新计算节点布局,回流的渲染耗时会高于重绘。
对于回流重绘,浏览器本身也有优化策略,浏览器会维护一个队列,将回流重绘操作放入队列中,等队列到达一定时间,再按顺序去一次性执行队列的操作。
但是也有例外,有时我们需要获取某些样式信息,例如:
offsetTop
,offsetLeft
,offsetWidth
,offsetHeight
,scrollTop/Left/Width/Height
,clientTop/Left/Width/Height
,getComputedStyle()
,或者 IE 的 currentStyle
。
这时,浏览器为了反馈准确的信息,需要立即回流重绘一次,所以可能导致队列提前执行。
事件循环(Event Loop)
在浏览器的实现上,诸如渲染任务、JavaScript 脚本执行、User Interaction(用户交互)、网络处理都跑在同一个线程上,当执行其中一个类型的任务的时候意味着其他任务的阻塞,为了有序的对各个任务按照优先级进行执行浏览器实现了我们称为 Event Loop 调度流程。
简单来说,Event Loop 就是执行代码、收集和处理事件以及执行队列中子任务的一个过程。
宏任务
在一次新的事件循环的过程中,遇到宏任务时,宏任务将被加入任务队列,但需要等到下一次事件循环才会执行。
常见宏任务:setTimeout
、setInterval
、requestAnimationFrame
微任务
当前事件循环的任务队列为空时,微任务队列中的任务就会被依次执行。在执行过程中,如果遇到微任务,微任务被加入到当前事件循环的微任务队列中。简单来说,只要有微任务就会继续执行,而不是放到下一个事件循环才执行。
微任务队列属于任务运行环境内的一员,并非处于全局的位置。也就是说,每个任务都会有一个微任务队列。
常见微任务:Promise.then
、Promise.catch
、MutationObserver
流程
- 取出一个宏任务执行,如果碰到宏任务,将其放入任务队列,如果碰到微任务,将其放入微任务队列
- 检查微任务队列是否有可执行的微任务,如果有则执行微任务。微任务执行过程中,如果碰到宏任务,将其放入任务队列。如果碰到微任务,继续将其放入当前的微任务队列,直到微任务全部执行。
- 更新渲染阶段,判断是否需要渲染,也就是说不一定每一轮
Event Loop
都会对应一次浏览器渲染。 - 对于需要渲染的文档,执行
requestAnimationFrame
帧动画回调。 - 对于需要渲染的文档,重新渲染绘制用户界面。
- 判断任务队列和微任务队列是否为空,如果是,则进行
Idle
空闲周期的算法,判断是否要执行requestIdleCallback
的回调函数。
小结
在当前任务运行环境内,微任务总是先于宏任务执行;
requestAnimationFrame
回调在页面渲染之前调用,适合做动画;
requestIdleCallback
在渲染屏幕之后调用,可以使用它来执行一些不太重要的任务。
同源策略(Same origin policy)
源是由 URL 中协议、主机名(域名)以及端口共同组成的部分。
同源策略是浏览器的行为,为了保护本地数据不被JavaScript代码获取回来的数据污染,它是存在于浏览器最核心也最基本的安全功能。
所谓同源指的是:协议、域名、端口号必须一致,只要有一个不相同,那么就是“跨源”。
最常见的同源策略是因为域名不同,也就是常说的“跨域”。一般分为请求跨域和页面跨域。
请求跨域解决方案
- 跨域资源共享(CORS)。服务端设置HTTP响应头(Access-Control-Allow-Origin)
- 代理转发。同源策略只存在于浏览器,使用服务端设置代理转发没有同源策略的限制。
- JSONP。依赖的是 script 标签跨域引用 js 文件不会受到浏览器同源策略的限制。
- Websocket。HTML5 规范提出的一个应用层的全双工协议,适用于浏览器与服务器进行实时通信场景。
常用方法是CORS和代理转发。
页面跨域解决方案
- postMessage。HTML5 的 postMessage 方法可用于两个页面之间通信,而且不论这两个页面是否同源。
- document.domain。对于主域名相同,子域名不同的情况,可以通过修改 document.domain 的值来进行跨域。
- window.location.hash,通过 url 带 hash ,通过一个非跨域的中间页面来传递数据。
- window. name,当 window 的 location 变化,然后重新加载,它的 name 属性可以依然保持不变。通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window. name 从外域传递到本地域。
CORS请求
对于CORS请求,浏览器将其分成两个类型:简单请求和非简单请求。
简单请求
简单请求符合下面 2 个特征:
-
请求方法为 GET、POST、HEAD。
-
请求头只能使用以下规定的安全字段:
- Accept(浏览器能够接受的响应内容类型)
- Accept-Language(浏览器能够接受的自然语言列表)
- Content-Type (请求对应的类型,只限于 text/plain、multipart/form-data、application/x-www-form-urlencode)
- Content-Language(浏览器希望采用的自然语言)
- Save-Data
- DPR
- DownLink
- Viewport-Width
- Width
非简单请求
任意一条要求不符合的即为非简单请求。常见是自定义 header
,例如将token
设置到请求头。
在处理非简单请求时,浏览器会先发出“预检请求”,预检请求为OPTIONS方法,以获知服务器是否允许该实际请求,避免跨域请求对服务器产生预期外的影响。如果预检请求返回200允许通过,才会发真实的请求。
预检请求并非每次都需要发送,可以使用 Access-Control-Max-Age 设置缓存时间进行优化,减少请求发送。
HTTP
HTTP 1.0、HTTP 1.1、HTTP 2.0的区别
HTTP1.0
增加头部设定,头部内容以键值对的形式设置。请求头部通过 Accept 字段来告诉服务端可以接收的文件类型,响应头部再通过 Content-Type 字段来告诉浏览器返回文件的类型。
HTTP1.1
HTTP1.0中每次通信都需要经历建立连接、传输数据和断开连接三个阶段,这会增加大量网络开销。
HTTP1.1增加持久化连接,即连接传输完毕后,TCP连接不会马上关闭,而是其他请求可以复用连接。这个连接保持到浏览器或者服务器要求断开连接为止。
HTTP2.0
HTTP1.1虽然减少连接带来的性能消耗,但是请求最大并发受到限制,同一域下的HTTP连接数根据浏览器不同有所变化,一般是6 ~ 8个。而且一个TCP连接同一时刻只能处理一个请求,当前请求未结束之前,其他请求只能处于阻塞状态。
HTTP2.0中增加“多路复用”的机制,不再受限于浏览器的连接数限制。基于二进制分帧,客户端发送的数据会被分割成带有编号的碎片(二进制帧),然后将这些碎片同时发送给服务端,服务端接收到数据后根据编号再合并成完整的数据。服务端返回数据也同样遵循这个过程。
三次握手
过程
第一次握手:客户端向服务端发起连接请求报文,报文中带有一个连接标识(SYN);
第二次握手:服务端接收到客户端的报文,发现报文中有连接标识,服务端知道是一个连接请求,于是给客户端回复确认报文(带有SYN标识);
第三次握手:客户端收到服务端回复确认报文,得知服务端允许连接,于是客户端回复确认报文给服务端,服务端收到客户端的回复报文后,正式建立TCP连接;
为什么需要三次握手,两次可以吗?
如果是两次握手,在第二次握手出现确认报文丢失,客户端不知道服务端是否准备好了,这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。
如果是三次握手,在第三次握手出现确认报文丢失,服务端在一段时间没有收到客户端的回复报文就会重新第二次握手,客户端收到重复的报文会再次给服务端发送确认报文。
三次握手主要考虑是丢包重连的问题。
四次挥手
过程
第一次挥手:客户端向服务端发出连接释放报文,报文中带有一个连接释放标识(FIN)。此时客户端不能再发送数据,但是可以正常接收数据;
第二次挥手:服务端接收到客户端的报文,知道是一个连接释放请求。服务端给客户端回复确认报文,但要注意这个回复报文未带有FIN标识。此时服务端处于关闭等待状态,这个状态还要持续一段时间,因为服务端可能还有数据没发完;
第三次挥手:服务端将最后的数据发送完毕后,给客户端回复确认报文(带有FIN标识),这个才是通知客户端可以释放连接的报文;
第四次挥手:客户端收到服务端回复确认报文后,于是客户端回复确认报文给服务端。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。
为什么握手需要三次,而挥手需要四次
服务端需要确保数据完整性,只能先回复客户端确认报文告诉客户端我收到了报文,进入关闭等待状态。服务端在数据发送完毕后,才回复FIN报文告知客户端数据发送完了,可以断开了,由此多了一次挥手过程。
HTTPS
HTTPS之所以比HTTP安全,是因为对传输内容加密。HTTPS加密使用对称加密和非对称加密。
对称加密:双方共用一把钥匙,可以对内容双向加解密。但是只要有人和服务器通信就能获得密钥,也可以解密其他通信数据。所以相比非对称加密,安全性较低,但是它的效率比非对称加密高。
非对称加密:非对称加密会生成公钥和私钥,一般是服务端持有私钥,公钥向外公开。非对称加密对内容单向加解密,即公钥加密只能私钥解,私钥加密只能公钥解。非对称加密安全性虽然高,但是它的加解密效率很低。
CA证书:由权威机构颁发,用于验证服务端的合法性,其内容包括颁发机构信息、公钥、公司信息、域名等。
对称加密不安全主要是因为密钥容易泄露,那只要保证密钥的安全,就可以得到两全其美的方案,加解密效率高且安全性好。所以HTTPS在传输过程中,对内容使用对称加密,而密钥使用非对称加密。
过程
- 客户端向服务端发起HTTPS请求
- 服务端返回HTTPS证书
- 客户端验证证书是否合法,不合法会提示告警
- 证书验证合法后,在本地生成随机数
- 用公钥加密随机数并发送到服务端
- 服务端使用私钥对随机数解密
- 服务端使用随机数构造对称加密算法,对内容加密后传输
- 客户端收到加密内容,使用本地存储的随机数构建对称加密算法进行解密
HTTP 缓存
HTTP 缓存包括强缓存和协商缓存,强缓存的优先级高于协商缓存。缓存优点在于使用浏览器缓存,对于某些资源服务端不必重复发送,减小服务端的压力,使用缓存的速度也会更快,从而提高用户体验。
强缓存
强缓存在浏览器加载资源时,先从缓存中查找结果,如果不存在则向服务端发起请求。
Expirss
HTTP/1.0 中可以使用响应头部字段 Expires 来设置缓存时间。
客户端第一次请求时,服务端会在响应头部添加 Expirss 字段,浏览器在下一次发送请求时,会对比时间和Expirss的时间,没有过期使用缓存,过期则发送请求。
Cache-Control
HTTP/1.1 提出了 Cache-Control 响应头部字段。
一般会设置 max-age
的值,表示该资源需要缓存多长时间。Cache-Control 的 max-age
优先级高于 Expires。
协商缓存
协商缓存的更新策略是不再指定缓存的有效时间,而是浏览器直接发送请求到服务端进行确认缓存是否更新,如果请求响应返回的 HTTP 状态为 304,则表示缓存仍然有效。
Last-Modified 和 If-Modified-Since
Last-Modified 和 If-Modified-Since 对比资源最后修改时间来实现缓存。
- 浏览器第一次请求资源,服务端在返回资源的响应头上添加 Last-Modified 字段,值是资源在服务端的最后修改时间;
- 浏览器再次请求资源,在请求头上添加 If-Modified-Since,值是上次服务端返回的最后修改时间;
- 服务端收到请求,根据 If-Modified-Since 的值进行判断。若资源未修改过,则返回 304 状态码,并且不返回内容,浏览器使用缓存;否则返回资源内容,并更新 Last-Modified 的值;
ETag 和 If-None-Match
ETag 和 If-None-Match 对比资源哈希值,哈希值由资源内容计算得出,即依赖资源内容实现缓存。
- 浏览器第一次请求资源,服务端在返回资源的响应头上添加 ETag 字段,值是资源的哈希值
- 浏览器再次请求资源,在请求头上添加 If-None-Match,值是上次服务端返回的资源哈希值;
- 服务端收到请求,根据 If-None-Match 的值进行判断。若资源内容没有变化,则返回 304 状态码,并且不返回内容,浏览器使用缓存;否则返回资源内容,并计算哈希值放到 ETag;
TCP 和 UDP 的区别
TCP
- 面向连接
- 一对一通信
- 面向字节流
- 可靠传输,使用流量控制和拥塞控制
- 报头最小20字节,最大60字节
UDP
- 无连接
- 支持一对一,一对多,多对一和多对多的通信
- 面向报文
- 不可靠传输,不使用流量控制和拥塞控制
- 报头开销小,仅8字节
正向代理
- 代理客户;
- 隐藏真实的客户,为客户端收发请求,使真实客户端对服务器不可见;
- 一个局域网内的所有用户可能被一台服务器做了正向代理,由该台服务器负责 HTTP 请求;
- 意味着同服务器做通信的是正向代理服务器;
反向代理
- 代理服务器;
- 隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户 端不可见;
- 负载均衡服务器,将用户的请求分发到空闲的服务器上;
- 意味着用户和负载均衡服务器直接通信,即用户解析服务器域名时得到的是负载均衡服务器的 IP ;
前端安全
跨站脚本攻击(XSS)
跨站脚本(Cross Site Scripting,XSS)指攻击者在页面插入恶意代码,当其他用户访问时,浏览会器解析并执行这些代码,达到窃取用户身份、钓鱼、传播恶意代码等行为。一般我们把 XSS 分为反射型、存储型、DOM 型 3 种类型。
反射型 XSS
反射型 XSS 也叫“非持久型 XSS”,是指攻击者将恶意代码通过请求提交给服务端,服务端返回的内容,也带上了这段 XSS 代码,最后导致浏览器执行了这段恶意代码。
反射型 XSS 攻击方式需要诱导用户点击链接,攻击者会伪装该链接(例如短链接),当用户点击攻击者的链接后,攻击者便可以获取用户的 cookie
身份信息。
案例:
服务端直接输出参数内容:
<? php
$input = $_GET["param"];
echo "<div>".$input."</div>";
恶意代码链接:
http://www.a.com/test.php?param=<srcipt src="xss.js"></script>
存储型 XSS
存储型 XSS 也叫“持久型XSS”,会把用户输入的数据存储在服务端,这种XSS具有很强的稳定性。
案例:
比如攻击者在一篇博客下留言,留言包含恶意代码,提交到服务端后被存储到数据库。所有访问该博客的用户,在加载出这条留言时,会在他们的浏览器中执行这段恶意的代码。
DOM 型 XSS
DOM 型 XSS 是一种特殊的反射型 XSS,它也是非持久型 XSS。相比于反射型 XSS,它不需要经过服务端,而是改变页面 DOM 来达到攻击。同样,这种攻击方式也需要诱导用户点击。
案例:
目标页面:
<html>
<body>hello</body>
</html>
<script>
let search = new URLSearchParams(location.search)
document.write("hello, " + search.get('name') + '!')
</script>
恶意代码链接:
http://www.a.com/test.index?name=<srcipt src="xss.js"></script>
防御手段
- 参数验证,不符合要求的数据不要存入数据库
- 对特殊字符转义,如"<"、">"、"/"、"&"等
- 避免使用
eval
、new Function
动态执行字符串的方法 - 避免使用
innerHTML
、document.write
直接将字符串输出到HTML - 把一些敏感的
cookie
设置为http only
,避免前端访问cookie
跨站请求伪造(CSRF)
CSRF 攻击就是在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作。CSRF 并不需要直接获取用户信息,只需要“借用”用户的登录信息相关操作即可,隐蔽性更强。
案例:
假设现在有一个博客网站,得知删除博文的 URL 为:
http://blog.com?m=delete&id=123
攻击者构造一个页面,内容为:
<img src="http://blog.com?m=delete&id=123"></img>
攻击者伪装该网站链接并诱导用户进行点击,用户恰好访问过 blog.com
,与该网站的 cookie
身份验证信息还未过期。这时进入攻击者的网站,img 发起请求,请求里携带上cookie
,成功删除博文。但是对于用户是无感知的,当用户返回到博客时会发现博文不见了,而这个请求是属于合法请求,因为攻击者借用受害者的身份信息进行操作。
防御手段
- 设置 Cookie 的 SameSite
- 服务端验证 Refer 字段,Refer 是请求源网址,对于不合法的 Refer 拒绝请求
- 添加
token
,让链接变得不可预测,攻击者无法构造一个完整的 URL 实施 CSRF 攻击 - 添加验证码,强制用户必须与应用交互,但会降低用户体验,只能作为辅助手段
点击劫持(ClickJacking)
攻击者创建一个网页利用 iframe 包含目标网站,然后通过设置透明度等方式隐藏目标网站,使用户无法察觉目标网站的存在,并且把它遮罩在网页上。在网页中诱导用户点击特定的按钮,而这个按钮的位置和目标网站的某个按钮重合,当用户点击网页上的按钮时,实际上是点击目标网站的按钮。
防御手段
- frame busting,通常可以写一段JavaScript,以禁止 iframe 的嵌套。
if (top.location != location) {
top.location = self.location
}
- 添加 HTTP 头 X-Frame-Options
参考资料
- 《白帽子讲Web安全》
- HTML规范 - 解析HTML文档
- 浏览器层合成与页面渲染优化
- 10种跨域解决方案(附终极大招)
- MDN - HTTP访问控制(CORS)
- HTML规范 - 事件循环
- MDN - 深入:微任务与Javascript运行时环境
- 深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)
- Tasks, microtasks, queues and schedules
- 卧槽!牛皮了,头一次见有大佬把TCP三次握手四次挥手解释的这么明白
- 你连 HTTPS 原理都不懂,还讲“中间人攻击”?
- 进阶 · 那些你必须搞懂的网络基础