使用 Storage Access API

Storage Access API 可由嵌入式跨站文件用於驗證其是否具有訪問 第三方 Cookie未分割槽狀態 的許可權,如果沒有,則可用於請求訪問。我們將簡要介紹一個常見的儲存訪問場景。

注意:當我們在 Storage Access API 的內容中談論第三方 Cookie 時,我們隱式地指的是 未分割槽 的第三方 Cookie。

用法說明

Storage Access API 的設計目的是允許嵌入式內容請求訪問第三方 Cookie 和未分割槽狀態——大多數現代瀏覽器預設會阻止此類訪問以保護使用者隱私。由於嵌入式內容不知道瀏覽器在這方面的行為將如何,因此在嘗試讀取或寫入 Cookie 之前,最好始終檢查嵌入的 <iframe> 是否具有儲存訪問許可權。這對於 Document.cookie 訪問尤其如此,因為當第三方 Cookie 訪問被阻止時,瀏覽器通常會返回一個空的 Cookie 罐。

在下面的示例中,我們展示了嵌入式跨站 <iframe> 如何在瀏覽器儲存訪問策略下訪問第三方 Cookie 和未分割槽狀態,而該策略否則會阻止對其的訪問。

允許沙盒化的 <iframe> 使用該 API

首先,如果 <iframe> 被沙盒化,嵌入網站需要新增 allow-storage-access-by-user-activation 沙盒令牌,以便 Storage Access API 請求能夠成功,同時還需要 allow-scriptsallow-same-origin 令牌,以便它能夠執行指令碼來呼叫 API 並在具有 Cookie 和狀態的源中執行它。

html
<iframe
  sandbox="allow-storage-access-by-user-activation
                 allow-scripts
                 allow-same-origin">
  …
</iframe>

檢查和請求儲存訪問

現在來看在嵌入式文件內部執行的程式碼。在這段程式碼中

  1. 我們首先使用功能檢測(if (document.hasStorageAccess) {})來檢查 API 是否受支援。如果不支援,我們就執行訪問 Cookie 的程式碼,並希望它能正常工作。無論如何,它都應該以防禦性方式編碼來應對這種情況。
  2. 如果 API 受支援,我們呼叫 document.hasStorageAccess()
  3. 如果該呼叫返回 true,則表示此 <iframe> 已獲得訪問許可權,我們可以立即執行訪問 Cookie 和狀態的程式碼。
  4. 如果該呼叫返回 false,我們則呼叫 Permissions.query() 來檢查是否已授予訪問第三方 Cookie 和未分割槽狀態的許可權(即,是否授予了給另一個同站嵌入)。我們將整個部分包裝在一個 try...catch 塊中,因為 某些瀏覽器不支援 "storage-access" 許可權,這可能導致 query() 呼叫丟擲錯誤。如果丟擲錯誤,我們會將其報告給控制檯,然後嘗試執行 Cookie 程式碼。
  5. 如果許可權狀態為 "granted",我們立即呼叫 document.requestStorageAccess()。此呼叫將自動解析,為使用者節省一些時間,然後我們可以執行訪問 Cookie 和狀態的程式碼。
  6. 如果許可權狀態為 "prompt",我們在使用者互動後呼叫 document.requestStorageAccess()。此呼叫可能會觸發使用者提示。如果此呼叫解析,那麼我們就可以執行訪問 Cookie 和狀態的程式碼。
  7. 如果許可權狀態為 "denied",則表示使用者已拒絕我們訪問第三方 Cookie 或未分割槽狀態的請求,我們的程式碼將無法使用它們。
js
function doThingsWithCookies() {
  document.cookie = "foo=bar"; // set a cookie
}

function doThingsWithLocalStorage(handle) {
  handle.localStorage.setItem("foo", "bar"); // set a local storage key
}

async function handleCookieAccess() {
  if (!document.hasStorageAccess) {
    // This browser doesn't support the Storage Access API
    // so let's just hope we have access!
    doThingsWithCookies();
  } else {
    const hasAccess = await document.hasStorageAccess();
    if (hasAccess) {
      // We have access to third-party cookies, so let's go
      doThingsWithCookies();
      // If we want to modify unpartitioned state, we need to request a handle.
      const handle = await document.requestStorageAccess({
        localStorage: true,
      });
      doThingsWithLocalStorage(handle);
    } else {
      // Check whether third-party cookie access has been granted
      // to another same-site embed
      try {
        const permission = await navigator.permissions.query({
          name: "storage-access",
        });

        if (permission.state === "granted") {
          // If so, you can just call requestStorageAccess() without a user interaction,
          // and it will resolve automatically.
          const handle = await document.requestStorageAccess({
            cookies: true,
            localStorage: true,
          });
          doThingsWithLocalStorage(handle);
          doThingsWithCookies();
        } else if (permission.state === "prompt") {
          // Need to call requestStorageAccess() after a user interaction
          btn.addEventListener("click", async () => {
            try {
              const handle = await document.requestStorageAccess({
                cookies: true,
                localStorage: true,
              });
              doThingsWithLocalStorage(handle);
              doThingsWithCookies();
            } catch (err) {
              // If there is an error obtaining storage access.
              console.error(`Error obtaining storage access: ${err}.
                            Please sign in.`);
            }
          });
        } else if (permission.state === "denied") {
          // User has denied third-party cookie access, so we'll
          // need to do something else
        }
      } catch (error) {
        console.log(`Could not access permission state. Error: ${error}`);
        doThingsWithCookies(); // Again, we'll have to hope we have access!
      }
    }
  }
}

注意:除非嵌入式內容當前正在處理使用者手勢(例如點選),否則 requestStorageAccess() 請求將自動被拒絕(瞬時啟用),或者如果之前已授予許可權。如果之前未授予許可權,則必須在基於使用者手勢的事件處理程式中執行 requestStorageAccess() 請求,如上所示。

僅限 Chrome 的 相關網站集 功能可以被視為一種漸進增強機制,它與 Storage Access API 一起工作——支援的瀏覽器將自動授予同一集內網站之間的第三方 Cookie 和未分割槽狀態訪問許可權。這意味著無需經過上面描述的 usual 使用者許可權提示流程,對該集內網站的使用者來說,可以獲得更友好的體驗。

代表嵌入式資源向頂級站點請求儲存訪問

上述 Storage Access API 功能允許嵌入式文件請求其自身的第三方 Cookie 訪問許可權。還有一個額外的實驗性方法可用,Document.requestStorageAccessFor(),這是 Storage Access API 的一項提議擴充套件,它允許頂級站點代表特定的相關源請求儲存訪問。

requestStorageAccessFor() 方法解決了在那些使用需要 Cookie 的跨站圖片或指令碼的頂級站點上採用 Storage Access API 所面臨的挑戰。它可以為直接嵌入到頂級站點中的、無法自行請求儲存訪問的跨站資源啟用第三方 Cookie 訪問,例如透過 <img><script> 元素。

要使 requestStorageAccessFor() 生效,呼叫它的頂級頁面和它請求儲存訪問的嵌入式資源都需要是同一 相關網站集 的一部分。

requestStorageAccessFor() 的典型用法如下(這次使用常規的 Promise 風格而不是 async/await)

js
navigator.permissions
  .query({
    name: "top-level-storage-access",
    requestedOrigin: "https://example.com",
  })
  .then((permission) => {
    if (permission.state === "granted") {
      // Permission has already been granted
      // No need to call requestStorageAccessFor() again, just start using cookies
      doThingsWithCookies();
    } else if (permission.state === "prompt") {
      // Need to call requestStorageAccessFor() after a user interaction
      btn.addEventListener("click", () => {
        // Request storage access
        rSAFor();
      });
    } else if (permission.state === "denied") {
      // User has denied third-party cookie access, so we'll
      // need to do something else
    }
  });

function rSAFor() {
  if ("requestStorageAccessFor" in document) {
    document.requestStorageAccessFor("https://example.com").then(
      (res) => {
        doThingsWithCookies();
      },
      (err) => {
        // Handle errors
      },
    );
  }
}

注意:requestStorageAccess() 不同,當呼叫 requestStorageAccessFor() 時,Chrome 不會檢查最近 30 天內頂級文件的互動情況,因為使用者已經在頁面上了。有關此行為的更多詳細資訊,請參閱 瀏覽器特定差異 > Chrome

當查詢代表其他源發出的儲存訪問請求的許可權狀態時,使用的許可權名稱與 Storage Access API 的其他部分不同:是 "top-level-storage-access" 而不是 "storage-access"。在上面的程式碼中,我們使用了以下呼叫

js
navigator.permissions.query({
  name: "top-level-storage-access",
  requestedOrigin: "https://example.com",
});

來發現該源是否已預先獲得許可權,或者是否仍需要請求 Cookie 訪問。

  • 如果許可權狀態為 "granted",我們就可以開始使用 Cookie 了;requestStorageAccessFor() 已經被呼叫,所以不需要再次呼叫它。
  • 如果許可權狀態為 "prompt",我們需要在使用者手勢(例如按鈕點選)內呼叫 document.requestStorageAccessFor("https://example.com")

"top-level-storage-access" 許可權被授予後,跨站請求將包含 Cookie(如果它們包含 CORS / crossorigin),因此站點可能希望在觸發請求之前等待。此類請求必須使用 credentials: "include" 選項,並且資源必須包含 crossorigin="use-credentials" 屬性。

例如

js
function checkCookie() {
  fetch("https://example.com/getcookies.json", {
    method: "GET",
    credentials: "include",
  })
    .then((response) => response.json())
    .then((json) => {
      // Do something
    });
}