確保所有 API 的使用者啟用作業一致

Mustaq Ahmed
Joe Medley
Joe Medley

為避免惡意指令碼濫用敏感 API (例如彈出式視窗、全螢幕等),瀏覽器會透過使用者啟用來控管這些 API 的存取權。使用者啟用是瀏覽工作階段的狀態,會因使用者動作而產生:「活躍」狀態通常表示使用者目前正在與網頁互動,或是在網頁載入後已完成互動。使用者手勢是相同概念的熱門但誤導性字詞。舉例來說,使用者的滑動或閃爍手勢不會啟動網頁,因此從指令碼的角度來看,也並非由使用者啟動。

現今的主要瀏覽器顯示,使用者啟動控制項如何控制啟用管制的 API 方面的行為差異相當大。在 Chrome 中,該實作是以權杖式模型為基礎,結果過於複雜,無法為所有啟用的 API 定義一致的行為。例如,Chrome 一直無法透過 postMessage()setTimeout() 呼叫存取啟用管制的 API;使用者啟動作業也不支援 PromiseXHR遊戲手把互動等。請注意,其中有些是長期熱門錯誤。

在第 72 版中,Chrome 提供使用者啟用第 2 版,因此所有啟用的 API 的使用者啟用功能皆已完成。這個方法可解決上述的不一致問題 (還有一些,例如 MessageChannels),因為我們相信這可以簡化使用者啟動程序的網頁開發作業。此外,新的實作方式為建議的新規格提供參考實作,有助於長期將所有瀏覽器整合在一起。

使用者啟用 v2 的運作方式為何?

新 API 會在頁框階層的每個 window 物件中保留兩個位元的使用者啟用狀態:用於記錄使用者啟用狀態的固定位元 (如果畫面曾有使用者啟動),另一個則是目前狀態的暫時性位元 (如果影格在使用者啟動約一秒後啟動)。在影格設定後,固定式位元絕不會重設。每次使用者互動時都會設定暫時位元,並會在到期時間結束 (約一秒) 後或透過啟用消耗的 API (例如 window.open()) 時重設。

請注意,不同的啟用管制 API 仰賴使用者啟用的方式不同;新的 API 不會變更上述任何 API 專屬行為。舉例來說,由於 window.open() 會照常利用使用者啟用行為,因此每次使用者啟動只能有一個彈出式視窗,但如果頁框 (或其任何子頁框) 曾看到使用者操作,Navigator.prototype.vibrate() 仍可持續有效,以此類推。

異動內容

  • 使用者啟用 v2 針對跨影格邊界的使用者啟用瀏覽權限進行正式化的概念:與特定影格的互動現在無論來源為何,都會啟用所有包含影格 (且僅限這些影格)。(在 Chrome 72 版中,我們提供暫時性的解決方法,可讓您將所有相同來源的影格的瀏覽權限擴大至適用範圍。一旦有辦法明確將使用者啟用作業明確傳遞至子頁框,我們就會移除這個解決方法)。
  • 從已啟用的影格呼叫啟用管制的 API 時,如果從事件處理常式程式碼外部呼叫,只要使用者的啟用狀態為「有效」(例如未使用或未使用),就會運作。在使用者啟用第 2 版之前,它會無條件失敗。
  • 在到期時間間隔內,如有多筆未使用的使用者互動,會整合到與最後一個互動對應的單次啟動中。

啟用管制 API 的一致性範例

以下兩個彈出式視窗 (使用 window.open() 開啟) 的範例,說明瞭使用者啟用 v2 如何使啟用管制的 API 行為保持一致。

鏈結 setTimeout() 呼叫

這個範例來自 我們的 setTimeout() 示範。如果 click 處理常式嘗試在一秒內嘗試開啟彈出式視窗,則無論程式碼如何「組合」延遲,都應該會成功。使用者啟用 v2 符合此預期,因此下列每個事件處理常式都會在 click 開啟彈出式視窗 (延遲時間為 100 毫秒):

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

如果沒有使用者啟用第 2 版,在我們測試的所有瀏覽器中,第二個事件處理常式就會失敗。(即使在某些情況下的第一個錯誤會失敗)。

跨網域 postMessage() 呼叫

以下是我們的 postMessage() 示範範例。假設跨來源子頁框中的 click 處理常式會將兩則訊息直接傳送至父項頁框。上層頁框應可在收到下列任一訊息時開啟彈出式視窗 (但不能同時顯示兩者):

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

如果沒有「使用者啟用」第 2 版,上層頁框在收到第二則訊息時,無法開啟彈出式視窗。即使第一則訊息「鏈結」到另一個跨來源影格 (也就是第一個接收端將訊息轉發給其他訊息),則即使第一則訊息失敗。

此做法適用於使用者啟用 2 版,無論是原始形式或鏈結,皆適用。