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

Mustaq Ahmed
Joe Medley
Joe Medley

為了防止惡意指令碼濫用彈出式視窗、全螢幕等敏感 API,瀏覽器會透過使用者啟用來控管對這些 API 的存取權。使用者啟用是指瀏覽工作階段與使用者動作相關的狀態:如果狀態為「啟用」,通常表示使用者目前正在與網頁互動,或是自網頁載入後已完成互動。使用者手勢是常見的詞彙,但容易造成誤解。舉例來說,使用者滑動或揮動手勢不會啟動網頁,因此從指令碼角度來看,這不是使用者啟動動作。

目前主要瀏覽器在使用者啟用功能控制啟用式 API的方式上,顯示出截然不同的行為。在 Chrome 中,實作方式是以符記為基礎的模型,但這項做法過於複雜,無法在所有啟用閘道 API 中定義一致的行為。舉例來說,Chrome 一直允許透過 postMessage()setTimeout() 呼叫,存取啟用閘道 API 的部分功能;此外,PromisesXHRGamepad 互動等功能不支援使用者啟用。請注意,其中有些是常見且長期存在的錯誤。

在 72 版中,Chrome 會提供使用者啟用功能第 2 版,讓所有需要啟用的 API 都能完成使用者啟用程序。這可解決上述不一致的問題 (以及其他幾個問題,例如 MessageChannels),我們認為這可簡化使用者啟用功能的網頁開發作業。此外,新實作方式還提供參考實作方式,以因應新規格的提案,該規格旨在讓所有瀏覽器在長期內整合在一起。

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

新的 API 會在影格階層中的每個 window 物件上維持兩位元的使用者啟用狀態:一個是歷史使用者啟用狀態的固定位元 (如果影格曾經歷過使用者啟用),另一個是目前狀態的暫時位元 (如果影格在約一秒內曾經歷過使用者啟用)。設定後,黏性位元在影格生命週期中不會重設。每次使用者互動都會設定暫時位元,並在一段到期間隔 (約一秒) 後或透過呼叫啟用消耗型 API (例如 window.open()) 重設。

請注意,不同啟用限制 API 會以不同方式依賴使用者啟用;新 API 不會變更任何這些 API 專屬行為。舉例來說,每個使用者啟動只能允許一個彈出式視窗,因為 window.open() 會像以往一樣消耗使用者啟動,Navigator.prototype.vibrate() 會在畫面 (或任何子畫面) 曾經顯示使用者動作時繼續生效,以此類推。

異動內容

  • 使用者啟用功能第 2 版將使用者啟用功能的可見性概念正式化,無論來源為何,使用者與特定影格互動時,現在都會啟用所有包含的框架 (以及僅限這些框架)。(在 Chrome 72 中,我們已提供暫時性解決方法,可將可見度擴大至所有同源框架。一旦我們找到明確將使用者啟用傳遞至子畫面的方法,就會移除這個解決方法。)
  • 當啟用閘道 API 從已啟用的影格呼叫,但從事件處理常式程式碼外部呼叫時,只要使用者啟用狀態為「已啟用」(例如未過期或未使用),就會運作。在使用者啟用功能 v2 推出前,這項操作會無條件失敗。
  • 在到期時間間隔內,多個未使用的使用者互動會融合為單一啟用事件,對應至最後一次互動。

啟用閘道 API 中的一致性示例

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

連接的 setTimeout() 呼叫

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

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

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

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

在沒有使用者啟用功能 v2 的情況下,第二個事件處理常式在我們測試的所有瀏覽器中都會失敗。(在某些情況下,第一個嘗試也會失敗)。

跨網域 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);
});

如果沒有使用者啟用功能 v2,父項框架在收到第二個訊息時,就無法開啟彈出式視窗。如果第一則訊息「連結」至另一個跨來源框架 (也就是第一個接收器將訊息轉寄給其他接收器),則第一則訊息也會失敗。

這項功能適用於 User Activation v2,無論是原始表單還是鏈結都適用。