Cookie的SameSite属性

背景

前几天在业务开发中,在iframe中嵌入打开一个xxx的url链接,在链接的主页中,会跳转到另一个登录的页面,然而登录一直失败,失败原因是xxx的服务端没有收到对应的cookie。但是在浏览器中的顶层搜索打开xxx的url链接,在跳转到另一个登录的页面后,就可以正常的登录。

页面嵌套关系如下所示:

image.png

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
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("服务器启动成功");
});
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>
<body>
    <div>i am B页面</div>
</body>

在浏览器顶部导航栏输入b.test.com:3002时,正常的B页面展示为

image.png

在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("服务器启动成功");
});
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>
<body>
    <div>i am A页面</div>
    <iframe src="http://b.test.com:3002"></iframe>//iframe嵌套B页面的url
</body>

在chrome中打开 a.cross.com:3001,页面展示如下:

image.png

可以看到在A页面的iframe中嵌套B页面时,在B服务中没有获取到cookie信息,所以没有展示正常的B页面。

排查问题

那接下来就一块来分析问题吧。

既然在浏览器顶部导航栏输入b.test.com:3002时可以正常显示,那看一下此时的网络请求情况:

image.png

看一下访问失败时的请求头情况

image.png

可以看到请求异常的情况下,请求头中没有携带cookie信息,并且在响应头中会提示SameSite=Lax信息。

查看b站点的Application中的Cookie信息

image.png

可以看到本地有b站点的cookie信息。为什么本地有cookie信息,但是请求的时候request header中没有携带此cookie信息呢?

查找资料得知,从 Chrome 80 开始,如果不指定 SameSite 就等效于设置为 Lax

SameSite属性

SameSite 是 HTTP 响应头 Set-Cookie的属性之一。它允许声明该 Cookie 是否仅限于第一方或者同一站点上下文。

SameSite 可以有下面三种值:

  1. Strict 仅允许一方请求携带 Cookie,即浏览器将只发送相同站点请求的 Cookie,即当前网页 URL 与请求目标 URL 完全一致。
  2. Lax 允许部分第三方请求携带 Cookie。
  3. 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.comwww.baidu.com是跨站,a.baidu.comb.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种解决此类问题的方法,具体还需要结合自己的业务场景选择合适的解决方案。