草庐IT

聊聊Cookie的SameSite属性

QiShare 2023-10-14 原文

背景

前几天在业务开发中,在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

开启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>

在浏览器顶部导航栏输入http://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("服务器启动成功");
});

serverA中index.html的body为:

<body>
    <div>i am A页面</div>
    <iframe src="http://b.test.com:3002"></iframe>//iframe嵌套B页面的url
</body>

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

image.png

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

排查问题

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

既然在浏览器顶部导航栏输入http://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 的协议/主机名/端口一致。例如,https://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.iob.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种解决此类问题的方法,具体还需要结合自己的业务场景选择合适的解决方案。

参考:

https://github.com/mqyqingfeng/Blog/issues/157

https://zhuanlan.zhihu.com/p/266282015

https://juejin.cn/post/6963632513914765320#heading-6

有关聊聊Cookie的SameSite属性的更多相关文章

  1. ruby-on-rails - 如果为空或不验证数值,则使属性默认为 0 - 2

    我希望我的UserPrice模型的属性在它们为空或不验证数值时默认为0。这些属性是tax_rate、shipping_cost和price。classCreateUserPrices8,:scale=>2t.decimal:tax_rate,:precision=>8,:scale=>2t.decimal:shipping_cost,:precision=>8,:scale=>2endendend起初,我将所有3列的:default=>0放在表格中,但我不想要这样,因为它已经填充了字段,我想使用占位符。这是我的UserPrice模型:classUserPrice回答before_val

  2. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  3. ruby - 多个属性的 update_column 方法 - 2

    我有一个具有一些属性的模型:attr1、attr2和attr3。我需要在不执行回调和验证的情况下更新此属性。我找到了update_column方法,但我想同时更新三个属性。我需要这样的东西:update_columns({attr1:val1,attr2:val2,attr3:val3})代替update_column(attr1,val1)update_column(attr2,val2)update_column(attr3,val3) 最佳答案 您可以使用update_columns(attr1:val1,attr2:val2

  4. ruby - Nokogiri 剥离所有属性 - 2

    我有这个html标记:我想得到这个:我如何使用Nokogiri做到这一点? 最佳答案 require'nokogiri'doc=Nokogiri::HTML('')您可以通过xpath删除所有属性:doc.xpath('//@*').remove或者,如果您需要做一些更复杂的事情,有时使用以下方法遍历所有元素会更容易:doc.traversedo|node|node.keys.eachdo|attribute|node.deleteattributeendend 关于ruby-Nokog

  5. ruby-on-rails - Rails 模型——非持久类成员或属性? - 2

    对于Rails模型,是否可以/建议让一个类的成员不持久保存到数据库中?我想将用户最后选择的类型存储在session变量中。由于我无法从我的模型中设置session变量,我想将值存储在一个“虚拟”类成员中,该成员只是将值传递回Controller。你能有这样的类(class)成员吗? 最佳答案 将非持久属性添加到Rails模型就像任何其他Ruby类一样:classUser扩展解释:在Ruby中,所有实例变量都是私有(private)的,不需要在赋值前定义。attr_accessor创建一个setter和getter方法:classUs

  6. ruby - Chef Ruby 遍历 .erb 模板文件中的属性 - 2

    所以这可能有点令人困惑,但请耐心等待。简而言之,我想遍历具有特定键值的所有属性,然后如果值不为空,则将它们插入到模板中。这是我的代码:属性:#===DefaultfileConfigurations#default['elasticsearch']['default']['ES_USER']=''default['elasticsearch']['default']['ES_GROUP']=''default['elasticsearch']['default']['ES_HEAP_SIZE']=''default['elasticsearch']['default']['MAX_OP

  7. ruby - 获取数组中的值并最小化某个类属性的最优雅的方法是什么? - 2

    假设我有以下类(class):classPersondefinitialize(name,age)@name=name@age=ageenddefget_agereturn@ageendend我有一组Person对象。是否有一种简洁的、类似于Ruby的方法来获取最小(或最大)年龄的人?如何根据它对它们进行排序? 最佳答案 这样做会:people_array.min_by(&:get_age)people_array.max_by(&:get_age)people_array.sort_by(&:get_age)

  8. ruby-on-rails - Rails Cookie 问题 - 2

    我在ruby​​onrails应用程序中有以下新方法:defnewifcookies[:owner].empty?cookies[:owner]=SecureRandom.hexend@movie=Movie.new@movie.owner=cookies[:owner]end基本上,每个新用户都应该获得一个代码来识别他们(尽管只是通过cookie)。因此,当用户创建电影时,创建的cookie将存储在owner字段中。所以有两个问题:使用.empty?方法,当我从浏览器中删除cookie时,返回一个undefinedmethodempty?'对于nil:NilClass`当我确实已经在

  9. ruby-on-rails - 为模型创建状态属性 - 2

    我想为我的Task模型创建一个status属性,该属性将按以下顺序指示它在三部分进度中的位置:打开=>进行中=>完成。它的工作方式类似于亚马逊包裹的交付方式:已订购=>已发货=>已交付。我想知道设置此属性的最佳方法是什么。我可能是错的,但创建三个独立的bool属性似乎有点多余。实现此目标的最佳方法是什么? 最佳答案 Rails4有一个内置的enummacro.它使用单个整数列并映射到键列表。classOrderenumstatus:[:ordered,:shipped,:delivered]end状态映射如下:{ordered:0,

  10. ruby - Chef LW 资源属性默认值如何引用另一个属性? - 2

    我正在尝试将一个资源属性的默认值设置为另一个属性的值。我正在为我正在构建的tomcat说明书定义一个资源,其中包含以下定义。我想要可以独立设置的“名称”和“服务名称”属性。当未设置服务名称时,我希望它默认为为“名称”提供的任何内容。以下不符合我的预期:attribute:name,:kind_of=>String,:required=>true,:name_attribute=>trueattribute:service_name,:kind_of=>String,:default=>:name注意第二行末尾的“:default=>:name”。当我在Recipe的新block中引用我

随机推荐