「查缺補漏」送你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 排版

相關文章
相關標籤/搜索