⤴Top⤴

Cookie & Session

博客分类: 前端
修改内容:add SameParty & fenced frames

Cookie & Session

Cookie & Session

会话(Session) 是一个客户与服务器之间的不中断的请求响应序列。可简单理解为:用户开一个浏览器,访问某一个 web 站点,在这个站点点击多个超链接,访问服务器多个 web 资源,然后关闭浏览器,整个过程称之为一个会话。

HTTP 会话 由于 HTTP 协议的无状态特征,为了识别不同的请求是否来自同一客户,引用 HTTP 会话机制,即多次 HTTP 连接间维护用户与同一用户发出的不同请求之间关联的情况称为维护一个会话(session)。

cookie 总是保存在客户端中,按在客户端中的存储位置,可分为:

使用 cookie 存在着一些限制,比如:

符合条件的意思是,客户端会检查所有存储的 cookie,如果某个 cookie 所声明的作用范围(域 + 路径)大于等于将要请求的资源所在的位置,则会附加于请求头中。也可以禁用 cookie。

cookie 既可以由服务端来设置,也可以由客户端来设置。

在响应头中可加入 set-cookie 字段设置 cookie:

Set-Cookie: <cookie名>=<cookie值>

HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

直接通过 document.cookie 设置,当 Name / domain / path 相同时则是修改,否则为新建。

document.cookie = "key=name; max-age=100; secure";

cookie.png

默认情况下,在发生 CORS 跨域请求时,cookie 作为一种 credential 信息是不会被传送到服务端的。必须要进行额外设置才可以。详情请查看 CORS 跨域一节

domain / path 作用域

document.cookie; // 这个方法只能获取非 HttpOnly 类型的 cookie
// "_ga=GA1.2.865451383.1517365757; __lnkrntdmcvrd=-1; PHPSESSID=web2~207a6ac37f624ce19d9a4f268dc01304; _gid=GA1.2.1650902441.1519796826; afpCT=1; Hm_lvt_e23800c454aa573c0ccb16b52665ac26=1519705527,1519796826,1519830084,1519866651; Hm_lpvt_e23800c454aa573c0ccb16b52665ac26=1519866651"


// 在设置 cookie 属性时,属性之间由一个分号和一个空格隔开
document.cookie = "key=name; max-age=-1; domain=.example.com; path=/; secure; HttpOnly"

其中 domain 和 path 共同决定了 cookie 的作用域,即限制 cookie 能被哪些 URL 访问。当前大多数浏览器遵循 RFC 6265,设置 domain 时 不需要加前导点。浏览器不遵循该规范,则需要加前导点,例如:domain=.mozilla.org。domain 如果不指定,则默认为 origin,不包含子域名。

path 标识指定了主机下的哪些路径可以接受 cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 path=/docs,则以下地址都会匹配:

/docs
/docs/Web/
/docs/Web/HTTP

SameSite

SameSite Cookie 允许服务器要求某个 cookie 在跨站(cross-site)请求时不会被发送,影响的是第三方 cookie,从而可以阻止跨站请求伪造攻击(CSRF)。SameSite 可以有下面三种值:

导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表:

请求类型 示例 正常情况或 None Lax Strict
链接 <a href=”…”></a> 发送 发送 不发送
预加载 <link rel=”prerender” href=”…”> 发送 发送 不发送
GET 表单 <form method="GET" action="..."> 发送 发送 不发送
POST 表单 <form method="POST" action="..."> 发送 不发送 不发送
iframe <iframe src="..."></iframe> 发送 不发送 不发送
AJAX $.get(“…”) 发送 不发送 不发送
Image <img src=”…”> 发送 不发送 不发送

如果 SameSite 属性在以前没有设置,或者没有得到运行浏览器的支持,那么它的行为等同于 None,Cookies 会被包含在任何请求中,包括跨站请求。大多数主流浏览器正在将 SameSite 的默认值迁移至 Lax。如果想要指定 Cookies 在同站、跨站请求都被发送,现在需要明确指定 SameSite 为 None。

如果要设置 SameSite None 属性,那么该 Cookie 就必须同时加上 Secure 属性,因为 HTTP 不支持 SameSite 为 None 的配置

那么 same site(同站)和 same origin(同域)到底有什么区别呢?我们再看下以下图示:

url

同域的判断比较严格,需要 protocol, hostname, port 三部分完全一致。相对而言,Cookie 中的同站判断就比较宽松,主要是根据 Mozilla 维护的公共后缀表(Pulic Suffix List)使用 有效顶级域名(eTLD) + 1 的规则查找得到的一级域名是否相同来判断是否是同站请求,不需要考虑协议和端口。举些例子:

# These are the same site because the registrable domain of mozilla.org is the same (different host and files path don't matter):

https://developer.mozilla.org/en-US/docs/
https://support.mozilla.org/en-US/

# These are the same site authough scheme and port are not relevant:

http://example.com:8080
https://example.com

# These are not same site because the registrable domain of the two URLs differs:

https://developer.mozilla.org/en-US/docs/
https://example.com

# 需要注意的是,`.github.io` 在 PSL 中记录的是有效顶级域名,所以以下是跨站请求

https://tateyoung.github.io
https://blog.github.io

PSL & eTLD

eTLD 的全称是 effective Top-Level Domain,它与我们往常理解的 Top-Level Domain 顶级域名有所区别。eTLD 记录在之前提到的 PSL(Pulic Suffix List) 文件中。而 TLD 也有一个记录的列表,那就是 Root Zone Database。RZD 中记录了所有的根域列表,其中不乏一些奇奇怪怪五花八门的后缀。

eTLD 的出现主要是为了解决 .com.cn, .com.hk, .co.jp 这种看起来像是一级域名的但其实需要作为顶级域名存在的场景。这里还可以分享一个有趣的事情,2020年5月份出现了一起阿里云所有 ac.cn 后缀网站解析全部挂掉的事件。原因就是 ac.cn 是中科院申请在册的 eTLD 域名。而阿里云的检测域名备案的脚本不了解规范,没有使用 PSL 列表去查找一级域名,而是使用了.分割的形式去查找的。最终所有 *.ac.cn 的域名由于 ac.cn 这个域名没有进行备案导致解析全部挂掉。而我们现在知道 ac.cn 这个域名是 eTLD 域名,它肯定是无法备案的。

与 domain 区别

参考 stackoverflow 的问答Domain 属性限制将 cookie 发送到的主机。 SameSite 属性限制了发送 cookie 的来源

因此,第一个 cookie 可以发送到 baz.qux.com 或其任何子域,而不管请求的来源是什么 (即是从 baz.qux.com 还是 foo.example.com 托管的网页发送的):

Set-Cookie: Foo=bar; Path=/; Secure; Domain=baz.qux.com;

第二个 cookie 只能发送到 baz.qux.com (因为未指定域,并且忽略 IE 异常),并且仅当请求源自 qux.com 站点,即不会针对跨站点请求发送:

Set-Cookie: Foo=bar; Path=/; Secure; SameSite=strict;

通过阻止随机网站 (hacker.example.com) 向包含会话 cookie 的第三方 (baz.qux.com) 执行经过身份验证的请求,可以帮助防止 CSRF。

First-Party Sets & SameParty

此节摘自这里 👈

Cookie 本质上不区分第一方或第三方,它取决于包含 Cookie 的当前上下文。我们还用之前的老例子:

如果是你正常的正在逛着天猫,天猫会把你的信息写入一些 Cookie 到 .tmall.com 这个域下,然而打开控制台你会看到,并不是所有 Cookie 都是 .tmall.com 这个域下的,里面还有很多其他域下的 Cookie,这些所有非当前域下的 Cookie 都属于第三方 Cookie,虽然你可能从来没访问过这些域,但是他们已经悄悄的通过这些第三方 Cookie 来标识你的信息,然后把你的个人信息发送过去了。

而 .tmall.com 这个域下的 Cookie 都属于第一方 Cookie,那么为什么还需要第三方 Cookie 呢?再打开 taobao.com,你会发现你已经不需要再登录了,因为淘宝、天猫都属于阿里旗下的产品,阿里为他们提供统一的登录服务,同时,你的登录信息也会存到这个统一登录服务的域下,所以存到这个域下的 Cookie 就成了三方 Cookie。

但是由于涉及到用户隐私,各大主流浏览器正在逐步禁用三方 Cookie。但是一个公司或组织往往在不同业务下会有多个不同的域名,例如 taobao.com、tianmao.com,所以很多正常的业务场景也许要借助三方 Cookie 来实现(比如单点登录和 consent 管理),直接禁用后可能会给我们的业务带来很大影响,而且之前一直以来都没有很好的解决方案,这也是 Chrome 禁用三方 Cookie 进展非常缓慢的原因。因此 Chrome 在之前的版本为 Cookie 新增了一个 SameSite 属性来限制三方 Cookie 的访问,具体参考上一节,在 Chrome 80 版本后 SameSite 的默认值被设定为 SameSite=Lax

在上面正常的业务场景中,所有不同的域名基本上都来自同一个组织或企业,我们希望在同一个运营主体下不同域名的 Cookie 也能共享。这里就需要使用 First-Party Sets 策略,即不得在不相关的站点之间交换用户信息,或对不属于同一实体的站点进行分组。

W3C 目前正在讨论新的 First-Party Sets 的配置和验证,其中一个考虑的选项是由独立实体而非浏览器公司处理验证。目前 First-Party Sets 已经确定的原则如下:

  1. First-Party Sets 中的域必须由同一组织拥有和运营
  2. 所有域名应该作为一个组被用户识别
  3. 所有域名应该共享一个共同的隐私政策

每一个需要用到 First-Party Sets 策略的域名都应该把一个 JSON 配置托管在 /.well-known/first-party-set 路由下。例如 brandx.site 的配置应该托管在 https://brandx.site/.well-known/first-party-set 下:

{
  "owner": "brandx.site",
  "version": 1,
  "members": ["fly-brandx.site", "drive-brandx.site"]
}

另外 members 里的两个域名均需要增加所有者的配置:

{ "owner": "brandx.site" }

First-Party Sets 还有一些限制:

  1. 一个集合可能只有一个所有者
  2. 一个成员只能属于一个集合,不能重叠或混合
  3. 域名列表不要过大

需要注意的是,所有开启了 First-Party Sets 域名下需要共享的 Cookie 都需要增加 SameParty 属性,例如,如果我在 brandx.site 下设置了下面的 Cookie:

Set-Cookie: name=tate; Secure; SameSite=Lax; SameParty

这时我在 fly-brandx.site 下发送 brandx.site 域名的请求,Cookie 也可以被携带了,但是如果我在另外一个网站,例如 hotel.xyz 下发送这个请求,Cookie 就不会被携带。在 SameParty 被广泛支持之前,你可以把它和 SameSite 属性一起定义来确保 Cookie 的行为降级,另外还有一些额外的要求:

  1. SameParty Cookie 必须包含 Secure.
  2. SameParty Cookie 不得包含 SameSite=Strict.

sameparty

CHIPS

具有独立分区状态的 Cookie (CHIPS - Cookies Having Independent Partitioned State) 是一项隐私沙盒提案,它允许开发人员将 cookie 选择到“分区(partitioned)”存储中,每个顶级站点都有单独的 cookie jar。分区的第三方 cookie 与最初设置的顶级站点相关联,无法从其他地方访问。其目的是允许第三方服务设置 cookie,但只能在最初设置它们的顶级站点的上下文中读取。看使用场景:

chips demo

假如我们现在有一个通用的聊天服务,由第三方服务 support.chat.example 提供支持,我们的网站 retail.example 希望用 iframe 的方式嵌入这个聊天框。这个嵌入式的聊天服务可能会依赖 Cookie 来保存用户的交互历史记录。

假如没有了设置跨站点三方 Cookie 的能力,则 support.chat.example 可能需要更改为依赖 retail.example 传递给他们的第一方会话的一些标识符。在这种情况下,每个嵌入 support.chat.example 聊天服务的网站都需要额外的设置来传递状态,这大大增加了开发成本。

或者,我们也可以允许 support.chat.example 请求 retail.example 页面上的 JavaScript。这引入了非常大的安全风险,也不是个靠谱的方法。

那我们可以怎么做呢?

<!-- Partitioned cookies must be set with Secure and Path=/ and without the Domain attribute.  -->
<!-- It is recommended to use the __Host prefix when setting partitioned cookies to make them bound to the hostname (and not the registrable domain). -->
Set-Cookie: __Host-example=34d8g; SameSite=None; Secure; Path=/; Partitioned;

chips demo 2

还是上面的例子,我们在站点 A 中通过 iframe 嵌入了一个站点 C,正常情况下如果三方 Cookie 被禁用后,C 是无法在 A 站点访问到它的 Cookie 的。

如果 C 在它的 Cookie 上指定了 Partitioned 属性,这个 Cookie 将保存在一个特殊的分区 jar 中。它只会在站点 A 中通过 iframe 嵌入站点 C 时才会生效,浏览器会判定只会在顶级站点为 A 时才发送该 Cookie。

当用户访问一个新站点时,例如站点 B,如果也它通过 iframe 嵌入了站点 C,这时在站点 B 下的站点 C 是无法访问到之前在 A 下面设置的那个 Cookie 的。

如果用户直接访问站点 C,一样也是访问不到这个 Cookie 的。这就在保护了用户隐私的情况下完美的解决了 iframe 页面三方 Cookie 的问题:

chips demo 3

下面是启用了 CHIPS 后 Cookie 的分区键(partition key)的变化:

partition key

当域名都属于同一个 First-Party Set 配置里时,Cookie 的 partition key 都一样:

patrtion key 2

fenced frames

上述提到的 Cookie 还很大程度影响到广告投放,我们的业务大多可能会使用 iframe 去嵌入一些智能推荐的广告,而我们的顶级站点可以读取到 iframe 的 src 属性,这就以为着顶级站点可以从广告的 URL 推断有关访问者兴趣的信息,这在一定程度上就泄露了用户隐私。这种技术主要还是通过使用第三方 Cookie 跨站点共享信息的跟踪技术来实现的。

Feature <iframe> <fencedframe>
Embed content Yes Yes
Embedded content can access embedding context DOM Yes No
Embedding context can access embedded content DOM Yes No
Observable attributes, such as name Yes No
URLs (http://example.com) Yes Yes(mode-dependent)
Browser -managed opaque source (urn:uuid) No Yes
Access to unpartitioned storage No Yes

使用 Fenced frames ,我们依然可以显示与访问者兴趣相匹配的广告,但顶级站点是无法从 frame 的 src 属性中推断出用户的兴趣信息的,这个信息只有广告商知道。当我们需要在同一页面上显示来自不同顶级分区的数据时,建议使用 Fenced frames,如果嵌入的网页是受信任的,还是用更灵活的 iframe 即可。

常规的用法和 iframe 一样,我们可以用 src 属性来引入一个嵌入的内容:

<fencedframe src="demo_fenced_frame.html"></fencedframe>

另外 Fenced frames 可能会和其他的隐私沙盒的 API 来配合使用,浏览器可能会为 Fenced frames 生成一个不透明的 URL 。例如,配合 FLEDGE,浏览器可以生成一个 urn:uuid,来映射智能广告推荐的 URL:

<fencedframe src="urn:uuid:c36973b5-e5d9-de59-e4c4-364f137b3c7a" mode="opaque-ads" ></fencedframe>

只有在 Fenced frames 内部嵌入的广告商的站点才能获取到 urn:uuid 和 URL 的真实映射关系,外部的顶级站点是获取不到的。需要注意的是,Fenced frames 不能使用 postMessage 与它的父元素进行通信。但是可以和作为 Fenced frames 子级的 iframes 进行通信。

浏览器会给从 Fenced frames 和嵌入在 Fenced frames 中的 iframes 发出的请求设置 Header:

Sec-Fetch-Dest: fencedframe

对应的,为了正常响应 Fenced frames 嵌入的文档,服务端也需要设置下面的 Header:

Supports-Loading-Mode: fenced-frame

fenced frames

有了 Fenced frames ,我们就可以在不和嵌入的广告商共享用户信息的情况下实现智能广告推荐了,相信它未来将是禁用三方 Cookie 后智能广告推荐领域的主要解决方案。

session

session 是一种服务器端的机制,内容通常是保存在服务器的内存中,也可以保存在文件、数据库等等。session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 session 对象中的变量将不会丢失。通常用来保存用户的登录状态。

实现流程机制:

常见的购物车案例:

session - cookie

session 也可设置过期时间,禁用 cookie 并不一定无法使用 session。除了 cookie,客户端还可以将发送给服务器的数据包含在请求的 URL 中:

http-cookie-session

Web Storage

localStorage

Web Storage 的概念和 cookie 相似,区别是它是为了更大容量存储设计的,且能以一种比使用 cookie 更直观的方式存储键/值对。其中包含如下两种机制:

localStorage 和 sessionStorage 都具有相同的操作方法,如:

// 保存数据到 localStorage
localStorage.setItem('key', 'value');

// 从 localStorage 获取数据
var data = localStorage.getItem('key');

// 从 localStorage 删除保存的数据
localStorage.removeItem('key');

// 从 localStorage 删除所有保存的数据
localStorage.clear();

遍历:

var storage = window.localStorage;
for (var i = 0, len = storage.length; i < len; i++){
  var key = storage.key(i);
  var value = storage.getItem(key);
  console.log(key + " - " + value);
}

当 Storage 对象发生变化时, StorageEvent 事件会触发:

if(window.addEventListener){
  window.addEventListener("storage", handle_storage, false);
}else if(window.attachEvent){
  window.attachEvent("onstorage", handle_storage);
}

function handle_storage(e){
  if(!e){e = window.event;}
}

上述 Web Storage 仍然有几个缺点:

localForage

为了解决上述问题,Mozilla 开发了 localForage 的库 ,使得离线数据存储在任何浏览器都变得容易。

// Callback version:
localforage.getItem('somekey', function(err, value) {
  // Run this code once the value has been
  // loaded from the offline store.
  console.log(value);
});

// Supply a list of drivers, in order of preference.
localforage.setDriver([localforage.WEBSQL, localforage.INDEXEDDB]);

本地数据库

上述 Web Storage 对于大量结构化数据就无能为力了,灵活大不够强大。

Web SQL(deprecated)

Web SQL 是属于前端的关系型数据库,由一张张的二维表组成的,它也是本地存储的一种,使用 SQLite 实现,可通过 SQL(即操作关系型 DB 的语言)支持增删查改等。同样的关系型数据库比如有 MySQL、SQLite、SQL Server、Oracle 等。

关系型数据库的缺点是:

IndexedDB

IndexedDB 属于非关系型数据库(NoSQL = not only sql),非关系型数据库根据它的存储特点,常用的有:

它的特点是存储比较灵活,但是查找没有像关系型数据库一样好用。适用于数据量很大,只需要单表 key 查询,一致性不用很高的场景。具体操作请参考这里

参考链接

  1. SameSite 那些事 By 公子
  2. fencedframe 可以替代 iframe 吗? By ConardLi
  3. First-Party Sets and the SameParty attribute
  4. Cookies Having Independent Partitioned State (CHIPS)