這些瀏覽器面試題,看看你能回答幾個?

前端南玖發表於2021-11-04

作為一名前端工程師,瀏覽器算是我們打交道最多的一個工具了,所以掌握相關瀏覽器的工作原理是一名合格的前端工程師必備的。

這篇文章主要講解瀏覽器相關的知識,文章內容比較長,知識點較多,非常建議收藏閱讀~

前言

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章~

1.常見的瀏覽器核心有哪些?

瀏覽器的核心可以分成兩部分:

渲染引擎JS引擎⚠️注意:我們常說的瀏覽器核心就是指渲染引擎

由於JS引擎越來越獨立,核心就指的只是渲染引擎了,渲染引擎主要用來請求網路頁面資源解析排版後呈現給使用者

瀏覽器/RunTime 核心(渲染引擎) JavaScript 引擎
Chrome Blink(28~) Webkit(Chrome 27) V8
FireFox Gecko SpiderMonkey
Safari Webkit JavaScriptCore
Edge EdgeHTML Chakra(For JavaScript)
IE Trident Chakra(For JScript)
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.瀏覽器的主要組成部分有哪些?

  • 使用者介面:包括位址列,前進/後退/重新整理/書籤?等按鈕
  • 瀏覽器引擎:在使用者介面和呈現引擎之間傳送指令
  • 渲染引擎:用來繪製請求的內容
  • 網路:用來完成網路呼叫,例如http請求,它具有平臺無關的介面,可以在不同平臺上工作
  • JavaScript直譯器:用來解析執行JavaScript程式碼
  • 使用者介面後端:用於繪製基本的視窗小部件,比如組合框和視窗,底層使用作業系統的使用者介面
  • 資料儲存:屬於持久層,瀏覽器在硬碟中儲存類似cookie的各種資料,HTML5定義了web database技術,這是一種輕量級完整的客戶端儲存技術

⚠️注意:與大多數瀏覽器不同的是,谷歌(Chrome)瀏覽器的每個標籤頁都分別對應一個呈現引擎例項。每個標籤頁都是一個獨立的程式

3.說一說從輸入URL到頁面呈現發生了什麼?

這個題可以說是面試最常見也是一道可以無限難的題了,一般面試官出這道題就是為了考察你的前端知識深度。

1.瀏覽器接受URL開啟網路請求執行緒(涉及到:瀏覽器機制,執行緒與程式等)

2.開啟網路執行緒到發出一個完整的http請求(涉及到:DNS查詢,TCP/IP請求,5層網路協議等)

3.從伺服器接收到請求到對應後臺接受到請求(涉及到:負載均衡,安全攔截,後臺內部處理等)

4.後臺與前臺的http互動(涉及到:http頭,響應碼,報文結構,cookie等)

5.快取問題(涉及到:http強快取與協商快取,快取頭,etag,expired,cache-control等)

6.瀏覽器接受到http資料包後的解析流程(涉及到html詞法分析,解析成DOM樹,解析CSS生成CSSOM樹,合併生成render渲染樹。然後layout佈局,painting渲染,複合圖層合成,GPU繪製,外鏈處理等)

7.css視覺化模型(涉及到:元素渲染規則,如:包含塊,控制框,BFC,IFC等)

8.JS引擎解析過程(涉及到:JS解析階段,預處理階段,執行階段生成執行上下文,VO(全域性物件),作用域鏈,回收機制等)

你會發現一個簡單的輸入URL到頁面呈現,之間會發生這麼多過程,是不是瞬間覺得崩潰了?(別急,這一章我們不講這麼深,先教你如何回答這個問題,後面這一節單獨出文章講)

  • 瀏覽器通過DNS伺服器得到域名的IP地址,向這個IP地址請求得到HTML文字
  • 瀏覽器渲染程式解析HTML文字,構建DOM樹
  • 解析HTML的同時,如果遇到內聯樣式或者樣式檔案,則下載並構建樣式規則,如果遇到JavaScript指令碼,則會下載執行指令碼
  • DOM樹和CSSOM構建完成之後,渲染程式將兩者合併成渲染樹(render tree)
  • 渲染程式開始對渲染樹進行佈局,生成佈局樹(layout tree)
  • 渲染樹對佈局樹進行繪製,生成繪製記錄

4.瀏覽器是如何解析程式碼的?

解析HTML

HTML是逐行解析的,瀏覽器的渲染引擎會將HTML文件解析並轉換成DOM節點。

  • 將HTML解析成許多Tokens
  • 將Tokens解析成object
  • 將object組合成一個DOM樹

解析CSS

瀏覽器會從右往左解析CSS選擇器

我們知道DOM樹與CSSOM樹合併成render樹,實際上是將CSSOM附著到DOM樹上,因此需要根據選擇器提供的資訊對DOM樹進行遍歷。

我們看一個例子?:

<style>
.nav .title span {color:blue}
</style>

<div class='nav'>
  <div class='title'>
    <span>南玖</span>
  </div>
  <div class="sub_title">前端</header>
</div>

從右至左的匹配:

  1. 先找到所有的最右節點 span,對於每一個 span,向上尋找節點 div.title
  2. 由 h3再向上尋找 div.nav 的節點
  3. 最後找到根元素 html 則結束這個分支的遍歷。

解析JS

在瀏覽器中有一個js解析器的工具,專門用來解析我們的js程式碼。

當瀏覽器遇到js程式碼時,立馬召喚“js解析器”出來工作。

解析器會找到js當中的所有變數、函式、引數等等,並且把變數賦值為未定義(undefined)。

把函式取出來成為一個函式塊,然後存放到倉庫當中。這件事情做完了之後才開始逐行解析程式碼(由上向下,由左向右),然後再去和倉庫進行匹配。

5.DOMContentLoaded與load的區別?

  • DOMContentLoaded:僅當DOM解析完成後觸發,不包括樣式表,圖片等資源。
  • Load:當頁面上所有的DOM,樣式表,指令碼,圖片等資源載入完畢事觸發。

6.瀏覽器重繪域重排的區別?

  • 重排: 部分渲染樹或整個渲染樹需要重新分析且節點尺寸需要重新計算,表現為重新生成佈局,重新排列元素
  • 重繪: 由於節點的幾何屬性發生改變或樣式改變,例如元素背景元素,表現為某些元素的外觀被改變

重繪不一定導致重排,但重排一定繪導致重繪

如何觸發重繪和重排?

任何改變用來構建渲染樹的資訊都會導致一次重排或重繪:

  • 新增、刪除、更新DOM節點
  • 通過display: none隱藏一個DOM節點-觸發重排和重繪
  • 通過visibility: hidden隱藏一個DOM節點-只觸發重繪,因為沒有幾何變化
  • 移動或者給頁面中的DOM節點新增動畫
  • 新增一個樣式表,調整樣式屬性
  • 使用者行為,例如調整視窗大小,改變字號,或者滾動。

如何避免重繪或重排?

  • 集中改變樣式:比如使用class的方式來集中改變樣式

  • 使用document.createDocumentFragment():我們可以通過createDocumentFragment建立一個遊離於DOM樹之外的節點,然後在此節點上批量操作,最後插入DOM樹中,因此只觸發一次重排

  • 提升為合成層

    將元素提升為合成層有以下優點:

    • 合成層的點陣圖,會交由 GPU 合成,比 CPU 處理要快
    • 當需要repaint 時,只需要 repaint 本身,不會影響到其他的層
    • 對於 transformopacity 效果,不會觸發 layoutpaint

    提升合成層的最好方式是使用 CSS 的 will-change 屬性:

    #target {
      will-change: transform;
    }
    

7.為什麼JS是單執行緒的?

這主要與JS的用途有關,JS作為瀏覽器的指令碼語言,最初主要是實現使用者與瀏覽器的互動,以及操作DOM。這就決定了它只能是單執行緒,否則會帶來許多複雜的同步問題。

舉個例子?: 如果JS是多執行緒的,其中一個執行緒要修改一個DOM元素,另外一個執行緒想要刪除這個DOM元素,這時候瀏覽器就不知道該聽誰的。所以為了避免複雜性,從一誕生,JavaScript就被設計成單執行緒。

為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質

8.CSS載入會阻塞DOM嗎?

先上結論

  • CSS不會阻塞DOM的解析,但會阻塞DOM的渲染
  • CSS會阻塞JS執行,但不會阻塞JS檔案的下載

CSSOM的作用

  • 第一個是提供給JavaScript操作樣式表的能力
  • 第二個是為佈局樹的合成提供基礎的樣式資訊
  • 這個CSSOM體現在DOM中就是document.styleSheets

由之前講到的瀏覽器渲染流程我們可以看出:

  • DOM和CSSOM通常是並行構建的,所以CSS載入不會阻塞DOM的解析

  • render樹是依賴DOM樹和CSSOM樹的,所以它必須等到兩者都載入完畢才能開始構建渲染,所以CSS載入會阻塞DOM的渲染

  • 由於JavaScript是可以操作DOM與CSS的,如果在修改這些元素屬性同時渲染介面(即JavaScript執行緒與UI執行緒同時進行),那麼渲染執行緒前後獲得的元素可能就不一致了。所以為了防止渲染出現不可預期的結果,瀏覽器設定GUI渲染執行緒與JavaScript執行緒為互斥的關係

JS需要等待CSS的下載,這是為什麼呢?(CSS阻塞DOM執行)

如果JS指令碼的內容是獲取元素的樣式,那它就必然依賴CSS。因為瀏覽器無法感知JS內部到底想幹什麼,為避免樣式獲取,就只好等前面所有的樣式下載完畢再執行JS。但JS檔案與CSS檔案下載是並行的,CSS檔案會在後面的JS檔案執行前先載入執行完畢,所以CSS會阻塞後面JS的執行

避免白屏,提高CSS的載入速度

  • 使用CDN(CDN會根據你的網路狀況,挑選最近的一個具有快取內容的節點為你提供資源,因此可以減少載入時間)
  • 對CSS進行壓縮
  • 合理使用快取
  • 減少http請求數,合併CSS檔案

9.JS會阻塞頁面嗎?

先上結論

JS會阻塞DOM的解析,因此也就會阻塞頁面的載入

這也是為什麼我們常說要把JS檔案放在最下面的原因

由於 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時渲染介面(即 JavaScript 執行緒和 UI 執行緒同時執行),那麼渲染執行緒前後獲得的元素資料就可能不一致了。

因此為了防止渲染出現不可預期的結果,瀏覽器設定 「GUI 渲染執行緒與 JavaScript 引擎為互斥」的關係。

當 JavaScript 引擎執行時 GUI 執行緒會被掛起,GUI 更新會被儲存在一個佇列中等到引擎執行緒空閒時立即被執行。

當瀏覽器在執行 JavaScript 程式的時候,GUI 渲染執行緒會被儲存在一個佇列中,直到 JS 程式執行完成,才會接著執行。

因此如果 JS 執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。

10.defer和async的區別?

  • 兩者都是非同步去載入外部JS檔案,不會阻塞DOM解析
  • Async是在外部JS載入完成後,瀏覽器空閒時,Load事件觸發前執行,標記為async的指令碼並不保證按照指定他們的先後順序執行,該屬性對於內聯指令碼無作用 (即沒有「src」屬性的指令碼)。
  • defer是在JS載入完成後,整個文件解析完成後,觸發 DOMContentLoaded 事件前執行,如果缺少 src 屬性(即內嵌指令碼),該屬性不應被使用,因為這種情況下它不起作用

11.瀏覽器的垃圾回收機制

垃圾回收是一種自動的記憶體管理機制。當計算機上的動態記憶體不再需要時,就應該予以釋放。

需要注意的是,自動的意思是瀏覽器可以自動幫助我們回收記憶體垃圾,但並不代表我們不用關心記憶體管理,如果操作不當,JavaScript中仍然會出現記憶體溢位的情況,造成系統崩潰。

由於字串,陣列,物件等都沒有固定大小,因此需要當它們大小已知時,才能對他們進行動態的儲存分配。JavaScript程式每次建立字串,陣列或物件時,直譯器都必須分配記憶體來儲存那個實體。

JavaScript直譯器可以檢測到何時程式不在使用一個物件了,當它確定這個物件是無用的時候,他就知道不再需要這個物件了,就可以把它佔用的記憶體釋放掉了。

瀏覽器通常採用的垃圾回收有兩種方法:標記清除引用計數

標記清除

這是JavaScript中最常用的垃圾回收方式

從2012年起,所有現代瀏覽器都使用了標記清除的垃圾回收方法,除了低版本IE還是採用的引用計數法。

那麼什麼叫標記清除呢?

JavaScript中有一個全域性物件,定期的,垃圾回收器將從這個全域性物件開始,找出所有從這個全域性物件開始引用的物件,再找這些物件引用的物件...對這些活躍的物件標記,這是標記階段。清楚階段就是清楚那些沒有被標記的物件。

標記清除有一個問題,就是在清除之後,記憶體空間是不連續的,即出現了記憶體碎片。如果後面需要一個比較大的連續的記憶體空間,那將不能滿足要求。而標記整理 方法可以有效德地解決這個問題。

在標記的過程中,引入了概念:三色標記法,三色為:

  • 白:未被標記的物件,即不可達物件(沒有掃描到的物件),可回收
  • 灰:已被標記的物件(可達物件),但是物件還沒有被掃描完,不可回收
  • 黑:已被掃描完(可達物件),不可回收

標記整理:

標記階段與標記清除法沒什麼區別,只是標記結束後,標記整理法會將存活的物件向記憶體的一邊移動,最後清理掉邊界記憶體。

引用計數

引用計數的含義是跟蹤記錄每個值被引用的次數。當一個變數A被賦值時,這個值的引用次數就是1,當變數A重新賦值後,則之前那個值的引用次數就減1。當引用次數變成0時,則說明沒有辦法再訪問這個值了,所以就可以清除這個值佔用的記憶體了。

大多數瀏覽器已經放棄了這種回收方式

記憶體洩漏

為避免記憶體洩漏,一旦資料不再使用,最好通過將其值設為null來釋放其引用,這個方法叫做接觸引用

哪些情況會造成記憶體洩漏?如何避免?

以 Vue 為例,通常有這些情況:

  • 監聽在 window/body 等事件沒有解綁
  • 綁在 EventBus 的事件沒有解綁
  • Vuex$storewatch 了之後沒有 unwatch
  • 使用第三方庫建立,沒有呼叫正確的銷燬函式

解決辦法:beforeDestroy 中及時銷燬

  • 繫結了 DOM/BOM 物件中的事件 addEventListenerremoveEventListener
  • 觀察者模式 $on$off處理。
  • 如果元件中使用了定時器,應銷燬處理。
  • 如果在 mounted/created 鉤子中使用了第三方庫初始化,對應的銷燬。
  • 使用弱引用 weakMapweakSet
瀏覽器中不同型別變數的記憶體都是何時釋放的?
  • 引用型別
    • 在沒有引用之後,通過 V8 自動回收。
  • 基本型別
    • 如果處於閉包的情況下,要等閉包沒有引用才會被 V8 回收。
    • 非閉包的情況下,等待 V8 的新生代切換的時候回收。

12.說一說瀏覽器的快取機制?

認識瀏覽器快取

當瀏覽器請求一個網站時,會載入各種資源,對於一些不經常變動的資源,瀏覽器會將他們儲存在本地記憶體中,下次訪問時直接載入這些資源,提高訪問速度。

如何知道資源是請求的伺服器還是讀取的快取呢?

看上面這張圖,有些資源的size值是大小,有些是from disk cache,有些是from memory cache,顯示大小的是請求的伺服器資源,而顯示後面兩種的則是讀取的快取。

  • disk cache: 就是將資源儲存在磁碟中,等待下次訪問時不需重新下載,直接從磁碟中讀取,它的直接操作物件為CurlCacheManager。(效率比記憶體快取慢,但儲存容量大,儲存時間長)
  • memory cache: 就是將資源快取到記憶體中,等待下次訪問時不需重新下載,直接從記憶體中讀取。(從效率上看它是最快的,從存活時間來看,它是最短的。)
- memory cache disk cache
相同點 只能儲存一些派生類資原始檔 只能儲存一些派生類資原始檔
不同點 退出程式時資料會被清除 退出程式時資料不會被清除
儲存資源 一般指令碼、字型、圖片會存在記憶體當中 一般非指令碼會存在記憶體當中,如css等

瀏覽器快取分類

  • 強快取
  • 協商快取

瀏覽器在向伺服器請求資源時,首先判斷是否命中強快取,沒命中再判斷是否命中協商快取

強快取

瀏覽器在載入資源時,會先根據本地快取資源的header中判斷是否命中強快取,如果命中則直接使用快取中的資源,不會再向伺服器傳送請求。(這裡的header中的資訊指的是 expirescache-control

Expires

該欄位是 http1.0 時的規範,它的值為一個絕對時間的 GMT 格式的時間字串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT。這個時間代表著這個資源的失效時間,在此時間之前,即命中快取。這種方式有一個明顯的缺點,由於失效時間是一個絕對時間,所以當伺服器與客戶端時間偏差較大時,就會導致快取混亂。所以這種方式很快在後來的HTTP1.1版本中被拋棄了。

Cache-Control

Cache-Control 是 http1.1 時出現的 header 資訊,主要是利用該欄位的 max-age 值來進行判斷,它是一個相對時間,例如 Cache-Control:max-age=3600,代表著資源的有效期是 3600 秒。cache-control 除了該欄位外,還有下面幾個比較常用的設定值:

no-cache:需要進行協商快取,傳送請求到伺服器確認是否使用快取。

no-store:禁止使用快取,每一次都要重新請求資料。

public:可以被所有的使用者快取,包括終端使用者和 CDN 等中間代理伺服器。

private:只能被終端使用者的瀏覽器快取,不允許 CDN 等中繼快取伺服器對其快取。

Cache-Control 與 Expires 可以在服務端配置同時啟用,同時啟用的時候 Cache-Control 優先順序高。

協商快取

當強快取沒命中時,瀏覽器會傳送一個請求到伺服器,伺服器根據 header 中的資訊來判斷是否命中協商快取。如果命中,則返回304 ,告訴瀏覽器資源未更新,可以使用本地快取。(這裡的header資訊指的是Last-Modify/If-Modify-SinceETag/If-None-Match

Last-Modify/If-Modify-Since

瀏覽器第一次請求一個資源的時候,伺服器返回的 header 中會加上 Last-Modify,Last-modify 是一個時間標識該資源的最後修改時間。

當瀏覽器再次請求該資源時,request 的請求頭中會包含 If-Modify-Since,該值為快取之前返回的 Last-Modify。伺服器收到 If-Modify-Since 後,根據資源的最後修改時間判斷是否命中快取。

如果命中快取,則返回 304,並且不會返回資源內容,並且不會返回 Last-Modify。

缺點:

短時間內資源發生了改變,Last-Modified 並不會發生變化。

週期性變化。如果這個資源在一個週期內修改回原來的樣子了,我們認為是可以使用快取的,但是 Last-Modified 可不這樣認為,因此便有了 ETag。

ETag/If-None-Match

與 Last-Modify/If-Modify-Since 不同的是,Etag/If-None-Match 返回的是一個校驗碼。ETag 可以保證每一個資源是唯一的,資源變化都會導致 ETag 變化。伺服器根據瀏覽器上送的 If-None-Match 值來判斷是否命中快取。

與 Last-Modified 不一樣的是,當伺服器返回 304 Not Modified 的響應時,由於 ETag 重新生成過,response header 中還會把這個 ETag 返回,即使這個 ETag 跟之前的沒有變化。

Last-Modified 與 ETag 是可以一起使用的,伺服器會優先驗證 ETag,一致的情況下,才會繼續比對 Last-Modified,最後才決定是否返回 304。

總結

當瀏覽器訪問一個已經訪問過的資源是,它的步驟是:

1.先看是否命中強快取,命中?的話直接使用快取

2.沒命中強快取,則會傳送請求到伺服器看是否命中?協商快取

3.如果命中了協商快取,伺服器會返回304告訴瀏覽器可以使用本地快取

4.沒命中協商快取,則伺服器會返回新的資源給瀏覽器

13.什麼是瀏覽器的同源策略,以及跨域?

同源策略

同源策略是瀏覽器的一種自我保護行為。所謂的同源指的是:協議,域名,埠均要相同

瀏覽器中大部分內容都是受同源策略限制的,但是以下三個標籤不受限制:

<img src="..." />
<link href="..." />
<script src="..."></script>

跨域

跨域指的是瀏覽器不能執行其它域名下的指令碼。它是由瀏覽器的同源策略限制的。

你可能會想跨域請求到底有沒有傳送到伺服器?

事實上,跨域請求時能夠傳送到伺服器的,並且伺服器也能過接受的請求並正常返回結果,只是結果被瀏覽器攔截了。

跨域解決方案(列出幾個常用的)

JSONP

它主要是利用script標籤不受瀏覽器同源策略的限制,可以拿到從其他源傳輸過來的資料,需要服務端支援。

優缺點:

相容性比較好,可用於解決主流瀏覽器的跨域資料訪問的問題。缺點就是僅支援get請求,具有侷限性,不安全,可能會受到XSS攻擊。

思路:

  • 宣告一個回撥函式,其函式名(如show)當做引數值,要傳遞給跨域請求資料的伺服器,函式形參為要獲取目標資料(伺服器返回的data)。

  • 建立一個<script>標籤,把那個跨域的API資料介面地址,賦值給script的src,還要在這個地址中向伺服器傳遞該函式名(可以通過問號傳參:?callback=show)。

  • 伺服器接收到請求後,需要進行特殊的處理:把傳遞進來的函式名和它需要給你的資料拼接成一個字串,例如:傳遞進去的函式名是show,它準備好的資料是show('南玖')

  • 最後伺服器把準備的資料通過HTTP協議返回給客戶端,客戶端再呼叫執行之前宣告的回撥函式(show),對返回的資料進行操作。

// front
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'wxgongzhonghao' },
  callback: 'show'
}).then(data => {
  console.log(data)
})
// server 藉助express框架
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show
  res.end(`${callback}('關注前端南玖')`)
})
app.listen(3000)

上面這段程式碼相當於向http://localhost:3000/say?wd=wxgongzhonghao&callback=show這個地址請求資料,然後後臺返回show('關注前端南玖'),最後會執行show()這個函式,列印出'關注前端南玖'

跨域資源共享(CORS)

CORS(Cross-Origin Resource Sharing)跨域資源共享,定義了必須在訪問跨域資源時,瀏覽器與伺服器應該如何溝通。CORS背後的基本思想是使用自定義的HTTP頭部讓瀏覽器與伺服器進行溝通,從而決定請求或響應是應該成功還是失敗。

CORS 需要瀏覽器和後端同時支援。IE 8 和 9 需要通過 XDomainRequest 來實現

瀏覽器會自動進行 CORS 通訊,實現 CORS 通訊的關鍵是後端。只要後端實現了 CORS,就實現了跨域。

服務端設定 Access-Control-Allow-Origin 就可以開啟 CORS。 該屬性表示哪些域名可以訪問資源,如果設定萬用字元則表示所有網站都可以訪問資源。

雖然設定 CORS 和前端沒什麼關係,但是通過這種方式解決跨域問題的話,會在傳送請求時出現兩種情況,分別為簡單請求複雜請求

簡單請求:(滿足以下兩個條件,就是簡單請求)

1.請求方法為以下三個之一:

  • GET
  • POST
  • HEAD

2.Content-Type的為以下三個之一:

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

複雜請求:

不是簡單請求那它肯定就是複雜請求了。複雜請求的CORS請求,會在正式發起請求前,增加一次HTTP查詢請求,稱為預檢 請求,該請求是option方法的,通過該請求來知道服務端是否允許該跨域請求。

Nginx反向代理

Nginx 反向代理的原理很簡單,即所有客戶端的請求都必須經過nginx處理,nginx作為代理伺服器再將請求轉發給後端,這樣就規避了瀏覽器的同源策略。

14.說說什麼是XSS攻擊

什麼是XSS?

XSS 全稱是 Cross Site Scripting,為了與css區分開來,所以簡稱XSS,中文叫作跨站指令碼

XSS是指黑客往頁面中注入惡意指令碼,從而在使用者瀏覽頁面時利用惡意指令碼對使用者實施攻擊的一種手段。

XSS能夠做什麼?

  • 竊取Cookie
  • 監聽使用者行為,比如輸入賬號密碼後之間發給黑客伺服器
  • 在網頁中生成浮窗廣告
  • 修改DOM偽造登入表單

XSS實現方式

  • 儲存型XSS攻擊
  • 反射型XSS攻擊
  • 基於DOM的XSS攻擊

如何阻止XSS攻擊?

對輸入指令碼進行過濾或轉碼

對使用者輸入的資訊過濾或者轉碼,保證使用者輸入的內容不能在HTML解析的時候執行。

利用CSP

該安全策略的實現基於一個稱作 Content-Security-Policy的HTTP首部。(瀏覽器內容安全策略)它的核心思想就是伺服器決定瀏覽器載入那些資源。

  • 限制載入其他域下的資原始檔,這樣即使黑客插入了一個 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

對於不受信任的輸入,可以限制輸入長度

15.說說什麼是CSRF攻擊?

什麼是CSRF攻擊?

CSRF 全稱 Cross-site request forgery,中文為跨站請求偽造 ,攻擊者誘導受害者進入第三方網站,在第三方網站中,向被攻擊網站傳送跨站請求。利用受害者在被攻擊網站已經獲取的註冊憑證,繞過後臺的使用者驗證,達到冒充使用者對被攻擊的網站執行某項操作的目的。 CSRF攻擊就是黑客利用使用者的登入狀態,並通過第三方站點來幹一些嘿嘿嘿的壞事

幾種常見的攻擊型別

1.GET型別的CSRF

GET型別的CSRF非常簡單,通常只需要一個HTTP請求:

 <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型別的CSRF

這種型別的CSRF利用起來通常使用的是一個自動提交的表單,如:

 <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操作。

3.連結型別的CSRF

連結型別的CSRF並不常見,比起其他兩種使用者開啟頁面就中招的情況,這種需要使用者點選連結才會觸發。這種型別通常是在論壇中釋出的圖片中嵌入惡意連結,或者以廣告的形式誘導使用者中招,攻擊者通常會以比較誇張的詞語誘騙使用者點選,例如:

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

由於之前使用者登入了信任的網站A,並且儲存登入狀態,只要使用者主動訪問上面的這個PHP頁面,則表示攻擊成功。

CSRF的特點

  • 攻擊一般發起在第三方網站,而不是被攻擊的網站。被攻擊的網站無法防止攻擊發生。
  • 攻擊利用受害者在被攻擊網站的登入憑證,冒充受害者提交操作;而不是直接竊取資料。
  • 整個過程攻擊者並不能獲取到受害者的登入憑證,僅僅是“冒用”。
  • 跨站請求可以用各種方式:圖片URL、超連結、CORS、Form提交等等。部分請求方式可以直接嵌入在第三方論壇、文章中,難以進行追蹤。

CSRF通常是跨域的,因為外域通常更容易被攻擊者掌控。但是如果本域下有容易被利用的功能,比如可以發圖和連結的論壇和評論區,攻擊可以直接在本域下進行,而且這種攻擊更加危險。

防護策略

黑客只能藉助受害者的cookie 騙取伺服器的信任,但是黑客並不能憑藉拿到「cookie」,也看不到 「cookie」的內容。另外,對於伺服器返回的結果,由於瀏覽器「同源策略」的限制,黑客也無法進行解析。

這就告訴我們,我們要保護的物件是那些可以直接產生資料改變的服務,而對於讀取資料的服務,則不需要進行CSRF的保護。而保護的關鍵,是 「在請求中放入黑客所不能偽造的資訊」

同源檢測

既然CSRF大多來自第三方網站,那麼我們就直接禁止外域(或者不受信任的域名)對我們發起請求。

那麼問題來了,我們如何判斷請求是否來自外域呢?

在HTTP協議中,每一個非同步請求都會攜帶兩個Header,用於標記來源域名:

  • Origin Header
  • Referer Header

這兩個Header在瀏覽器發起請求時,大多數情況會自動帶上,並且不能由前端自定義內容。 伺服器可以通過解析這兩個Header中的域名,確定請求的來源域。

使用Origin Header確定來源域名

在部分與CSRF有關的請求中,請求的Header中會攜帶Origin欄位。欄位內包含請求的域名(不包含path及query)。

如果Origin存在,那麼直接使用Origin中的欄位確認來源域名就可以。

但是Origin在以下兩種情況下並不存在:

  • IE11同源策略: IE 11 不會在跨站CORS請求上新增Origin標頭,Referer頭將仍然是唯一的標識。最根本原因是因為IE 11對同源的定義和其他瀏覽器有不同,有兩個主要的區別,可以參考MDN Same-origin_policy#IE_Exceptions
  • 302重定向: 在302重定向之後Origin不包含在重定向的請求中,因為Origin可能會被認為是其他來源的敏感資訊。對於302重定向的情況來說都是定向到新的伺服器上的URL,因此瀏覽器不想將Origin洩漏到新的伺服器上。
使用Referer Header確定來源域名

根據HTTP協議,在HTTP頭中有一個欄位叫Referer,記錄了該HTTP請求的來源地址。 對於Ajax請求,圖片和script等資源請求,Referer為發起請求的頁面地址。對於頁面跳轉,Referer為開啟頁面歷史記錄的前一個頁面地址。因此我們使用Referer中連結的Origin部分可以得知請求的來源域名。

這種方法並非萬無一失,Referer的值是由瀏覽器提供的,雖然HTTP協議上有明確的要求,但是每個瀏覽器對於Referer的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方(即瀏覽器)來保障,從理論上來講,這樣並不是很安全。在部分情況下,攻擊者可以隱藏,甚至修改自己請求的Referer。

2014年,W3C的Web應用安全工作組釋出了Referrer Policy草案,對瀏覽器該如何傳送Referer做了詳細的規定。截止現在新版瀏覽器大部分已經支援了這份草案,我們終於可以靈活地控制自己網站的Referer策略了。新版的Referrer Policy規定了五種Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。之前就存在的三種策略:never、default和always,在新標準裡換了個名稱。他們的對應關係如下:

策略名稱 屬性值(新) 屬性值(舊)
No Referrer no-Referrer never
No Referrer When Downgrade no-Referrer-when-downgrade default
Origin Only (same or strict) origin origin
Origin When Cross Origin (strict) origin-when-crossorigin -
Unsafe URL unsafe-url always

根據上面的表格因此需要把Referrer Policy的策略設定成same-origin,對於同源的連結和引用,會傳送Referer,referer值為Host不帶Path;跨域訪問則不攜帶Referer。例如:aaa.com引用bbb.com的資源,不會傳送Referer。

設定Referrer Policy的方法有三種:

  1. 在CSP設定
  2. 頁面頭部增加meta標籤
  3. a標籤增加referrerpolicy屬性

上面說的這些比較多,但我們可以知道一個問題:攻擊者可以在自己的請求中隱藏Referer。如果攻擊者將自己的請求這樣填寫:

 <img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer"> 

那麼這個請求發起的攻擊將不攜帶Referer。

另外在以下情況下Referer沒有或者不可信:

1.IE6、7下使用window.location.href=url進行介面的跳轉,會丟失Referer。

2.IE6、7下使用window.open,也會缺失Referer。

3.HTTPS頁面跳轉到HTTP頁面,所有瀏覽器Referer都丟失。

4.點選Flash上到達另外一個網站的時候,Referer的情況就比較雜亂,不太可信。

無法確認來源域名情況

當Origin和Referer標頭檔案不存在時該怎麼辦?如果Origin和Referer都不存在,建議直接進行阻止,特別是如果您沒有使用隨機CSRF Token(參考下方)作為第二次檢查。

如何阻止外域請求

通過Header的驗證,我們可以知道發起請求的來源域名,這些來源域名可能是網站本域,或者子域名,或者有授權的第三方域名,又或者來自不可信的未知域名。

我們已經知道了請求域名是否是來自不可信的域名,我們直接阻止掉這些的請求,就能防禦CSRF攻擊了嗎?

且慢!當一個請求是頁面請求(比如網站的主頁),而來源是搜尋引擎的連結(例如百度的搜尋結果),也會被當成疑似CSRF攻擊。所以在判斷的時候需要過濾掉頁面請求情況,通常Header符合以下情況:

Accept: text/html
Method: GET

但相應的,頁面請求就暴露在了CSRF的攻擊範圍之中。如果你的網站中,在頁面的GET請求中對當前使用者做了什麼操作的話,防範就失效了。

例如,下面的頁面請求:

GET https://example.com/addComment?comment=XXX&dest=orderId

注:這種嚴格來說並不一定存在CSRF攻擊的風險,但仍然有很多網站經常把主文件GET請求掛上引數來實現產品功能,但是這樣做對於自身來說是存在安全風險的。

另外,前面說過,CSRF大多數情況下來自第三方域名,但並不能排除本域發起。如果攻擊者有許可權在本域釋出評論(含連結、圖片等,統稱UGC),那麼它可以直接在本域發起攻擊,這種情況下同源策略無法達到防護的作用。

綜上所述:同源驗證是一個相對簡單的防範方法,能夠防範絕大多數的CSRF攻擊。但這並不是萬無一失的,對於安全性要求較高,或者有較多使用者輸入內容的網站,我們就要對關鍵的介面做額外的防護措施。

CSRF Token

前面講到CSRF的另一個特徵是,攻擊者無法直接竊取到使用者的資訊(Cookie,Header,網站內容等),僅僅是冒用Cookie中的資訊。

而CSRF攻擊之所以能夠成功,是因為伺服器誤把攻擊者傳送的請求當成了使用者自己的請求。那麼我們可以要求所有的使用者請求都攜帶一個CSRF攻擊者無法獲取到的Token。伺服器通過校驗請求是否攜帶正確的Token,來把正常的請求和攻擊的請求區分開,也可以防範CSRF的攻擊。

利用Cookie的SameSite屬性

可以看看MDN對此的解釋

SameSite可以設定為三個值,StrictLaxNone

  1. Strict模式下,瀏覽器完全禁止第三方請求攜帶Cookie。比如請求sanyuan.com網站只能在sanyuan.com域名當中請求才能攜帶 Cookie,在其他網站請求都不能。
  2. Lax模式,就寬鬆一點了,但是隻能在 get 方法提交表單況或者a 標籤傳送 get 請求的情況下可以攜帶 Cookie,其他情況均不能。
  3. 在None模式下,Cookie將在所有上下文中傳送,即允許跨域傳送。

點贊、收藏和評論

我是前端南玖,感謝各位的:點贊、收藏和評論,我們下期見!
歡迎?加入前端交流群928029210,一起交流~

相關文章