使用存储访问 API

可以使用 存储访问 API 使嵌入式跨站点文档验证它们是否可以访问 第三方 Cookie未分区状态,如果没有,则请求访问。我们将简要介绍一个常见的存储访问场景。

注意:当我们在存储访问 API 内容中谈论第三方 Cookie 时,我们隐含地指的是 未分区 第三方 Cookie。

使用说明

存储访问 API 旨在允许嵌入式内容请求访问第三方 Cookie 和未分区状态 - 大多数现代浏览器默认情况下会阻止此类访问,以保护用户隐私。由于嵌入式内容无法知道浏览器在这方面的行为将是什么,因此在尝试读取或写入 Cookie 之前始终检查嵌入式 <iframe> 是否具有存储访问权限是最好的做法。这对于 Document.cookie 访问尤其重要,因为当阻止第三方 Cookie 访问时,浏览器通常会返回一个空的 Cookie 罐。

在下面的示例中,我们展示了嵌入式跨站点 <iframe> 如何在浏览器存储访问策略下访问第三方 Cookie 和未分区状态,该策略否则将阻止访问它们。

允许沙盒化的 <iframe> 使用 API

首先,如果 <iframe> 已被沙盒化,则嵌入网站需要添加 allow-storage-access-by-user-activation 沙盒令牌 以允许存储访问 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 的 相关网站集 功能可以被认为是一种与存储访问 API 一起使用的渐进增强机制 - 支持的浏览器会默认授予同一集中网站之间的第三方 Cookie 和未分区状态访问权限。这意味着不必经历上面描述的典型用户权限提示工作流程,这意味着对于集中网站的用户来说,体验更加友好。

代表嵌入式资源从顶层网站请求存储访问权限

上面的存储访问 API 功能允许嵌入式文档请求其自身的第三方 Cookie 访问权限。还有一个额外的实验性方法可用,即 Document.requestStorageAccessFor(),这是对存储访问 API 的一项提议扩展,它允许顶层网站代表特定相关来源请求存储访问权限。

requestStorageAccessFor() 方法解决了在使用跨站点图像或需要 Cookie 的脚本的顶层网站上采用存储访问 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

当查询代表其他来源进行的存储访问请求的权限状态时,使用的权限名称与存储访问 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
    });
}