背景
前几天在业务开发中,在iframe中嵌入打开一个xxx的url链接,在链接的主页中,会跳转到另一个登录的页面,然而登录一直失败,失败原因是xxx的服务端没有收到对应的cookie。但是在浏览器中的顶层搜索打开xxx的url链接,在跳转到另一个登录的页面后,就可以正常的登录。
页面嵌套关系如下所示:
Cookie简介:
HTTP 协议是无状态的,但可以通过 Cookie 来维持客户端与服务端之间的“会话状态”。
简单来说就是:服务端通过 Set-Cookie 响应头设置 Cookie 到客户端,而客户端在下次向服务器发送请求时添加名为 Cookie 的请求头,以携带服务端之前“埋下”的内容,从而使得服务端可以识别客户端的身份。
场景模拟
本地代码示例如下:
配置本地host
127.0.0.1 a.cross.com
127.0.0.1 b.test.com
127.0.0.1 a.test.com
开启serverB:
const http = require("http");
const fs = require("fs");
let server = http.createServer((req, res) => {
const cookie = req.headers.cookie
console.log('cookie', cookie);
res.writeHead(200, [
["Set-Cookie", "name=bbb"], // 设置 cookie
]);
if (!cookie) {
res.end("no cookie");//没有cookie时
return
}
if ("/" == req.url) {
fs.readFile(__dirname + "/index.html", "utf-8", (err, data) => {
if (err) {
throw err;
} else {
res.end(data);
}
});
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.end("404 NOT Found");
}
});
server.listen(3002, () => {
console.log("服务器启动成功");
});
serverB中index.html的body为:
<body>
<div>i am B页面</div>
</body>
在浏览器顶部导航栏输入b.test.com:3002时,正常的B页面展示为
在A中使用iframe嵌套B的url,开启serverA:
const http = require("http");
const fs = require("fs");
let server = http.createServer((req, res) => {
console.log(req.url);
if ("/" == req.url) {
fs.readFile(__dirname + "/index.html", "utf-8", (err, data) => {
if (err) {
throw err;
} else {
res.end(data);
}
});
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.end("404 NOT Found");
}
});
server.listen(3001, () => {
console.log("服务器启动成功");
});
serverA中index.html的body为:
<body>
<div>i am A页面</div>
<iframe src="http://b.test.com:3002"></iframe>//iframe嵌套B页面的url
</body>
在chrome中打开a.cross.com:3001,页面展示如下:
可以看到在A页面的iframe中嵌套B页面时,在B服务中没有获取到cookie信息,所以没有展示正常的B页面。
排查问题
那接下来就一块来分析问题吧。
既然在浏览器顶部导航栏输入b.test.com:3002时可以正常显示,那看一下此时的网络请求情况:
看一下访问失败时的请求头情况
可以看到请求异常的情况下,请求头中没有携带cookie信息,并且在响应头中会提示SameSite=Lax信息。
查看b站点的Application中的Cookie信息
可以看到本地有b站点的cookie信息。为什么本地有cookie信息,但是请求的时候request header中没有携带此cookie信息呢?
查找资料得知,从 Chrome 80 开始,如果不指定 SameSite 就等效于设置为 Lax
。
SameSite属性
SameSite
是 HTTP 响应头 Set-Cookie的属性之一。它允许声明该 Cookie 是否仅限于第一方或者同一站点上下文。
SameSite 可以有下面三种值:
- Strict 仅允许一方请求携带 Cookie,即浏览器将只发送相同站点请求的 Cookie,即当前网页 URL 与请求目标 URL 完全一致。
- Lax 允许部分第三方请求携带 Cookie。
- None 无论是否跨站都会发送 Cookie。
之前默认是 None 的,Chrome80 后默认是 Lax。
Lax的情况见下表:
请求类型 | 示例 | 正常情况 | Lax |
---|---|---|---|
链接 | <a href="..."></a> | 发送 Cookie | 发送 Cookie |
预加载 | <link rel="prerender" href="..."/> | 发送 Cookie | 发送 Cookie |
GET 表单 | <form method="GET" action="..."> | 发送 Cookie | 发送 Cookie |
POST 表单 | <form method="POST" action="..."> | 发送 Cookie | 不发送 |
iframe | <iframe src="..."></iframe> | 发送 Cookie | 不发送 |
AJAX | $.get("...") | 发送 Cookie | 不发送 |
Image | <img src="..."> | 发送 Cookie | 不发送 |
当sameSite为Lax时,post、iframe、ajax、image的跨站请求都不会发送cookie。
要理解上面的规则,还需要了解一下跨域和跨站的区别。
跨域和跨站
首先要理解的一点就是跨站和跨域是不同的。同站(same-site)/跨站(cross-site)和第一方(first-party)/第三方(third-party)是等价的。但是与浏览器同源策略(SOP)中的同源(same-origin)/跨域(cross-origin)是完全不同的概念。
同源策略的同源是指两个 URL 的协议/主机名/端口一致。例如,www.baidu.com,它的协议是 https,主机名是 www.baidu.com,端口是 443。
同源策略作为浏览器的安全基石,其同源判断是比较严格的。相对而言,Cookie中的同站判断就比较宽松:只要两个 URL 的 eTLD+1 相同即可,不需要考虑协议和端口。其中,eTLD 表示有效顶级域名,注册于 Mozilla 维护的公共后缀列表(Public Suffix List)中,例如,.com、.co.uk、.github.io 等。eTLD+1 则表示,有效顶级域名+二级域名,例如 baidu.com 等。
举几个例子,www.taobao.com 和 www.baidu.com 是跨站,a.baidu.com 和 b.baidu.com是同站,a.github.io 和 b.github.io 是跨站(注意是跨站)。
在上面的模拟示例中我使用的chrome浏览器的版本是107版本,虽然本地是有cookie信息,但是SameSite为空,也就是没有设置,所以默认SameSite=Lax,导致在A页面访问iframe中的B站点时,是跨站的方式,不会发送B站点的cookie信息。
解决方案
这种问题的解决方案有以下几种
1、服务器在set-cookie时,设置SameSite=None; Secure。但是这里需要注意:
- HTTP 接口不支持 SameSite=none。如果你想加 SameSite=none 属性,那么该 Cookie 就必须同时加上 Secure 属性,表示只有在 HTTPS 协议下该 Cookie 才会被发送。
- 部分浏览器不支持部分SameSite=none。IOS 12 的 Safari 以及老版本的一些 Chrome 会把 SameSite=none 识别成 SameSite=Strict,所以服务端必须在下发 Set-Cookie 响应头时进行 User-Agent 检测,对这些浏览器不下发 SameSite=none 属性。
2、使用Nginx或其他网关工具进行Proxy操作,使跨站请求变为同站请求
将这个被调用接口的应用和发起请求的应用放在同一个站下面,使他们是同站请求,这样就不存在跨站问题了。
比如上面模拟示例所示,在host配置中,将A站点127.0.0.1使用a.test.com映射,这样在a.test.com中访问b.test.com就是同站访问了。
或者使用nginx代理请求,将a.test.com代理到a.cross.com,这样在浏览器中顶部导航栏中输入a.test.com就可以被nginx代理访问到a.cross.com,而这时浏览器会认为在a.test.com页面中访问b.test.com,浏览器会当作同站处理cookie。同理可以使用nginx代理b站点的url,使与A站点同站。
3、使用http auth也就是header auth方式进行,将令牌通过header的形式传输,不使用Cookie,那当然也就不存在Cookie中奇奇怪怪的问题了。
4、使用指定版本的浏览器,使用chrome内核低于80的浏览器,或者在safari中关闭防止跨站追踪选项。
以上列出了4种解决此类问题的方法,具体还需要结合自己的业务场景选择合适的解决方案。
参考: