「查缺补漏」送你18道浏览器面试题

前言

想要成为一名合格的前端工程师,掌握相关浏览器的工做原理是必备的,这样子才会有一个完整知识体系,要是「能参透浏览器的工做原理,你就能解决80%的前端难题」。php

这篇梳理的话,更多的是对浏览器工做原理篇的查缺补漏,对于一些没有涉及到的知识点,准备梳理梳理,也正好回顾以前梳理的内容。css

感谢掘友的鼓励与支持🌹🌹🌹,往期文章都在最后梳理出来了(●'◡'●)html


接下来以问题形式展开梳理前端

1. 常见的浏览器内核有哪些?

浏览器/RunTime 内核(渲染引擎) JavaScript 引擎
Chrome webkit->blink V8
FireFox Gecko SpiderMonkey
Safari Webkit JavaScriptCore
Edge EdgeHTML Chakra(for JavaScript)
IE Trident JScript(IE3.0-IE8.0)
Opera Presto->blink Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-)
Node.js - V8

2. 浏览器的主要组成部分是什么?

  1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。
  2. 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
  3. 呈现引擎 - 负责显示请求的内容。若是请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  4. 网络 - 用于网络调用,好比 HTTP 请求。
  5. 用户界面后端 -用于绘制基本的窗口小部件,好比组合框和窗口。
  6. JavaScript 解释器- 用于解析和执行 JavaScript 代码。
  7. 数据存储 - 这是持久层。浏览器须要在硬盘上保存各类数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(可是轻便)的浏览器内数据库。

值得注意的是,和大多数浏览器不一样,Chrome 浏览器的每一个标签页都分别对应一个呈现引擎实例。每一个标签页都是一个独立的进程。jquery


3. 为何JavaScript是单线程的,与异步冲突吗

补充:JS中实际上是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS的设计初衷就没有考虑这些,针对JS这种不具有并行任务处理的特性,咱们称之为“单线程”。nginx

JS的单线程是指一个浏览器进程中只有一个JS的执行线程,同一时刻内只会有一段代码在执行。程序员

举个通俗例子,假设JS支持多线程操做的话,JS能够操做DOM,那么一个线程在删除DOM,另一个线程就在获取DOM数据,这样子明显不合理,这算是证实之一。web

来看段代码👇面试

function foo() {
 console.log("first");  setTimeout(( function(){  console.log( 'second' );  }),5); }  for (var i = 0; i < 1000000; i++) {  foo(); }  复制代码

打印结果就是首先是不少个first,而后再是second。ajax

异步机制是浏览器的两个或以上常驻线程共同完成的,举个例子,好比异步请求由两个常驻线程,JS执行线程和事件触发线程共同完成的。

  • JS执行线程发起异步请求(浏览器会开启一个HTTP请求线程来执行请求,这时JS的任务完成,继续执行线程队列中剩下任务)
  • 而后在将来的某一时刻事件触发线程监视到以前的发起的HTTP请求已完成,它就会把完成事件插入到JS执行队列的尾部等待JS处理

再好比定时器触发(settimeout和setinterval) 是由浏览器的定时器线程执行的定时计数,而后在定时时间把定时处理函数的执行请求插入到JS执行队列的尾端(因此用这两个函数的时候,实际的执行时间是大于或等于指定时间的,不保证能准肯定时的)。

因此这么说,JS单线程与异步更可能是浏览器行为,之间不冲突。


4. CSS加载会形成阻塞吗

先给出结论

  • CSS不会阻塞DOM解析,但会阻塞DOM渲染。
  • CSS会阻塞JS执行,并不会阻塞JS文件下载

先讲一讲CSSOM做用

  • 第一个是提供给 JavaScript 操做样式表的能力
  • 第二个是为布局树的合成提供基础的样式信息
  • 这个 CSSOM 体如今 DOM 中就是document.styleSheets。

由以前讲过的浏览器渲染流程咱们能够看出:

DOM 和 CSSOM一般是并行构建的,因此CSS 加载不会阻塞 DOM 的解析

然而因为Render Tree 是依赖DOM Tree和 CSSOM Tree的,因此它必须等到二者都加载完毕后,完成相应的构建,才开始渲染,所以,CSS加载会阻塞DOM渲染

因为 JavaScript 是可操纵 DOM 和 css 样式 的,若是在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程先后得到的元素数据就可能不一致了。

所以为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。

有个须要注意的点就是:

有时候JS须要等到CSS的下载,这是为何呢?

仔细思考一下,其实这样作是有道理的,若是脚本的内容是获取元素的样式,宽高等CSS控制的属性,浏览器是须要计算的,也就是依赖于CSS。浏览器也没法感知脚本内容究竟是什么,为避免样式获取,于是只好等前面全部的样式下载完后,再执行JS

JS文件下载和CSS文件下载是并行的,有时候CSS文件很大,因此JS须要等待。

所以,样式表会在后面的 js 执行前先加载执行完毕,因此css 会阻塞后面 js 的执行


5. 为何JS会阻塞页面加载

先给出结论👇

  • JS阻塞DOM解析,也就会阻塞页面

这也是为何说JS文件放在最下面的缘由,那为何会阻塞DOM解析呢

你能够这样子理解:

因为 JavaScript 是可操纵 DOM 的,若是在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程先后得到的元素数据就可能不一致了。

所以为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。

当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时当即被执行。

当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。

所以若是 JS 执行的时间过长,这样就会形成页面的渲染不连贯,致使页面渲染加载阻塞的感受。

另外,若是 JavaScript 文件中没有操做 DOM 相关代码,就能够将该 JavaScript 脚本设置为异步加载,经过 async 或 defer 来标记代码


6. defer 和 async 的区别 ?

  • 二者都是异步去加载外部JS文件,不会阻塞DOM解析
  • Async是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行,标记为async的脚本并不保证按照指定他们的前后顺序执行,该属性对于内联脚本无做用 (即没有src属性的脚本)。
  • defer是在JS加载完成后,整个文档解析完成后,触发 DOMContentLoaded 事件前执行,若是缺乏 src 属性(即内嵌脚本),该属性不该被使用,由于这种状况下它不起做用

7. DOMContentLoaded 与 load 的区别 ?

  • DOMContentLoaded事件触发时:仅当DOM解析完成后,不包括样式表,图片等资源。
  • onload 事件触发时,页面上全部的 DOM,样式表,脚本,图片等资源已经加载完毕。

那么也就是先DOMContentLoaded -> load,那么在Jquery中,使用(document).load(callback)监听的就是load事件。

那咱们能够聊一聊它们与async和defer区别

带async的脚本必定会在load事件以前执行,可能会在DOMContentLoaded以前或以后执行。

  • 状况1: HTML 尚未被解析完的时候,async脚本已经加载完了,那么 HTML 中止解析,去执行脚本,脚本执行完毕后触发DOMContentLoaded事件
  • 状况2: HTML 解析完了以后,async脚本才加载完,而后再执行脚本,那么在HTML解析完毕、async脚本还没加载完的时候就触发DOMContentLoaded事件

若是 script 标签中包含 defer,那么这一块脚本将不会影响 HTML 文档的解析,而是等到HTML 解析完成后才会执行。而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发。

  • 状况1:HTML还没解析完成时,defer脚本已经加载完毕,那么defer脚本将等待HTML解析完成后再执行。defer脚本执行完毕后触发DOMContentLoaded事件
  • 状况2:HTML解析完成时,defer脚本还没加载完毕,那么defer脚本继续加载,加载完成后直接执行,执行完毕后触发DOMContentLoaded事件

8. 为何CSS动画比JavaScript高效

我以为这个题目说法上可能就是行不通,不能这么说,若是了解的话,都知道will-change只是一个优化的手段,使用JS改变transform也能够享受这个属性带来的变化,因此这个说法上有点不妥。

因此围绕这个问题展开话,更应该说建议推荐使用CSS动画,至于为何呢,涉及的知识点大概就是重排重绘,合成,这方面的点,我在浏览器渲染流程中也说起了。

尽量的避免重排和重绘,具体是哪些操做呢,若是非要去操做JS实现动画的话,有哪些优化的手段呢?

好比👇

  • 使用createDocumentFragment进行批量的 DOM 操做
  • 对于 resize、scroll 等进行防抖/节流处理。
  • rAF优化等等

剩下的东西就留给大家思考吧,但愿我这是抛砖引玉吧(●'◡'●)


9. 能不能实现事件防抖和节流

函数节流(throttle)

节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。

规定在一个单位时间内,只能触发一次函数。若是这个单位时间内触发屡次函数,只有一次生效。

抓取一个关键的点:就是执行的时机。要作到控制执行的时机,咱们能够经过一个开关,与定时器setTimeout结合完成。

function throttle(fn, delay) {
 let flag = true,  timer = null;  return function (...args) {  let context = this;  if (!flag) return;  flag = false;  clearTimeout(timer)  timer = setTimeout(() => {  fn.apply(context, args);  flag = true;  }, delay);  };  }; 复制代码

函数防抖(debounce)

在事件被触发n秒后再执行回调,若是在这n秒内又被触发,则从新计时。

核心思想:每次事件触发都会删除原有定时器,创建新的定时器。通俗意思就是反复触发函数,只认最后一次,从最后一次开始计时。

代码:

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)  }  } 复制代码

如何使用 debounce 和 throttle 以及常见的坑

本身造一个 debounce / throttle 的轮子看起来多么诱人,或者随便找个博文复制过来。我是建议直接使用 underscore 或 Lodash 。若是仅须要 _.debounce_.throttle 方法,可使用 Lodash 的自定义构建工具,生成一个 2KB 的压缩库。使用如下的简单命令便可:

npm i -g lodash-cli
npm i -g lodash-clilodash-cli include=debounce,throttle 复制代码

常见的坑是,不止一次地调用 _.debounce 方法:

// 错误
$(window).on('scroll', function() {  _.debounce(doSomething, 300); }); // 正确 $(window).on('scroll', _.debounce(doSomething, 200));  复制代码

debounce 方法保存到一个变量之后,就能够用它的私有方法 debounced_version.cancel(),lodash 和 underscore.js 都有效。

let debounced_version = _.debounce(doSomething, 200);
 $(window).on('scroll', debounced_version);   // 若是须要的话debounced_version.cancel(); 复制代码

适合应用场景

防抖

  • search搜索,用户不断输入值时,用防抖来节约Ajax请求,也就是输入框事件。
  • window触发resize时,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

节流

  • 鼠标的点击事件,好比mousedown只触发一次
  • 监听滚动事件,好比是否滑到底部自动加载更多,用throttle判断
  • 好比游戏中发射子弹的频率(1秒发射一颗)

10. 谈一谈你对requestAnimationFrame(rAF)理解

正好跟节流有点关系,有点类似处,就准备梳理一下这个知识点。

高性能动画是什么,那它衡量的标准是什么呢?

动画帧率能够做为衡量标准,通常来讲画面在 60fps 的帧率下效果比较好。

换算一下就是,每一帧要在 16.7ms (16.7 = 1000/60) 内完成渲染。

咱们来看看MDN对它的解释吧👇

window.requestAnimationFrame() 方法告诉浏览器您但愿执行动画并请求浏览器在下一次重绘以前调用指定的函数来更新动画。该方法使用一个回调函数做为参数,这个回调函数会在浏览器重绘以前调用。-- MDN

当咱们调用这个函数的时候,咱们告诉它须要作两件事:

  1. 咱们须要新的一帧;
  2. 当你渲染新的一帧时须要执行我传给你的回调函数

rAF与 setTimeout 相比

rAF(requestAnimationFrame) 最大的优点是由系统来决定回调函数的执行时机

具体一点讲就是,系统每次绘制以前会主动调用 rAF 中的回调函数,若是系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,若是绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。

换句话说就是,rAF 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次(上一个知识点刚刚梳理完函数节流),这样就不会引发丢帧现象,也不会致使动画出现卡顿的问题。

另外它能够自动调节频率。若是callback工做太多没法在一帧内完成会自动下降为30fps。虽然下降了,但总比掉帧好。

与setTimeout动画对比的话,有如下几点优点

  • 当页面隐藏或者最小化时,setTimeout仍然在后台执行动画,此时页面不可见或者是不可用状态,动画刷新没有意义,而言浪费CPU。
  • rAF不同,当页面处理未激活的状态时,该页面的屏幕绘制任务也会被系统暂停,所以跟着系统步伐走的rAF也会中止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。

何时调用呢

规范中彷佛是这么去定义的:

  • 在从新渲染前调用。
  • 极可能在宏任务以后不去调用

这样子分析的话,彷佛很合理嘛,为何要在从新渲染前去调用呢?由于rAF做为官方推荐的一种作流畅动画所应该使用的API,作动画不可避免的去操做DOM,而若是是在渲染后去修改DOM的话,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这样子彷佛不合理。

rAF在浏览器决定渲染以前给你最后一个机会去改变 DOM 属性,而后很快在接下来的绘制中帮你呈现出来,因此这是作流畅动画的不二选择。

至于宏任务,微任务,这能够提及来就要展开篇幅了,暂时不在这里梳理了。

rAF与节流相比

_.throttle(dosomething, 16) 等价。它是高保真的,若是追求更好的精确度的话,能够用浏览器原生的 API 。

可使用 rAF API 替换 throttle 方法,考虑一下优缺点:

优势

  • 动画保持 60fps(每一帧 16 ms),浏览器内部决定渲染的最佳时机
  • 简洁标准的 API,后期维护成本低

缺点

  • 动画的开始/取消须要开发者本身控制,不像 ‘.debounce’ 或 ‘.throttle’由函数内部处理。
  • 浏览器标签未激活时,一切都不会执行。
  • 尽管全部的现代浏览器都支持 rAF ,IE9,Opera Mini 和 老的 Android 仍是须要打补丁
  • Node.js 不支持,没法在服务器端用于文件系统事件。

根据经验,若是 JavaScript 方法须要绘制或者直接改变属性,我会选择 requestAnimationFrame,只要涉及到从新计算元素位置,就可使用它。

涉及到 AJAX 请求,添加/移除 class (能够触发 CSS 动画),我会选择 _.debounce 或者 _.throttle ,能够设置更低的执行频率(例子中的200ms 换成16ms)。


11. 能不能实现图片的懒加载

页可见区域宽: document.body.clientWidth;
网页可见区域高: document.body.clientHeight; 网页可见区域宽: document.body.offsetWidth (包括边线的宽); 网页可见区域高: document.body.offsetHeight (包括边线的宽); 网页正文全文宽: document.body.scrollWidth; 网页正文全文高: document.body.scrollHeight; 网页被卷去的高: document.body.scrollTop; 网页被卷去的左: document.body.scrollLeft; 网页正文部分上: window.screenTop; 网页正文部分左: window.screenLeft; 屏幕分辨率的高: window.screen.height; 屏幕分辨率的宽: window.screen.width; 屏幕可用工做区高度: window.screen.availHeight; 复制代码

关于scrollTop,offsetTop,scrollLeft,offsetLeft用法介绍,点这里

原理思路

  1. 拿到因此的图片img dom
  2. 重点是第二步,判断当前图片是否到了可视区范围内
  3. 到了可视区的高度之后,就将img的data-src属性设置给src
  4. 绑定window的scroll事件

固然了,为了用户的体验更加,默认的状况下,设置一个占位图

本次测试代码

CSS代码👇

<style>
 img{  display: block;  height: 320px;  margin-top: 20px;  margin: 10px auto;  } </style> 复制代码

HTML👇

<img src="default.png" data-src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595328889118&di=1665d7e122bc96be92d0f3e1b2f5e302&imgtype=0&src=http%3A%2F%2Fwork.361ser.com%2FContent%2Fueditor%2Fnet%2Fupload%2Fimage%2F20171014%2F6364359407281350179759303.jpg" />
复制代码

第一种方式

clientHeight-scrollTop-offsetTop

直接上我运行的代码👇

let Img = document.getElementsByTagName("img"),
 len = Img.length,  count = 0;  function lazyLoad () {  let viewH = document.body.clientHeight, //可见区域高度  scrollTop = document.body.scrollTop; //滚动条距离顶部高度  for(let i = count; i < len; i++) {  if(Img[i].offsetTop < scrollTop + viewH ){  if(Img[i].getAttribute('src') === 'default.png'){  Img[i].src = Img[i].getAttribute('data-src')  count++;  }  }  }  }  function throttle(fn, delay) {  let flag = true,  timer = null;  return function (...args) {  let context = this;  if (!flag) return;  flag = false;  clearTimeout(timer)  timer = setTimeout(() => {  fn.apply(context, args);  flag = true;  }, delay);  };  };  window.addEventListener('scroll', throttle(lazyLoad,1000))   lazyLoad(); // 首次加载 复制代码

第二种方式

使用 element.getBoundingClientRect() API 直接获得 top 值。

代码👇

let Img = document.getElementsByTagName("img"),
 len = Img.length,  count = 0;  function lazyLoad () {  let viewH = document.body.clientHeight, //可见区域高度  scrollTop = document.body.scrollTop; //滚动条距离顶部高度  for(let i = count; i < len; i++) {  if(Img[i].getBoundingClientRect().top < scrollTop + viewH ){  if(Img[i].getAttribute('src') === 'default.png'){  Img[i].src = Img[i].getAttribute('data-src')  count++;  }  }  }  }  function throttle(fn, delay) {  let flag = true,  timer = null;  return function (...args) {  let context = this;  if (!flag) return;  flag = false;  clearTimeout(timer)  timer = setTimeout(() => {  fn.apply(context, args);  flag = true;  }, delay);  };  };  window.addEventListener('scroll', throttle(lazyLoad,1000))   lazyLoad(); // 首次加载  复制代码

好像也差很少,不知道是否是我写的方式有问题(●'◡'●),感受差很少

来看看效果吧,我给这个事件加了一个节流,这样子操做看起来就更好了。

图片懒加载
图片懒加载

12. 说一说你对Cookie localStorage sessionStorage

Cookie

得扯一下HTTP是一个无状态的协议,这里主要指的是HTTP1.x版本,简单的能够理解为即便同一个客户端连续两次发送请求给服务器,服务器也没法识别这个同一个客户端发的请求,致使的问题,好比现实生活中你加入一个商品到购物车,可是由于没法识别同一个客户端,你刷新页面的话就🤭

为了解决 HTTP 无状态致使的问题(HTTP1.x),后来出现了 Cookie。

Cookie 的存在也不是为了解决通信协议无状态的问题,只是为了解决客户端与服务端会话状态的问题,这个状态是指后端服务的状态而非通信协议的状态。

Cookie存放在本地的好处就在于即便你关闭了浏览器,Cookie 依然能够生效。

Cookie设置

怎么去设置呢?简单来讲就是👇

  1. 客户端发送 HTTP 请求到服务器
  2. 当服务器收到 HTTP 请求时,在响应头里面添加一个 Set-Cookie 字段
  3. 浏览器收到响应后保存下 Cookie
  4. 以后对该服务器每一次请求中都经过 Cookie 字段将 Cookie 信息发送给服务器。

Cookie指令

在下面这张图里咱们能够看到 Cookies 相关的一些属性👇

Cookie
Cookie

这里主要说一些你们可能没有注意的点:

Name/Value

用 JavaScript 操做 Cookie 的时候注意对 Value 进行编码处理。

Expires/Max-Age

Expires 用于设置 Cookie 的过时时间。好比:

Set-Cookie: id=aad3fWa; Expires=Wed, 21 May 2020 07:28:00 GMT;
复制代码
  • 当 Expires 属性缺省时,表示是会话性 Cookie。
  • 像上图 Expires 的值为 Session,表示的就是会话性 Cookie。
  • 会话性 Cookie 的时候,值保存在客户端内存中,并在用户关闭浏览器时失效。
  • 须要注意的是,有些浏览器提供了会话恢复功能,关闭浏览器,会话期Cookie会保留下来。
  • 与会话性 Cookie 相对的是持久性 Cookie,持久性 Cookies 会保存在用户的硬盘中,直至过时或者清除 Cookie。

Max-Age 用于设置在 Cookie 失效以前须要通过的秒数。好比:

Set-Cookie: id=a3fWa; Max-Age=604800;
复制代码

假如 Expires 和 Max-Age 都存在,Max-Age 优先级更高。

Domain

Domain 指定了 Cookie 能够送达的主机名。假如没有指定,那么默认值为当前文档访问地址中的主机部分(可是不包含子域名)。

在这里注意的是,不能跨域设置 Cookie

Path

Path 指定了一个 URL 路径,这个路径必须出如今要请求的资源的路径中才能够发送 Cookie 首部。好比设置 Path=/docs/docs/Web/ 下的资源会带 Cookie 首部,/test 则不会携带 Cookie 首部。

Domain 和 Path 标识共同定义了 Cookie 的做用域:即 Cookie 应该发送给哪些 URL。

Secure属性

标记为 Secure 的 Cookie 只应经过被HTTPS协议加密过的请求发送给服务端。使用 HTTPS 安全协议,能够保护 Cookie 在浏览器和 Web 服务器间的传输过程当中不被窃取和篡改。

HTTPOnly

设置 HTTPOnly 属性能够防止客户端脚本经过 document.cookie 等方式访问 Cookie,有助于避免 XSS 攻击。

SameSite

SameSite 属性可让 Cookie 在跨站请求时不会被发送,从而能够阻止跨站请求伪造攻击(CSRF)。

后面讲CSRF攻击会将讲到,这里过。

这个属性值修改有什么影响呢?

Samesite
Samesite

从上图能够看出,对大部分 web 应用而言,Post 表单,iframe,AJAX,Image 这四种状况从之前的跨站会发送三方 Cookie,变成了不发送。

Cookie 的做用

Cookie 主要用于如下三个方面:

  1. 会话状态管理(如用户登陆状态、购物车、游戏分数或其它须要记录的信息)
  2. 个性化设置(如用户自定义设置、主题等)
  3. 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie 的缺点

从大小,安全,增长请求大小。

  • 容量缺陷。Cookie 的体积上限只有4KB,只能用来存储少许的信息。
  • 下降性能,Cookie紧跟着域名,无论域名下的某个地址是否须要这个Cookie,请求都会带上完整的Cookie,请求数量增长,会形成巨大的浪费。
  • 安全缺陷,Cookie是以纯文本的形式在浏览器和服务器中传递,很容易被非法用户获取,当HTTPOnly为false时,Cookie信息还能够直接经过JS脚本读取。

localStorage 和 sessionStorage

在 web 本地存储场景上,cookie 的使用受到种种限制,最关键的就是存储容量过小和数据没法持久化存储。

在 HTML 5 的标准下,出现了 localStorage 和 sessionStorage 供咱们使用。

异同点

分类 生命周期 存储容量 存储位置
cookie 默认保存在内存中,随浏览器关闭失效(若是设置过时时间,在到过时时间后失效) 4KB 保存在客户端,每次请求时都会带上
localStorage 理论上永久有效的,除非主动清除。 4.98MB(不一样浏览器状况不一样,safari 2.49M) 保存在客户端,不与服务端交互。节省网络流量
sessionStorage 仅在当前网页会话下有效,关闭页面或浏览器后会被清除。 4.98MB(部分浏览器没有限制) 同上

操做方式

接下来咱们来具体看看如何来操做localStoragesessionStorage

let obj = { name: "TianTianUp", age: 18 };
localStorage.setItem("name", "TianTianUp"); localStorage.setItem("info", JSON.stringify(obj)); 复制代码

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

let name = localStorage.getItem("name");
let info = JSON.parse(localStorage.getItem("info")); 复制代码

从这里能够看出,localStorage其实存储的都是字符串,若是是存储对象须要调用JSONstringify方法,而且用JSON.parse来解析成对象。

应用场景

  • localStorage 适合持久化缓存数据,好比页面的默认偏好配置,如官网的logo,存储Base64格式的图片资源等;
  • sessionStorage 适合一次性临时数据保存,存储本次浏览信息记录,这样子页面关闭的话,就不须要这些记录了,还有对表单信息进行维护,这样子页面刷新的话,也不会让表单信息丢失。

13. 聊一聊浏览器缓存

浏览器缓存是性能优化的一个重要手段,对于理解缓存机制而言也是很重要的,咱们来梳理一下吧👇

强缓存

强缓存两个相关字段,ExpiresCache-Control

强缓存分为两种状况,一种是发送HTTP请求,一种不须要发送。

首先检查强缓存,这个阶段**不须要发送HTTP请求。**经过查找不一样的字段来进行,不一样的HTTP版本因此不一样。

  • HTTP1.0版本,使用的是Expires,HTTP1.1使用的是Cache-Control

Expires

Expires即过时时间,时间是相对于服务器的时间而言的,存在于服务端返回的响应头中,在这个过时时间以前能够直接从缓存里面获取数据,无需再次请求。好比下面这样:

Expires:Mon, 29 Jun 2020 11:10:23 GMT
复制代码

表示该资源在2020年7月29日11:10:23过时,过时时就会从新向服务器发起请求。

这个方式有一个问题:服务器的时间和浏览器的时间可能并不一致,因此HTTP1.1提出新的字段代替它。

Cache-Control

HTTP1.1版本中,使用的就是该字段,这个字段采用的时间是过时时长,对应的是max-age。

Cache-Control:max-age=6000
复制代码

上面表明该资源返回后6000秒,能够直接使用缓存。

固然了,它还有其余不少关键的指令,梳理了几个重要的👇

注意点:

  • 当Expires和Cache-Control同时存在时,优先考虑Cache-Control。
  • 固然了,当缓存资源失效了,也就是没有命中强缓存,接下来就进入协商缓存👇

协商缓存

强缓存失效后,浏览器在请求头中携带响应的缓存Tag来向服务器发送请求,服务器根据对应的tag,来决定是否使用缓存。

缓存分为两种,Last-ModifiedETag。二者各有优点,并不存在谁对谁有绝对的优点,与上面所讲的强缓存两个Tag所不一样。

Last-Modified

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

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

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

  • 若是请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程同样。
  • 不然返回304,告诉浏览器直接使用缓存。

ETag

ETag是服务器根据当前文件的内容,对文件生成惟一的标识,好比MD5算法,只要里面的内容有改动,这个值就会修改,服务器经过把响应头把该字段给浏览器。

浏览器接受到ETag值,会在下次请求的时候,将这个值做为If-None-Match这个字段的内容,发给服务器。

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

  • 若是二者同样的话,直接返回304,告诉浏览器直接使用缓存
  • 若是不同的话,说明内容更新了,返回新的资源,跟常规的HTTP请求响应的流程同样

二者对比

  • 性能上,Last-Modified优于ETagLast-Modified记录的是时间点,而Etag须要根据文件的MD5算法生成对应的hash值。
  • 精度上,ETag优于Last-ModifiedETag按照内容给资源带上标识,能准确感知资源变化,Last-Modified在某些场景并不能准确感知变化,好比👇
    • 编辑了资源文件,可是文件内容并无更改,这样也会形成缓存失效。
    • Last-Modified 可以感知的单位时间是秒,若是文件在 1 秒内改变了屡次,那么这时候的 Last-Modified 并无体现出修改了。

最后,若是两种方式都支持的话,服务器会优先考虑ETag

缓存位置

接下来咱们考虑使用缓存的话,缓存的位置在哪里呢?

浏览器缓存的位置的话,能够分为四种,优先级从高到低排列分别👇

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

这个应用场景好比PWA,它借鉴了Web Worker思路,因为它脱离了浏览器的窗体,所以没法直接访问DOM。它能完成的功能好比:离线缓存消息推送网络代理,其中离线缓存就是Service Worker Cache

Memory Cache

指的是内存缓存,从效率上讲它是最快的,从存活时间来说又是最短的,当渲染进程结束后,内存缓存也就不存在了。

Disk Cache

存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,优点在于存储容量和存储时长。

Disk Cache VS Memory Cache

二者对比,主要的策略👇

内容使用率高的话,文件优先进入磁盘

比较大的JS,CSS文件会直接放入磁盘,反之放入内存。

Push Cache

推送缓存,这算是浏览器中最后一道防线吧,它是HTTP/2的内容。具体我也不是很清楚,有兴趣的能够去了解。

总结

  • 首先检查Cache-Control, 尝鲜,看强缓存是否可用
  • 若是可用的话,直接使用
  • 不然进入协商缓存,发送HTTP请求,服务器经过请求头中的If-Modified-Since或者If-None-Match字段检查资源是否更新
  • 资源更新,返回资源和200状态码。
  • 不然,返回304,直接告诉浏览器直接从缓存中去资源。

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

一旦问这个问题的话,我以为确定是一个很是深的问题了,不管从深度仍是广度上,要真的答好这个题目,或者梳理清楚的话,挺难的,毕竟一个很是综合性的问题,我做为一个刚刚入门的小白,只能梳理部分知识,更深的知识能够去看看参考连接。

那么咱们就开始吧,假设你输入的内容是👇

https://juejin.cn
复制代码

👇👇👇

网络请求

1. 构建请求

首先,浏览器构建请求行信息(以下所示),构建好后,浏览器准备发起网络请求👇

GET / HTTP1.1
GET是请求方法,路径就是根路径,HTTP协议版本1.1 复制代码

2. 查找缓存

在真正发起网络请求以前,浏览器会先在浏览器缓存中查询是否有要请求的文件。

先检查强缓存,若是命中的话直接使用,不然进入下一步,强缓存的知识点,上面👆梳理过了。

3. DNS解析

输入的域名的话,咱们须要根据域名去获取对应的ip地址。 这个过程须要依赖一个服务系统,叫作是DNS域名解析, 从查找到获取到具体IP的过程叫作是DNS解析

关于DNS篇,能够看看阮一峰的网络日志

首先,浏览器提供了DNS数据缓存功能,若是一个域名已经解析过了,那么就会把解析的结果缓存下来,下次查找的话,直接去缓存中找,不须要结果DNS解析。

解析过程总结以下👇

  1. 首先查看是否有对应的域名缓存,有的话直接用缓存的ip访问

    1. ipconfig /displaydns
      // 输入这个命令就能够查看对应的电脑中是否有缓存 复制代码
  2. 若是缓存中没有,则去查找hosts文件 通常在 c:\windows\system32\drivers\etc\hosts

  3. 若是hosts文件里没找到想解析的域名,则将域名发往本身配置的dns服务器,也叫本地dns服务器

    1. ipconfig/all
      经过这个命令能够查看本身的本地dns服务器 复制代码
  4. 若是本地dns服务器有相应域名的记录,则返回记录。

    1. 电脑的dns服务器通常是各大运营商如电信联通提供的,或者像180.76.76.76,223.5.5.5,4个114等知名dns服务商提供的,自己缓存了大量的常见域名的ip,因此常见的网站,都是有记录的。不须要找根服务器。

  5. 若是电脑本身的服务器没有记录,会去找根服务器。根服务器全球只要13组,回去找其中之一,找了根服务器后,根服务器会根据请求的域名,返回对应的“顶级域名服务器”,如:

    1. 若是请求的域名是xxx.com,则返回负责com域的服务器
    2. 若是是xxx.cn,则发给负责cn域的服务器
    3. 若是是xxx.ca,则发给负责ca域的服务器
  6. 顶级域服务器收到请求,会返回二级域服务器的地址

    1. 好比一个网址是www.xxx.edu.cn,则顶级域名服务器再转发给负责.edu.cn域的二级服务器
  7. 以此类推,最终会发到负责锁查询域名的,最精确的那台dns,能够获得查询结果。

  8. 最后一步,本地dns服务器,把最终的解析结果,返回给客户端,对客户端来说,只是一去一回的事,客户端并不知道本地dns服务器通过了千山万水。

以上就是大概的过程了,有兴趣的话,能够仔细去看看。

创建TCP连接

咱们所了解的就是👉Chrome 在同一个域名下要求同时最多只能有 6 个 TCP 链接,超过 6 个的话剩下的请求就得等待。

那么咱们假设不须要等待,咱们进入了TCP链接的创建阶段。

创建TCP链接经历下面三个阶段:

  • 经过三次握手创建客户端和服务器之间的链接。
  • 进行数据传输。
  • 断开链接的阶段。数据传输完成,如今要断开链接了,经过四次挥手来断开链接。

从上面看得出来,TCP 链接经过什么手段来保证数据传输的可靠性,一是三次握手确认链接,二是数据包校验保证数据到达接收方,三是经过四次挥手断开链接。

深刻理解的话,能够看看对应的文章,掘金上面不少文章都有深刻了解,这里就不梳理了。

发送HTTP请求

TCP链接完成后,接下来就能够与服务器通讯了,也就是咱们常常说的发送HTTP请求。

发送HTTP请求的话,须要携带三样东西:请求行请求头请求体

咱们看看大概是是什么样子的吧👇

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: keep-alive Cookie: /* 省略cookie信息 */ Host: juejin.im Pragma: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 复制代码

最后就是请求体,请求体的话只有在POST请求场景下存在,常见的就是表单提交

网络响应

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

跟请求部分相似,网络响应具备三个部分:响应行响应头响应体

响应行相似下面这样👇

HTTP/1.1 200 OK
复制代码

对应的响应头数据是怎么样的呢?咱们来举个例子看看👇

Access-Control-Max-Age: 86400
Cache-control: private Connection: close Content-Encoding: gzip Content-Type: text/html;charset=utf-8 Date: Wed, 22 Jul 2020 13:24:49 GMT Vary: Accept-Encoding Set-Cookie: ab={}; path=/; expires=Thu, 22 Jul 2021 13:24:49 GMT; secure; httponly Transfer-Encoding: chunked 复制代码

接下来,咱们数据拿到了,你认为就会断开TCP链接吗?

这个的看响应头中的Connection字段。上面的字段值为close,那么就会断开,通常状况下,HTTP1.1版本的话,一般请求头会包含Connection: Keep-Alive表示创建了持久链接,这样TCP链接会一直保持,以后请求统一站点的资源会复用这个链接。

上面的状况就会断开TCP链接,请求-响应流程结束。

到这里的话,网络请求就告一段落了,接下来的内容就是渲染流程了👇

渲染阶段

较为专业的术语总结为如下阶段:

  1. 构建DOM树
  2. 样式计算
  3. 布局阶段
  4. 分层
  5. 绘制
  6. 分块
  7. 光栅化
  8. 合成

关于渲染流程的话,能够看我以前总结的一篇✅✅✅

[1.1W字]写给女朋友的秘籍-浏览器工做原理(渲染流程)篇


15. 谈一谈你对重排和重绘理解

关于重排和重绘,能够上面的知识点去梳理,也就是渲染阶段,里面也梳理了部分的点,(●'◡'●)

偷个懒,看下面的文章噢👇

[1.1W字]写给女朋友的秘籍-浏览器工做原理(渲染流程)篇


16. 谈一谈跨域,同源策略,以及跨域解决方案

什么是跨域

跨域,是指浏览器不能执行其余网站的脚本。它是由浏览器的同源策略形成的,是浏览器对JavaScript实施的安全限制。

同源策略

同源策略是一个安全策略。所谓的同源,指的是协议,域名,端口相同。

同源策略
同源策略

浏览器处于安全方面的考虑,只容许本域名下的接口交互,不一样源的客户端脚本,在没有明确受权的状况下,不能读写对方的资源。

限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 没法读取
  • DOM 和 JS 对象没法获取
  • Ajax请求发送不出去

解决方案

固然了,我梳理了几个我以为工做中经常使用的,其余的自行去了解。

jsonp跨域

利用script标签没有跨域限制的漏洞,网页能够拿到从其余来源产生动态JSON数据,固然了JSONP请求必定要对方的服务器作支持才能够。

与AJAX对比

JSONP和AJAX相同,都是客户端向服务器发送请求,从服务器获取数据的方式。可是AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

JSONP优势

兼容性比较好,可用于解决主流浏览器的跨域数据访问的问题。缺点就是仅支持get请求,具备局限性,不安全,可能会受到XSS攻击。

思路👇

  • 建立script标签
  • 设置script标签的src属性,以问号传递参数,设置好回调函数callback名称
  • 插入html文本中
  • 调用回调函数,res参数就是获取的数据
let script = document.createElement('script');
 script.src = 'http://www.baidu.cn/login?username=TianTianUp&callback=callback';  document.body.appendChild(script);  function callback(res) {  console.log(res);  } 复制代码

固然,jquery也支持jsonp的实现方式

$.ajax({
 url: 'http://www.baidu.cn/login',  type: 'GET',  dataType: 'jsonp', //请求方式为jsonp  jsonpCallback: 'callback',  data: {  "username": "Nealyang"  }  }) 复制代码

JSONP优势

  • 它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制
  • 它的兼容性更好,在更加古老的浏览器中均可以运行,不须要XMLHttpRequest或ActiveX的支持
  • 而且在请求完毕后能够经过调用callback的方式回传结果。

JSONP缺点

  • 它只支持GET请求而不支持POST等其它类型的HTTP请求
  • 它只支持跨域HTTP请求这种状况,不能解决不一样域的两个页面之间如何进行JavaScript调用的问题

跨域资源共享 CORS

CORS(Cross-Origin Resource Sharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功仍是失败。目前,全部浏览器都支持该功能,IE浏览器不能低于IE10。整个CORS通讯过程,都是浏览器自动完成,不须要用户参与。对于开发者来讲,CORS通讯与同源的AJAX通讯没有差异,代码彻底同样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感受。

上面是引用,你要记住的关键点👇

CORS 须要浏览器和后端同时支持。IE 8 和 9 须要经过 XDomainRequest 来实现

  • 浏览器会自动进行 CORS 通讯,实现 CORS 通讯的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就能够开启 CORS。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源。

请求分为简单请求非简单请求,因此咱们的了解这两种状况。

简单请求

知足下面两个条件,就属于简单请求👇

条件1:使用下列方法之一:

  • GET
  • HEAD
  • POST

条件2:Content-Type 的值仅限于下列三者之一👇

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

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;

XMLHttpRequestUpload 对象可使用 XMLHttpRequest.upload 属性访问。

复杂请求

不符合以上条件的请求就确定是复杂请求了。 复杂请求的CORS请求,会在正式通讯以前,增长一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,经过该请求来知道服务端是否容许跨域请求。

直接上一个例子吧👇 看看一个完整的复杂请求吧,而且介绍一下CORS请求的字段。

//server2.js
let express = require('express') let app = express() let whitList = ['http://localhost:3000'] //设置白名单 app.use(function(req, res, next) {  let origin = req.headers.origin  if (whitList.includes(origin)) {  // 设置哪一个源能够访问我  res.setHeader('Access-Control-Allow-Origin', origin)  // 容许携带哪一个头访问我  res.setHeader('Access-Control-Allow-Headers', 'name')  // 容许哪一个方法访问我  res.setHeader('Access-Control-Allow-Methods', 'PUT')  // 容许携带cookie  res.setHeader('Access-Control-Allow-Credentials', true)  // 预检的存活时间  res.setHeader('Access-Control-Max-Age', 6)  // 容许返回的头  res.setHeader('Access-Control-Expose-Headers', 'name')  if (req.method === 'OPTIONS') {  res.end() // OPTIONS请求不作任何处理  }  }  next() }) app.put('/getData', function(req, res) {  console.log(req.headers)  res.setHeader('name', 'jw') //返回一个响应头,后台需设置  res.end('我不爱你') }) app.get('/getData', function(req, res) {  console.log(req.headers)  res.end('我不爱你') }) app.use(express.static(__dirname)) app.listen(4000) 复制代码

上述代码由http://localhost:3000/index.htmlhttp://localhost:4000/跨域请求,正如咱们上面所说的,后端是实现 CORS 通讯的关键。

上述的例子,必定对你会有所帮助的,这块代码,是跟着浪里行舟代码来的,参考处注明了出处。

与JSONP对比

  • JSONP只能实现GET请求,而CORS支持全部类型的HTTP请求。
  • 使用CORS,开发者可使用普通的XMLHttpRequest发起请求和得到数据,比起JSONP有更好的错误处理。
  • JSONP主要被老的浏览器支持,它们每每不支持CORS,而绝大多数现代浏览器都已经支持了CORS)

WebSocket协议跨域

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通讯,同时也是跨域的一种解决方案。

WebSocket和HTTP都是应用层协议,都基于 TCP 协议。可是 WebSocket 是一种双向通讯协议,在创建链接以后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在创建链接时须要借助 HTTP 协议,链接创建好了以后 client 与 server 之间的双向通讯就与 HTTP 无关了。

咱们先来看个例子👇

本地文件socket.html向localhost:3000发生数据和接受数据👇

// socket.html
<script>  let socket = new WebSocket('ws://localhost:3000');  socket.onopen = function () {  socket.send('我爱你');//向服务器发送数据  }  socket.onmessage = function (e) {  console.log(e.data);//接收服务器返回的数据  } </script>  复制代码

后端部分👇

// server.js
let WebSocket = require('ws'); //记得安装ws let wss = new WebSocket.Server({port:3000}); wss.on('connection',function(ws) {  ws.on('message', function (data) {  console.log(data);  ws.send('我不爱你')  }); })  复制代码

若是 你想去尝试的话,建议能够去玩一玩Socket.io,

  • 这是由于原生WebSocket API使用起来不太方便,它很好地封装了webSocket接口
  • 提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

nginx代理跨域


17. 谈一谈你对XSS攻击理解

什么是 XSS 攻击

XSS 全称是 Cross Site Scripting ,为了与CSS区分开来,故简称 XSS,翻译过来就是“跨站脚本”。

XSS是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。

最开始的时候,这种攻击是经过跨域来实现的,因此叫“跨域脚本”。发展到如今,往HTML文件中中插入恶意代码方式愈来愈多,因此是否跨域注入脚本已经不是惟一的注入手段了,可是 XSS 这个名字却一直保留至今。

注入恶意脚本能够完成这些事情:

  1. 窃取Cookie
  2. 监听用户行为,好比输入帐号密码后之间发给黑客服务器
  3. 在网页中生成浮窗广告
  4. 修改DOM伪造登入表单

通常的状况下,XSS攻击有三种实现方式

  • 存储型 XSS 攻击
  • 反射型 XSS 攻击
  • 基于 DOM 的 XSS 攻击

存储型 XSS 攻击

存储型XSS
存储型XSS

从图上看,存储型 XSS 攻击大体步骤以下:

  1. 首先黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中;
  2. 而后用户向网站请求包含了恶意 JavaScript 脚本的页面;
  3. 当用户浏览该页面的时候,恶意脚本就会将用户的 Cookie 信息等数据上传到服务器。

好比常见的场景:

在评论区提交一份脚本代码,假设先后端没有作好转义工做,那内容上传到服务器,在页面渲染的时候就会直接执行,至关于执行一段未知的JS代码。这就是存储型 XSS 攻击。

反射型 XSS 攻击

反射型 XSS 攻击指的就是恶意脚本做为网络请求的一部分,随后网站又把恶意的JavaScript脚本返回给用户,当恶意 JavaScript 脚本在用户页面中被执行时,黑客就能够利用该脚本作一些恶意操做。

举个例子:

http://TianTianUp.com?query=<script>alert("你受到了XSS攻击")</script>
复制代码

如上,服务器拿到后解析参数query,最后将内容返回给浏览器,浏览器将这些内容做为HTML的一部分解析,发现是Javascript脚本,直接执行,这样子被XSS攻击了。

这也就是反射型名字的由来,将恶意脚本做为参数,经过网络请求,最后通过服务器,在反射到HTML文档中,执行解析。

主要注意的就是,服务器不会存储这些恶意的脚本,这也算是和存储型XSS攻击的区别吧

基于 DOM 的 XSS 攻击

基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。具体来说,黑客经过各类手段将恶意脚本注入用户的页面中,在数据传输的时候劫持网络数据包

常见的劫持手段有:

  • WIFI路由器劫持
  • 本地恶意软件

阻止 XSS 攻击的策略

以上讲述的XSS攻击原理,都有一个共同点:让恶意脚本直接在浏览器执行。

针对三种不一样形式的XSS攻击,有如下三种解决办法

对输入脚本进行过滤或转码

对用户输入的信息过滤或者是转码

举个例子👇

转码后👇

&lt;script&gt;alert(&#39;你受到XSS攻击了&#39;)&lt;/script&gt;
复制代码

这样的代码在 html 解析的过程当中是没法执行的。

固然了对于<script><img><a>等关键字标签也是能够过来的,效果以下👇

复制代码

最后什么都没有剩下了

利用 CSP

该安全策略的实现基于一个称做 Content-Security-PolicyHTTP 首部。

能够移步MDN,有更加规范的解释。我在这里就是梳理一下吧。

CSP,即浏览器中的内容安全策略,它的核心思想大概就是服务器决定浏览器加载哪些资源,具体来讲有几个功能👇

  • 限制加载其余域下的资源文件,这样即便黑客插入了一个 JavaScript 文件,这个 JavaScript 文件也是没法被加载的;
  • 禁止向第三方域提交数据,这样用户数据也不会外泄;
  • 提供上报机制,能帮助咱们及时发现 XSS 攻击。
  • 禁止执行内联脚本和未受权的脚本;

利用 HttpOnly

因为不少 XSS 攻击都是来盗用 Cookie 的,所以还能够经过使用 HttpOnly 属性来保护咱们 Cookie 的安全。这样子的话,JavaScript 便没法读取 Cookie 的值。这样也能很好的防范 XSS 攻击。

一般服务器能够将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器经过 HTTP 响应头来设置的,下面是打开 Google 时,HTTP 响应头中的一段:

set-cookie: NID=189=M8l6-z41asXtm2uEwcOC5oh9djkffOMhWqQrlnCtOI; expires=Sat, 18-Apr-2020 06:52:22 GMT; path=/; domain=.google.com; HttpOnly
复制代码

总结

XSS 攻击是指浏览器中执行恶意脚本, 而后拿到用户的信息进行操做。主要分为存储型反射型文档型。防范的措施包括:

  • 对输入内容过滤或者转码,尤为是相似于<script><img><a>标签
  • 利用CSP
  • 利用Cookie的HttpOnly属性

除了以上策略以外,咱们还能够经过添加验证码防止脚本冒充用户提交危险操做。而对于一些不受信任的输入,还能够限制其输入长度,这样能够增大 XSS 攻击的难度。


18. 能不能说一说CSRF攻击

什么是CSRF攻击呢?

CSRF 英文全称是 Cross-site request forgery,因此又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登陆状态发起的跨站请求。简单来说,CSRF 攻击就是黑客利用了用户的登陆状态,并经过第三方的站点来作一些坏事。

通常的状况下,点开一个诱导你的连接,黑客会在你不知情的时候作哪些事情呢

1. 自动发起 Get 请求

黑客网页里面可能有一段这样的代码👇

<img src="http://bank.example/withdraw?amount=10000&for=hacker" > 
复制代码

在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。

bank.example就会收到包含受害者登陆信息的一次跨域请求。

2. 自动发起 POST 请求

黑客网页中有一个表单,自动提交的表单👇

<form action="http://bank.example/withdraw" method=POST>
 <input type="hidden" name="account" value="xiaoming" />  <input type="hidden" name="amount" value="10000" />  <input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script> 复制代码

访问该页面后,表单会自动提交,至关于模拟用户完成了一次POST操做。

一样也会携带相应的用户 cookie 信息,让服务器误觉得是一个正常的用户在操做,让各类恶意的操做变为可能。

3. 引诱用户点击连接

这种须要诱导用户去点击连接才会触发,这类的状况好比在论坛中发布照片,照片中嵌入了恶意连接,或者是以广告的形式去诱导,好比:

<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
  重磅消息!!!
  <a/>
复制代码

点击后,自动发送 get 请求,接下来和自动发 GET 请求部分同理。

以上三种状况,就是CSRF攻击原理,跟XSS对比的话,CSRF攻击并不须要将恶意代码注入HTML中,而是跳转新的页面,利用服务器的验证漏洞用户以前的登陆状态来模拟用户进行操做

防御策略

其实咱们能够想到,黑客只能借助受害者的**cookie**骗取服务器的信任,可是黑客并不能凭借拿到cookie,也看不到 cookie的内容。另外,对于服务器返回的结果,因为浏览器同源策略的限制,黑客也没法进行解析。

这就告诉咱们,咱们要保护的对象是那些能够直接产生数据改变的服务,而对于读取数据的服务,则不须要进行**CSRF**的保护。而保护的关键,是 在请求中放入黑客所不能伪造的信息

用户操做限制——验证码机制

方法:添加验证码来识别是否是用户主动去发起这个请求,因为必定强度的验证码机器没法识别,所以危险网站不能伪造一个完整的请求。

1. 验证来源站点

在服务器端验证请求来源的站点,因为大量的CSRF攻击来自第三方站点,所以服务器跨域禁止来自第三方站点的请求,主要经过HTTP请求头中的两个Header

  • Origin Header
  • Referer Header

这两个Header在浏览器发起请求时,大多数状况会自动带上,而且不能由前端自定义内容。

服务器能够经过解析这两个Header中的域名,肯定请求的来源域。

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

在某些状况下,这二者都是能够伪造的,经过AJax中自定义请求头便可,安全性略差。

2. 利用Cookie的SameSite属性

能够看看MDN对此的解释

SameSite能够设置为三个值,StrictLaxNone

  1. Strict模式下,浏览器彻底禁止第三方请求携带Cookie。好比请求sanyuan.com网站只能在sanyuan.com域名当中请求才能携带 Cookie,在其余网站请求都不能。
  2. Lax模式,就宽松一点了,可是只能在 get 方法提交表单况或者a 标签发送 get 请求的状况下能够携带 Cookie,其余状况均不能。
  3. 在None模式下,Cookie将在全部上下文中发送,即容许跨域发送。

3. CSRF Token

前面讲到CSRF的另外一个特征是,攻击者没法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。

那么咱们可使用Token,在不涉及XSS的前提下,通常黑客很难拿到Token。

能够看看这篇文章,将了Token是怎么操做的👉完全理解cookie,session,token

Token(令牌)作为Web领域验证身份是一个不错的选择,固然了,JWT有兴趣的也能够去了解一下。

Token步骤以下:

第一步:将CSRF Token输出到页面中

首先,用户打开页面的时候,服务器须要给这个用户生成一个Token,该Token经过加密算法对数据进行加密,通常Token都包括随机字符串和时间戳的组合,显然在提交时Token不能再放在Cookie中了(XSS可能会获取Cookie),不然又会被攻击者冒用。所以,为了安全起见Token最好仍是存在服务器的Session中,以后在每次页面加载时,使用JS遍历整个DOM树,对于DOM中全部的a和form标签后加入Token。这样能够解决大部分的请求,可是对于在页面加载以后动态生成的HTML代码,这种方法就没有做用,还须要程序员在编码时手动添加Token。

第二步:页面提交的请求携带这个Token

对于GET请求,Token将附在请求地址以后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来讲,要在 form 的最后加上: <input type=”hidden” name=”csrftoken” value=”tokenvalue”/> 这样,就把Token以参数的形式加入请求了。

第三步:服务器验证Token是否正确

当用户从客户端获得了Token,再次提交给服务器的时候,服务器须要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,若是加密字符串一致且时间未过时,那么这个Token就是有效的。

很是感兴趣的,能够仔细去阅读一下相关的文章,Token是如何加密的,又是如何保证不被攻击者获取道。

总结

CSRF(Cross-site request forgery), 即跨站请求伪造,本质是冲着浏览器分不清发起请求是否是真正的用户本人,因此防范的关键在于在请求中放入黑客所不能伪造的信息。从而防止黑客伪造一个完整的请求欺骗服务器。

防范措施:验证码机制,验证来源站点,利用Cookie的SameSite属性,CSRF Token

参考

❤️ 感谢你们

若是你以为这篇内容对你挺有有帮助的话:

  1. 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)

  2. 欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。

  3. 以为不错的话,也能够阅读TianTian近期梳理的文章(感谢掘友的鼓励与支持🌹🌹🌹):

本文使用 mdnice 排版

相关文章
相关标签/搜索