跨域嵌入:让第三方 Cookie 再次工作






4.27/5 (3投票s)
使用 Node.js Web 主机示例,再次启用第三方 Cookie 的基础知识
引言
早在 2020 年 2 月,谷歌开始推出他们对第三方 Cookie 处理方式的更改。此举旨在帮助阻止嵌入式跨域网站(通常是社交媒体网站)在用户不知情的情况下跟踪其在网络上的活动。主要进行了两项更改:
- Cookie 的
SameSite
值现在默认为Lax
而不是None
。 - 如果明确设置了
None
值,则必须同时设置Secure
。
这导致大多数跨域嵌入式网站无法再使用 Cookie,即使是那些无恶意网站,因为浏览器开始阻止它们。
好消息是,仍然可以在 iframe 中使用嵌入式跨域网站的第三方 Cookie。坏消息是,现在更困难了,而且 Safari/iOS 还需要使用实验性 API 来实现这一点。
本文将通过一个 Node.js Web 主机示例,介绍实现此场景的基础知识。不过,技术栈并不重要——任何 Web 主机和语言都可以实现相同的功能。
旧方法
我们要做的第一件事是创建一个基本示例,该示例包含两个不同域上的网站,其中一个嵌入在另一个的 iframe 中。为此,我们将利用 localhost
被视为与 127.0.0.1
不同的域这一事实,这意味着我们可以在本地机器上进行所有这些测试。万岁。
首先,如果您的机器上还没有安装 Node.js 和某种 IDE,请安装它们。除了 Node.js,我们还需要 express,这是一个基于 Node.js 的快速简便的 Web 主机包。在您的工作项目目录中运行 npm install express
即可安装。
首先,我们将创建 app.js,它是我们两个网站的入口点
'use strict'
// Define the basic imports and constants.
const express = require('express');
const app = express();
const embeddedApp = express();
const port = 3000;
const embeddedPort = 3001;
// Setup the outside app with the www folder as static content.
app.use(express.static('www'));
// Create the outside app and run it.
app.listen(port, () => {
console.log(`Open browser to https://:${port}/ to begin.`);
});
// Create the embedded app with the www2 folder as static content and
// set the cookie from the embedded app in the headers on all requests.
embeddedApp.use(express.static('www2', {
setHeaders: function (res, path, stat) {
res.set('Set-Cookie', "embeddedCookie=Hello from an
embedded third party cookie!;Path=/");
}
}));
// Create the server and start it.
embeddedApp.listen(embeddedPort, () => {
console.log(`Embedded server now running on ${embeddedPort}...`)
});
此外,我们将在同一目录下创建两个文件夹,一个名为 www 用于顶级应用程序,另一个名为 www2 用于嵌入式应用程序。在两个目录中,我们将创建 index.html,如下所示:
www/index.html
<head>
<title>Hello World top URL with embedded iframe</title>
<link rel="stylesheet" href="content/basic.css">
</head>
<body>
<h1>Cross domain iframe cookie example</h1>
<iframe src="http://127.0.0.1:3001/" width="100%" height="75%"></iframe>
</body>
www2/index.html
<head>
<title>Hello World from third party embedded URL</title>
<link rel="stylesheet" href="content/basic.css">
<script type="text/javascript" src="scripts/index.js"></script>
</head>
<body>
<h2>I am cross-domain embedded content in an iframe</h2>
<div id="cookieValue">Cookie cannot be found,
it's being rejected by the browser...</div>
</body>
最后,我们将创建一些 JavaScript 来尝试获取 Cookie,在本例中,将其放入 www2/scripts/index.js
// Helper function to get a cookie.
// From https://stackoverflow.com/questions/10730362/get-cookie-by-name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
document.addEventListener('DOMContentLoaded', event => {
// Always try to get the cookie.
const cookieValue = getCookie('embeddedCookie');
if (cookieValue) {
document.getElementById('cookieValue').innerText = cookieValue;
}
});
最后,我们准备运行它。可以使用命令行运行:node app.js。
运行后,我们可以浏览到 https://:3000/,这将显示:
请注意,文本显示找不到 Cookie。这意味着我们的 JavaScript 无法找到 Cookie。
这是因为我们正在发送“旧式”Cookie,没有 SameSite
和 Secure
值。如果打开开发者工具并检查文档请求,就可以看到这一点。
Cookie 被浏览器以黄色突出显示,因为它正在被主动拒绝。那么我们如何解决这个问题呢?
新方法——SameSite、Secure 和 HTTPS
为了使其工作,我们必须修改我们发送的 Cookie,使其包含 SameSite=None
以避免新的默认值 Lax
。
res.set('Set-Cookie', "embeddedCookie=Hello from an embedded third party cookie!;
Path=/;SameSite=None");
但这还不够,如果您这样加载页面,您会看到同样的问题——开发者工具会显示 SameSite=None
,但仍然拒绝它。
这是因为我们还需要根据谷歌的第二次更改设置 Secure
值。Secure
值表示 Cookie 只能通过安全的 HTTPS 连接接受。
为了使其工作,我们必须将 Web 应用程序迁移到 HTTPS。要在本地使用 HTTPS,我们可以使用自签名证书(毕竟这只是用于开发)。为此,我遵循了这份指南,它非常有帮助。
第一步是生成自签名 SSL 密钥,这可以使用 openssl
命令完成。
openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365
openssl rsa -in keytmp.pem -out key.pem
为了简化此操作,我已在项目中包含了已生成的自签名密钥,有效期为一年。
现在我们可以修改 Cookie,使其包含 Secure
和 SameSite=None
。
res.set('Set-Cookie', "embeddedCookie=Hello from an embedded third party cookie!;
Path=/;Secure;SameSite=None");
我们还需要修改 app.js 文件以支持 HTTPS 而不是 HTTP。修改后的完整文件如下所示:
'use strict'
// Define the basic imports and constants.
const fs = require('fs');
const https = require('https');
const express = require('express');
const app = express();
const embeddedApp = express();
const port = 3000;
const embeddedPort = 3001;
// Get the keys and certs for HTTPS.
const key = fs.readFileSync('./ssl/www-key.pem');
const cert = fs.readFileSync('./ssl/www-cert.pem');
const embeddedKey = fs.readFileSync('./ssl/www2-key.pem');
const embeddedCert = fs.readFileSync('./ssl/www2-cert.pem');
// Setup the outside app with the www folder as static content.
app.use(express.static('www'));
// Create the outside app with the first key / cert and run it.
const server = https.createServer({ key: key, cert: cert }, app);
server.listen(port, () => {
console.log(`Open browser to https://:${port}/ to begin.`);
});
// Create the embedded app with the www2 folder as static content and
// set the cookie from the embedded app in the headers on all requests.
embeddedApp.use(express.static('www2', {
setHeaders: function (res, path, stat) {
res.set('Set-Cookie', "embeddedCookie=Hello from an embedded third party cookie!;
Path=/;Secure;SameSite=None");
}
}));
// Create the server and start it.
const embeddedServer = https.createServer({ key: embeddedKey, cert: embeddedCert },
embeddedApp);
embeddedServer.listen(embeddedPort, () => {
console.log(`Embedded server now running on ${embeddedPort}...`)
});
我们还需要更新 www/index.html 中 iframe 的 URL。
<iframe src="https://127.0.0.1:3001/" width="100%" height="75%"></iframe>
运行此代码将创建两个 HTTPS 网站,而不是 HTTP 网站。
但是,如果您浏览到外部网站 https://:3000/,它将显示一个带有文本 NET::ERR_CERT_INVALID
的可怕红色错误,因为证书不受信任。
要绕过此问题,内部和外部网站(https://:3000 和 https://127.0.0.1:3001)都必须在顶级(在新标签页中)打开,并且必须输入“thisisunsafe
”。输入这个神奇的短语后,网站将显示:
它成功了!我们现在可以从不同域上的嵌入式网站向客户端发送 Cookie。
但苹果仍然存在问题……
macOS 和 iOS 上的 Safari
如果您在 macOS 或 iOS 上的 Safari 中打开完全相同的网站,您会看到以下内容:
没错,它又不能工作了。这是因为 Safari 根本不接受第三方 Cookie,即使在 Cookie 上设置了新的 SameSite
和 Secure
值。更令人沮丧的是,如果您打开开发工具并检查响应,列表中没有 Cookie(也没有拒绝原因)。
幸运的是,这也可以通过 Safari 的实验性存储访问 API 解决。该过程在此 webkit 文章中概述,可以概括如下:
- 在嵌入式站点中,使用实验性
document.hasStorageAccess()
来确定是否可以访问 Cookie。 - 如果无法访问,请提供一个按钮,按下该按钮将调用
document.requestStorageAccess()
。此方法仅适用于 UI 事件(并将消耗它)。 - 如果请求失败,则用户要么拒绝了请求,要么从未将嵌入式网站作为第一方网站打开过(我们必须帮助他们这样做)。
为了实现这一点,我们首先将一个按钮添加到 www2/index.html 中:
<button id="requestStorageAccessButton">Click to request storage access</button>
然后我们将修改 www2/index.js 中的 JavaScript,通过添加以下代码来使用新的实验性 API:
// Check for iOS / Safari.
if (!!document.hasStorageAccess) {
document.hasStorageAccess().then(result => {
// If we don't have access we must request it, but the request
// must come from a UI event.
if (!result) {
// Show the button and tie to the click.
const requestStorageAccessButton =
document.getElementById('requestStorageAccessButton');
requestStorageAccessButton.style.display = "block";
requestStorageAccessButton.addEventListener("click", event => {
// On UI event, consume the event by requesting access.
document.requestStorageAccess().then(result => {
// Finally, we are allowed! Reload to get the cookie.
window.location.reload();
}).catch(err => {
// If we get here, it means either our page
// was never loaded as a first party page,
// or the user clicked 'Don't Allow'.
// Either way open that now so the user can request
// from there (or learn more about us).
window.top.location = window.location.href +
"requeststorageaccess.html";
});
});
}
}).catch(err => console.error(err));
该过程很简单:首先检查实验性 API 是否存在(在 Safari 之外不存在),如果存在则检查访问权限,如果不存在访问权限,则当用户单击按钮时通过打开一个名为 requeststorageaccess.html 的新页面来请求它。
最后一步是创建这个新页面,requeststorageaccess.html。
<head>
<title>Request Storage Access</title>
<link rel="stylesheet" href="content/basic.css">
<script type="text/javascript" src="scripts/requeststorageaccess.js"></script>
</head>
<body>
<h2>Hi there. This is my brand. Learn about it, then click the button.</h2>
<button id="theButton">Click to return</button>
</body>
以及 requeststorageaccess.js 中用于处理按钮点击的 JavaScript:
document.addEventListener('DOMContentLoaded', event => {
document.getElementById('theButton').addEventListener("click", event => {
// Just go back to the outside iframe we came from.
window.history.back();
});
});
我们在这里回溯历史,因为我们知道我们会从主页重定向。现在,如果我们重新启动 node 并在 Safari 中渲染它,我们将看到以下内容:
点击“请求”按钮将把用户转发到我们在嵌入域上创建的新页面。
请注意,URL 是我们的嵌入式 URL,https://127.0.0.1:3001/。这很重要,用户通常会想知道该 URL 上存在哪些公司。一旦他们知道,他们(理想情况下)会点击按钮返回他们来的地方,这将再次显示与上面相同的页面。
返回后,用户必须再次点击“请求”按钮,此时他们将最终收到 Safari 的提示。
点击“不允许”将拒绝 requestStorageAccess()
的 Promise
,最终导致我们回到打开页面(因为我们不知道为什么被拒绝),而点击“允许”将执行我们的代码以重新加载页面并最终正常工作。
Cookie 最终也会在 Safari 的开发工具中显示。
作为开发人员须知,一旦您接受浏览器提示“允许”,撤消它的唯一方法是从 Safari->偏好设置…->隐私->管理网站数据…中清除网站数据。
如果您想重试此过程,请删除嵌入式 URL 的数据并重新开始该过程。
结论
尽管可能需要大量工作,但仍可以在 iframe 中嵌入的跨域网站中实现第三方 Cookie 的工作。
即使有 Safari 的新限制,仍然可以通过其新的实验性 API 来实现。
运行完整项目的完整源代码已附在此文章中,也可在我的 github 存储库中找到。源代码包括 packages.json,这意味着您可以在目录中运行 npm install
以获取所需的包,然后运行 node app.js
以运行应用程序。
历史
- 2022 年 4 月 21 日:初始版本