使用 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
    });
}