「查缺補漏」高頻考點瀏覽器面試題

SHERlocked93發表於2020-08-13

前言

想要成為一名合格的前端工程師,掌握相關瀏覽器的工作原理是必備的,這樣子才會有一個完整知識體系,要是「能參透瀏覽器的工作原理,你就能解決80%的前端難題」。

這篇梳理的話,更多的是對瀏覽器工作原理篇的查缺補漏,對於一些沒有涉及到的知識點,準備梳理梳理,也正好回顧之前梳理的內容。

感謝掘友的鼓勵與支援????????????,往期文章都在最後梳理出來了(●'◡'●)


「接下來以問題形式展開梳理」

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

瀏覽器/RunTime核心(渲染引擎)JavaScript 引擎
Chromewebkit->blinkV8
FireFoxGeckoSpiderMonkey
SafariWebkitJavaScriptCore
EdgeEdgeHTMLChakra(for JavaScript)
IETridentJScript(IE3.0-IE8.0)
OperaPresto->blinkLinear 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 瀏覽器的每個標籤頁都分別對應一個呈現引擎例項。每個標籤頁都是一個獨立的程式。


3. 為什麼JavaScript是單執行緒的,與非同步衝突嗎

補充:JS中其實是沒有執行緒概念的,所謂的單執行緒也只是相對於多執行緒而言。JS的設計初衷就沒有考慮這些,針對JS這種不具備並行任務處理的特性,我們稱之為“單執行緒”。

JS的單執行緒是指一個瀏覽器程式中只有一個JS的執行執行緒,同一時刻內只會有一段程式碼在執行。

舉個通俗例子,假設JS支援多執行緒操作的話,JS可以操作DOM,那麼一個執行緒在刪除DOM,另外一個執行緒就在獲取DOM資料,這樣子明顯不合理,這算是證明之一。

來看段程式碼????

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


列印結果就是首先是很多個first,然後再是second。

非同步機制是瀏覽器的兩個或以上常駐執行緒共同完成的,舉個例子,比如非同步請求由兩個常駐執行緒,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中,使用<embed>(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的src屬性設定給src

  4. 繫結window的scroll事件

當然了,為了使用者的體驗更加,預設的情況下,設定一個「佔點陣圖」

本次測試程式碼

CSS程式碼????

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

HTML????

<img src="default.png" 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('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('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 相關的一些屬性????

這裡主要說一些大家可能沒有注意的點:

「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攻擊會將講到,這裡過。

這個屬性值修改有什麼影響呢?

從上圖可以看出,對大部分 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. 聊一聊瀏覽器快取

瀏覽器快取是效能優化的一個重要手段,對於理解快取機制而言也是很重要的,我們來梳理一下吧????

強快取

強快取兩個相關欄位,「Expires」「Cache-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-Modified」「ETag」。兩者各有優勢,並不存在誰對誰有絕對的優勢,與上面所講的強快取兩個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.im/

????????????

網路請求

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. 如果請求的域名是http://xxx.com,則返回負責com域的伺服器

    2. 如果是http://xxx.cn,則發給負責cn域的伺服器

    3. 如果是http://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 攻擊大致步驟如下:

  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攻擊,有以下三種解決辦法

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

對使用者輸入的資訊過濾或者是轉碼

舉個例子????

轉碼後????

<script>alert('你受到XSS攻擊了')</script>


這樣的程式碼在 html 解析的過程中是無法執行的。

當然了對於<script><img><a>等關鍵字標籤也是可以過來的,效果如下????

最後什麼都沒有剩下了

利用 CSP

該安全策略的實現基於一個稱作 Content-Security-Policy的 HTTP 首部。

可以移步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

參考

  • 還在看那些老掉牙的效能優化文章麼?這些最新效能指標瞭解下

  • 原來 CSS 與 JS 是這樣阻塞 DOM 解析和渲染的

  • 從瀏覽器多程式到JS單執行緒,JS執行機制最全面的一次梳理

  • 實現圖片懶載入的幾種方案比較

  • 九種跨域方式實現原理(完整版)

  • 極客時間專欄

  • 還分不清 Cookie、Session、Token、JWT?


最後

如果你覺得這篇內容對你挺有啟發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

  2. 歡迎加我微信「qianyu443033099」拉你進技術群,長期交流學習...

  3. 關注公眾號「前端下午茶」,持續為你推送精選好文,也可以加我為好友,隨時聊騷。

「查缺補漏」高頻考點瀏覽器面試題

點個在看支援我吧,轉發就更好了



相關文章