Cookie 存储 API

现行标准 — 最后更新

参与方式:
GitHub whatwg/cookiestore新建问题, 已开放问题
Matrix 聊天
提交记录:
GitHub whatwg/cookiestore/commits
当前提交快照
@cookiestoreapi
测试:
web-platform-tests cookiestore/持续更新中
翻译 (非规范性)
简体中文
日本語
한국어

摘要

用于文档和 Service Worker 的异步 JavaScript Cookie API。

1. 介绍

本节是非规范性的。

本标准为运行在HTML文档和服务工作线程中的脚本定义了一个异步的Cookie API。

HTTP Cookie自从其在Netscape的起源以来(由archive.org保存的文档),一直为网络提供了有价值的状态管理机制

同步的单线程脚本级别document.cookie 接口是复杂性和性能问题的来源,这些问题因许多浏览器从以下模型转变而愈加严重:

……到追求平滑响应和高性能的现代网络:

在现代网络中,网络应用程序某部分的Cookie操作不能阻塞:

在服务工作线程中构建的网络的新部分也需要访问Cookie,但由于没有文档且无法阻塞事件循环(这会干扰无关事件的处理),因此无法使用同步的阻塞document.cookie 接口。

1.1. 作为document.cookie的替代方案

如今写入一个Cookie意味着阻塞事件循环,同时等待浏览器以Set-Cookie格式同步更新Cookie存储区:

document.cookie =
  '__Secure-COOKIENAME=cookie-value' +
  '; Path=/' +
  '; expires=Fri, 12 Aug 2016 23:05:17 GMT' +
  '; Secure' +
  '; Domain=example.org';
// 现在我们可以假设写入成功了,但由于
// 失败是静默的,因此很难判断,所以我们
// 读取以查看写入是否成功
var successRegExp =
  /(^|; ?)__Secure-COOKIENAME=cookie-value(;|$)/;
if (String(document.cookie).match(successRegExp)) {
  console.log('成功了!');
} else {
  console.error('未成功,且原因不明');
}

如果您可以改为写:

const one_day_ms = 24 * 60 * 60 * 1000;
cookieStore.set(
  {
    name: '__Secure-COOKIENAME',
    value: 'cookie-value',
    expires: Date.now() + one_day_ms,
    domain: 'example.org'
  }).then(function() {
    console.log('成功了!');
  }, function(reason) {
    console.error('未成功,原因如下:',
      reason);
  });
// 同时我们可以在等待Cookie存储处理写入时做其他事情...

这还具有不依赖文档且不阻塞的优点,这使得它可以在服务工作线程中使用,后者从脚本中无法访问Cookie。

本标准还包含一个高效的监控API,用于替代基于setTimeout的轮询Cookie监视器,通过Cookie更改观察器监控Cookie更改。

1.2. 概要

简而言之,该API提供以下功能:

1.3. 查询Cookies

文档服务工作线程通过 cookieStore 属性访问相同的查询API,该属性位于全局对象上。

get()getAll() 方法位于CookieStore 上,用于查询Cookies。 两种方法均返回Promise。 两种方法接受相同的参数,可以是:

get() 方法本质上是getAll() 的一种形式,只返回第一个结果。

读取一个Cookie:
try {
  const cookie = await cookieStore.get('session_id');
  if (cookie) {
    console.log(`找到 ${cookie.name} Cookie: ${cookie.value}`);
  } else {
    console.log('未找到Cookie');
  }
} catch (e) {
  console.error(`Cookie存储错误: ${e}`);
}
读取多个Cookies:
try {
  const cookies = await cookieStore.getAll('session_id'});
  for (const cookie of cookies)
    console.log(`结果: ${cookie.name} = ${cookie.value}`);
} catch (e) {
  console.error(`Cookie存储错误: ${e}`);
}

服务工作线程可以获取将在fetch请求发送到 其范围内的任何URL时发送的Cookies列表。

读取特定URL的Cookies(在服务工作线程中):
await cookieStore.getAll({url: '/admin'});

文档只能获取其当前URL下的Cookies。换句话说, 文档上下文中唯一有效的url 值是该文档的URL。

get()getAll() 方法返回的对象包含Cookie存储中的所有相关信息,而不仅仅是旧版document.cookie API中的名称

访问所有Cookie数据:
await cookie = cookieStore.get('session_id');
console.log(`Cookie范围 - 域名: ${cookie.domain} 路径: ${cookie.path}`);
if (cookie.expires === null) {
  console.log('Cookie在会话结束时过期');
} else {
  console.log(`Cookie过期时间: ${cookie.expires}`);
}
if (cookie.secure)
  console.log('该Cookie仅限安全源使用');

1.4. 修改Cookies

文档服务工作线程通过 cookieStore 属性访问相同的修改API,该属性位于全局对象上。

Cookie的创建或修改(写入)通过set() 方法完成。

写入一个Cookie:
try {
  await cookieStore.set('opted_out', '1');
} catch (e) {
  console.error(`设置Cookie失败: ${e}`);
}

上述set() 调用是使用选项字典的简写,如下所示:

await cookieStore.set({
  name: 'opted_out',
  value: '1',
  expires: null,  // 会话Cookie

  // 默认情况下,domain为null,表示作用域限定在当前域名。
  domain: null,
  path: '/'
});

Cookie的删除(过期)通过delete() 方法完成。

删除一个Cookie:
try {
  await cookieStore.delete('session_id');
} catch (e) {
  console.error(`删除Cookie失败: ${e}`);
}

实际上,删除Cookie是通过将Cookie过期时间设置为过去来实现的,这样依然有效。

通过更改过期时间删除Cookie:
try {
  const one_day_ms = 24 * 60 * 60 * 1000;
  await cookieStore.set({
    name: 'session_id',
    value: 'value will be ignored',
    expires: Date.now() - one_day_ms });
} catch (e) {
  console.error(`删除Cookie失败: ${e}`);
}

1.5. 监控Cookies

为避免轮询,可以直接观察Cookie的变化。

文档中,所有相关Cookie变更都会触发change事件。

在文档中注册change事件:
cookieStore.addEventListener('change', event => {
  console.log(`${event.changed.length} 个Cookie已更改`);
  for (const cookie in event.changed)
    console.log(`Cookie ${cookie.name} 变更为 ${cookie.value}`);

  console.log(`${event.deleted.length} 个Cookie已删除`);
  for (const cookie in event.deleted)
    console.log(`Cookie ${cookie.name} 已删除`);
});

服务工作线程中,cookiechange事件会在全局作用域上触发,但需要显式订阅,并与服务工作线程的注册相关联。

在服务工作线程中注册cookiechange事件:
self.addEventListener('activate', (event) => {
  event.waitUntil(async () => {
    // 快照当前订阅状态。
    const subscriptions = await self.registration.cookies.getSubscriptions();

    // 清除所有现有订阅。
    await self.registration.cookies.unsubscribe(subscriptions);

    await self.registration.cookies.subscribe([
      {
        name: 'session_id',  // 订阅名为session_id的Cookie变更事件。
      }
    ]);
  });
});

self.addEventListener('cookiechange', event => {
  // 事件拥有 |changed| 和 |deleted| 属性,与Document事件语义一致。
  console.log(`${event.changed.length} 个Cookie已更改`);
  console.log(`${event.deleted.length} 个Cookie已删除`);
});

subscribe() 的调用是累积的,不同模块或库可以独立设置自己的订阅。服务工作线程的订阅会与服务工作线程注册一起持久化。

订阅可以使用与get()getAll() 相同的选项。细粒度订阅的复杂性是有合理性的, 因为向服务工作线程 派发无关Cookie变更事件的成本远高于向 Window 派发同类事件的成本。具体来说,向服务工作线程 派发事件可能需要唤醒该线程,这对电池寿命有显著影响。

getSubscriptions() 允许服务工作线程检查已订阅的内容。

检查变更订阅:
const subscriptions = await self.registration.cookies.getSubscriptions();
for (const sub of subscriptions) {
  console.log(sub.name, sub.url);
}

2. 概念

cookie 的规范性定义见 Cookies § User Agent Requirements

根据 Cookies § Storage Model,一个 cookie 包含以下字段: name(名称), value(值), domain(域名), path(路径), http-only-flag(仅HTTP标志).

对给定的string input规范化 cookie 名称或值: 移除所有位于input开头或结尾的 U+0009 TAB 和 U+0020 SPACE。

当一个cookie处于作用域内,且其 http-only-flag 未设定时,该cookie为 脚本可见。这一点在处理模型中得到更正式的约束,会在适当的时候参考 Cookies § Retrieval Model

Cookie 还受限于一些大小限制。根据 Cookies § Storage Model

Cookie 的属性值以 字节序列 存储,而不是字符串。

cookie store(Cookie存储区) 的规范性定义见 Cookies § User Agent Requirements

cookie store 发生下列任一情况时,执行 处理Cookie变更 的步骤。

2.3. Service Worker 扩展

[Service-Workers] 定义了 service worker registration(服务工作线程注册),本规范对此进行了扩展。

服务工作线程注册 关联了一个 Cookie 变更订阅列表,它是一个 列表; 每个成员是一个 Cookie 变更订阅项。一个 Cookie 变更订阅项 一个包含 二元组 的结构: name(名称)url(网址)

3. CookieStore 接口

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStore : EventTarget {
  Promise<CookieListItem?> get(USVString name);
  Promise<CookieListItem?> get(optional CookieStoreGetOptions options = {});

  Promise<CookieList> getAll(USVString name);
  Promise<CookieList> getAll(optional CookieStoreGetOptions options = {});

  Promise<undefined> set(USVString name, USVString value);
  Promise<undefined> set(CookieInit options);

  Promise<undefined> delete(USVString name);
  Promise<undefined> delete(CookieStoreDeleteOptions options);

  [Exposed=Window]
  attribute EventHandler onchange;
};

dictionary CookieStoreGetOptions {
  USVString name;
  USVString url;
};

enum CookieSameSite {
  "strict",
  "lax",
  "none"
};

dictionary CookieInit {
  required USVString name;
  required USVString value;
  DOMHighResTimeStamp? expires = null;
  USVString? domain = null;
  USVString path = "/";
  CookieSameSite sameSite = "strict";
  boolean partitioned = false;
};

dictionary CookieStoreDeleteOptions {
  required USVString name;
  USVString? domain = null;
  USVString path = "/";
  boolean partitioned = false;
};

dictionary CookieListItem {
  USVString name;
  USVString value;
};

typedef sequence<CookieListItem> CookieList;

3.1. get() 方法

cookie = await cookieStore . get(name)
cookie = await cookieStore . get(options)

返回一个Promise,解析为指定cookie名称(或其他选项)的第一个在作用域内的脚本可见值。 在服务工作线程上下文中,默认路径为服务工作线程注册的scope路径。 在文档中,默认路径为当前文档的路径,不受 replaceState()document.domain 的更改影响。

get(name) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. p一个新的Promise

  6. 并行执行以下步骤:

    1. list 为运行 查询cookie(参数为 urlname)的结果。

    2. 如果 list 失败,则 拒绝 p,错误为 TypeError,并中止这些步骤。

    3. 如果 list 为空,则 解析 p 为 null。

    4. 否则,解析 plist 的第一个项。

  7. 返回 p

get(options) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. 如果 options 为空,则返回 一个被拒绝的Promise,错误为 TypeError

  6. 如果 options["url"] 存在,则执行以下步骤:

    1. parsed 为用 settingsAPI基础URL 解析 options["url"] 的结果。

    2. 如果 this相关全局对象Window 对象且 parsed等于 url(排除片段), 则返回 一个被拒绝的Promise,错误为 TypeError

    3. 如果 parsed来源url来源 不为 同源, 则返回 一个被拒绝的Promise,错误为 TypeError

    4. urlparsed

  7. p一个新的Promise

  8. 并行执行以下步骤:

    1. list 为使用 urloptions["name"], 查询 cookies 的结果, 默认值为 null。

    2. 如果 list 失败,则 拒绝 p,错误为 TypeError,并中止这些步骤。

    3. 如果 list 为空,则 解析 p 为 null。

    4. 否则,解析 plist 的第一个项。

  9. 返回 p

3.2. getAll() 方法

cookies = await cookieStore . getAll(name)
cookies = await cookieStore . getAll(options)

返回一个Promise,解析为指定cookie名称(或其他选项)的所有在作用域内的脚本可见值。 在服务工作线程上下文中,默认路径为服务工作线程注册的scope路径。 在文档中,默认路径为当前文档的路径,不受 replaceState()document.domain 的更改影响。

getAll(name) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. p一个新的Promise

  6. 并行执行以下步骤:

    1. list 为运行 查询cookie(参数为 urlname)的结果。

    2. 如果 list 失败,则 拒绝 p,错误为 TypeError

    3. 否则,解析 plist

  7. 返回 p

getAll(options) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. 如果 options["url"] 存在,则执行以下步骤:

    1. parsed 为用 settingsAPI基础URL 解析 options["url"] 的结果。

    2. 如果 this相关全局对象Window 对象且 parsed等于 url(排除片段), 则返回 一个被拒绝的Promise,错误为 TypeError

    3. 如果 parsed来源url来源 不为 同源, 则返回 一个被拒绝的Promise,错误为 TypeError

    4. urlparsed

  6. p一个新的Promise

  7. 并行执行以下步骤:

    1. list 为使用 urloptions["name"], 查询 cookies 的结果, 默认值为 null。

    2. 如果 list 失败,则 拒绝 p,错误为 TypeError

    3. 否则,解析 plist

  8. 返回 p

3.3. set() 方法

await cookieStore . set(name, value)
await cookieStore . set(options)

写入(创建或修改)一个cookie。

选项默认值为:

  • 路径:/

  • 域名:与当前文档或服务工作线程的位置域名一致

  • 无过期时间

  • SameSite:strict

set(name, value) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. domain 为 null。

  6. path 为 "/"。

  7. sameSitestrict

  8. partitioned 为 false。

  9. p一个新的Promise

  10. 并行执行以下步骤:

    1. r 为运行 设置cookie(参数为 url, name, value, domain, path, sameSitepartitioned)的结果。

    2. 如果 r 失败,则 拒绝 p,错误为 TypeError,并中止这些步骤。

    3. 解析 p 为 undefined。

  11. 返回 p

set(options) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. p一个新的Promise

  6. 并行执行以下步骤:

    1. r 为运行 设置cookie(参数为 url, options["name"], options["value"], options["expires"], options["domain"], options["path"], options["sameSite"], 以及 options["partitioned"] 的结果。

    2. 如果 r 失败,则 拒绝 p,错误为 TypeError,并中止这些步骤。

    3. 解析 p 为 undefined。

  7. 返回 p

3.4. delete() 方法

await cookieStore . delete(name)
await cookieStore . delete(options)

删除(使过期)指定名称或名称与可选域名和路径的cookie。

delete(name) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. p一个新的Promise

  6. 并行执行以下步骤:

    1. r 为运行 删除cookie(参数为 url, name, null, "/" 和 true) 的结果。

    2. 如果 r 失败,则 拒绝 p,错误为 TypeError,并中止这些步骤。

    3. 解析 p 为 undefined。

  7. 返回 p

delete(options) 方法步骤如下:
  1. settingsthis相关设置对象

  2. originsettings来源

  3. 如果 origin不透明来源,则返回 一个被拒绝的Promise,错误为 "SecurityError" DOMException

  4. urlsettings创建URL

  5. p一个新的Promise

  6. 并行执行以下步骤:

    1. r 为运行 删除cookie(参数为 url, options["name"], options["domain"], options["path"], 以及 options["partitioned"] 的结果。

    2. 如果 r 失败,则 拒绝 p,错误为 TypeError,并中止这些步骤。

    3. 解析 p 为 undefined。

  7. 返回 p

4. CookieStoreManager 接口

CookieStoreManager 关联了一个 registration(注册),即 服务工作线程注册

CookieStoreManager 接口允许 Service Worker 订阅cookie变更事件。必须使用 subscribe() 方法来标识某个 服务工作线程注册对变更事件感兴趣。

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStoreManager {
  Promise<undefined> subscribe(sequence<CookieStoreGetOptions> subscriptions);
  Promise<sequence<CookieStoreGetOptions>> getSubscriptions();
  Promise<undefined> unsubscribe(sequence<CookieStoreGetOptions> subscriptions);
};

4.1. subscribe() 方法

await registration . cookies . subscribe(subscriptions)

订阅cookie变更。订阅项可以使用与 get()getAll() 相同的选项, 属性 nameurl 是可选的。

订阅后,通知会以 "cookiechange" 事件的形式在 Service Worker 全局作用域上触发:

subscribe(subscriptions) 方法步骤如下:
  1. settingsthis相关设置对象

  2. registrationthisregistration

  3. p一个新的Promise

  4. 并行执行以下步骤:

    1. subscription listregistration 关联的 cookie 变更订阅列表

    2. 对于 subscriptions 中的每个 entry,执行以下步骤:

      1. nameentry["name"]。

      2. 规范化 name

      3. url解析 entry["url"] 且使用 settingsAPI 基础 URL 的结果。

      4. 如果 url 并非以 registrationscope url 开头, 则使用 TypeError 拒绝 p,并终止这些步骤。

      5. subscriptioncookie 变更订阅 (name, url)。

      6. 如果 subscription list 尚未 包含 subscription,则 subscription 添加到 subscription list

    3. 用 undefined 解析 p

  5. 返回 p

4.2. getSubscriptions() 方法

subscriptions = await registration . cookies . getSubscriptions()

此方法返回一个Promise,解析为该服务工作线程注册所订阅的cookie变更列表。

getSubscriptions() 方法步骤如下:
  1. registrationthisregistration

  2. p一个新的Promise

  3. 并行执行以下步骤:

    1. subscriptionsregistration 关联的 cookie变更订阅列表

    2. result 为一个新的 列表

    3. subscriptions 中每个 subscription,执行以下步骤:

      1. 追加 «[ "name" → subscriptionname, "url" → subscriptionurl]» 到 result

    4. 解析 presult

  4. 返回 p

4.3. unsubscribe() 方法

await registration . cookies . unsubscribe(subscriptions)

调用此方法将使已注册的服务工作线程停止接收之前订阅的事件。subscriptions参数应与传给 subscribe()getSubscriptions() 返回的订阅项格式相同。

unsubscribe(subscriptions) 方法步骤如下:
  1. settingsthis相关设置对象

  2. registrationthisregistration

  3. p一个新的Promise

  4. 并行执行以下步骤:

    1. subscription listregistration 关联的 cookie变更订阅列表

    2. subscriptions 中每个 entry,执行以下步骤:

      1. nameentry["name"]。

      2. 规范化 name

      3. url解析 entry["url"] 且使用 settingsAPI 基础 URL 的结果。

      4. 如果 url 并非以 registrationscope url 开头, 则使用 TypeError 拒绝 p,并终止这些步骤。

      5. subscriptioncookie 变更订阅 (name, url)。

      6. 移除 subscription list 中等于 subscription 的任何

    3. 解析 p 为 undefined。

  5. 返回 p

4.4. ServiceWorkerRegistration 接口

ServiceWorkerRegistration 接口被扩展,以通过 CookieStoreManager 提供 cookies 属性,用于订阅cookie变更。

[Exposed=(ServiceWorker,Window)]
partial interface ServiceWorkerRegistration {
  [SameObject] readonly attribute CookieStoreManager cookies;
};

每个 ServiceWorkerRegistration 都关联一个 CookieStoreManager 对象。 该 CookieStoreManagerregistration 等于 ServiceWorkerRegistration服务工作线程注册

cookies getter 步骤是返回 this 关联的 CookieStoreManager 对象。

从Service Worker脚本订阅cookie变更:
self.registration.cookies.subscribe([{name:'session-id'}]);
从窗口上下文中的脚本订阅cookie变更:
navigator.serviceWorker.register('sw.js').then(registration => {
  registration.cookies.subscribe([{name:'session-id'}]);
});

5. 事件接口

5.1. CookieChangeEvent 接口

CookieChangeEvent 会在CookieStore 对象上分发,适用于Window 上下文,当发生任何脚本可见的cookie变更时。

[Exposed=Window,
 SecureContext]
interface CookieChangeEvent : Event {
  constructor(DOMString type, optional CookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary CookieChangeEventInit : EventInit {
  CookieList changed;
  CookieList deleted;
};

changeddeleted 属性必须返回初始化时设置的值。

5.2. ExtendableCookieChangeEvent 接口

ExtendableCookieChangeEvent 会在ServiceWorkerGlobalScope 对象上分发,当发生任何匹配Service Workercookie变更订阅列表脚本可见cookie变更时。

注意: ExtendableEvent 作为所有Service Worker 事件的父接口,以便在执行异步操作时保持工作线程存活。

[Exposed=ServiceWorker]
interface ExtendableCookieChangeEvent : ExtendableEvent {
  constructor(DOMString type, optional ExtendableCookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary ExtendableCookieChangeEventInit : ExtendableEventInit {
  CookieList changed;
  CookieList deleted;
};

changeddeleted 属性必须返回初始化时设置的值。

6. 全局接口

CookieStore 可通过在WindowServiceWorkerGlobalScope 上下文的全局作用域属性访问。

6.1. Window 接口

[SecureContext]
partial interface Window {
  [SameObject] readonly attribute CookieStore cookieStore;
};

Window 关联了一个CookieStore,其类型为CookieStore

cookieStore getter 步骤为返回 this 关联的 CookieStore

6.2. ServiceWorkerGlobalScope 接口

partial interface ServiceWorkerGlobalScope {
  [SameObject] readonly attribute CookieStore cookieStore;

  attribute EventHandler oncookiechange;
};

ServiceWorkerGlobalScope 关联了一个CookieStore,其类型为CookieStore

cookieStore getter 步骤为返回 this 关联的 CookieStore

7. 算法

要将日期和时间dateTime表示为时间戳, 返回从1970年1月1日00:00:00 UTC到dateTime的毫秒数 (假定每一天正好为86,400,000毫秒)。

注意: 这与ECMAScript中的[ECMAScript]时间值表示方法一致。

序列化日期一个 DOMHighResTimeStamp millis, 令dateTime为自1970年1月1日00:00:00 UTC起millis毫秒后的日期和时间 (假定每一天正好为86,400,000毫秒), 并返回dateTime对应的最接近的cookie-date表示的字节序列,该表示依据Cookies § Dates定义。

7.1. 查询cookie

要根据 URL urlstring-或-null name 查询 cookies

  1. 执行 Cookies § 检索模型 中定义的步骤,以使用 url 作为 request-uri 计算 "cookie-string from a given cookie store"。 cookie-string 本身会被忽略,后续步骤使用其中间结果 cookie-list

    在这些步骤中,cookie-string 是为 “非 HTTP” API 生成的。

  2. list 为一个新的 列表

  3. 对于 cookie-list 中的每个 cookie,执行以下步骤:

    1. 断言:cookiehttp-only-flag 为 false。

    2. 如果 name 非 null:

      1. 规范化 name

      2. cookieName 为对 cookie无 BOM 的 UTF-8 解码结果, 使用 cookiename

      3. 如果 cookieName 不等于 name,则 继续下一项

    3. item 为对 cookie 创建 CookieListItem 的结果。

    4. item 添加到 list

  4. 返回 list

要根据 cookie cookie 创建 CookieListItem

  1. name 为对 cookie无 BOM 的 UTF-8 解码结果, 使用 cookiename

  2. value 为对 cookie无 BOM 的 UTF-8 解码结果, 使用 cookievalue

  3. 返回 «[ "name" → name, "value" → value ]»。

注意: 已知有一个实现会暴露除 name 和 value 之外的信息。

要使用 urlnamevalue、可选的 expiresdomainpathsameSitepartitioned 设置 cookie,请执行以下步骤:

  1. 规范化 name

  2. 规范化 value

  3. 如果 namevalue 包含 U+003B (;)、任何 C0 控制字符(除了 U+0009 TAB),或 U+007F DELETE,则返回失败。

    注意,这些字符限制是否也应该应用于 expiresdomainpathsameSite 仍在讨论中。[httpwg/http-extensions Issue #1593]

  4. 如果 name 包含 U+003D (=),则返回失败。

  5. 如果 name长度为 0:

    1. 如果 value 包含 U+003D (=),则返回失败。

    2. 如果 value长度为 0,则返回失败。

    3. 如果 value 字节小写后, `__host-`、`__host-http-`、`__http-` 或 `__secure-` 开头,则返回失败。

  6. 如果 name 字节小写后, `__host-http-` 或 `__http--` 开头,则返回失败。

  7. encodedNameUTF-8 编码 name 的结果。

  8. encodedValueUTF-8 编码 value 的结果。

  9. 如果 encodedName字节序列 长度加上 encodedValue字节序列 长度大于 最大 name/value 对长度,则返回失败。

  10. hosturl主机

  11. attributes 为一个新的 列表

  12. 如果 domain 不为 null,则执行以下步骤:

    1. 如果 domain 以 U+002E (.) 开头,则返回失败。

    2. 如果 name 字节小写后, `__host-` 开头,则返回失败。

    3. 如果 domain 不是 host 的可注册域名后缀且不等于 host,则返回失败。

    4. parsedDomain主机解析 domain 的结果。

    5. 断言:parsedDomain 不为失败。

    6. encodedDomainUTF-8 编码 parsedDomain 的结果。

    7. 如果 encodedDomain字节序列 长度大于 最大属性值长度,则返回失败。

    8. `Domain`/encodedDomain 添加到 attributes

  13. 如果提供了 expires,则将 `Expires`/expires日期序列化追加attributes

  14. 如果 path 是空字符串,则将 path 设为 url序列化 Cookie 默认路径

  15. 如果 path 不以 U+002F(/)开头,则返回失败。

  16. 如果 path 不等于 U+002F(/),且 name字节小写后, `__host-` 开头,则返回失败。

  17. encodedPathUTF-8 编码后的 path

  18. 如果 encodedPath字节序列长度大于最大属性值大小,则返回失败。

  19. 追加 `Path`/encodedPathattributes

  20. 追加 `Secure`/`` 到 attributes

  21. 根据 sameSite 进行分支:

    "none"

    追加 `SameSite`/`None` 到 attributes

    "strict"

    追加 `SameSite`/`Strict` 到 attributes

    "lax"

    追加 `SameSite`/`Lax` 到 attributes

  22. 如果 partitioned 为 true,追加 `Partitioned`/`` 到 attributes

  23. 执行 Cookies § 存储模型中,当用户代理“收到 cookie”时定义的步骤,其中 url 作为 request-uriencodedName 作为 cookie-nameencodedValue 作为 cookie-valueattributes 作为 cookie-attribute-list

    在这些步骤中,新创建的 cookie 被视为来自“非 HTTP”API。

  24. 返回成功。

    注意:由于 [RFC6265BIS-14] 中的要求,存储 cookie 仍可能失败,但这些步骤将视为成功。

要使用 urlnamedomainpathpartitioned 删除 cookie,请执行以下步骤:

  1. expires 为可表示的最早日期,并以 时间戳表示。

    注意: 对于本算法而言,expires 的具体值并不重要,只要是过去的日期即可。

  2. 规范化 name

  3. value 为空字符串。

  4. 如果 name长度为 0,则将 value 设为任意非空的 实现定义字符串。

  5. 返回运行 设置 cookie 的结果,参数为 urlnamevalueexpiresdomainpath、"strict" 和 partitioned

7.4. 处理变更

处理cookie变更,执行以下步骤:

  1. 对于每个Window window,执行以下步骤:

    1. urlwindow相关设置对象创建URL

    2. changesurl可观察变更

    3. 如果changes为空,则继续

    4. 在 DOM 操作任务源上排队全局任务,给定 window,以触发名为 "change" 的更改事件,携带 changes,在 windowCookieStore 上。

  2. 对于每个服务工作线程注册 registration,执行以下步骤:

    1. changes为一个新的集合

    2. registration可观察变更中的每个change(针对registrationscope url),执行:

      1. cookiechange的cookie。

      2. registrationcookie变更订阅列表中的每个subscription,执行:

        1. 如果change不在subscriptionurl对应的可观察变更中, 则继续

        2. cookieName为对cookieUTF-8无BOM解码得到的name

        3. 如果cookieName等于subscriptionname, 则追加changechanges跳出循环。

    3. 如果changes为空,则继续

    4. changedListdeletedList为对changes执行准备列表得到的结果。

    5. registration触发一个名为"cookiechange"的功能事件, 使用ExtendableCookieChangeEvent, 并设置如下属性:

      changed

      changedList

      deleted

      deletedList

可观察变更,针对url,是集合, 包含cookie变更, 这些变更针对cookie存储区中的cookie, 满足Cookies § Retrieval Algorithm步骤1的要求, 即用url作为request-uri,针对“非HTTP”API计算“从给定cookie存储生成的cookie-string”的条件。

cookie变更是一个cookie和一个类型(changeddeleted):

要在target触发change事件,事件名为type,带有changes,执行:

  1. event为用CookieChangeEvent创建的事件。

  2. 设置eventtype 属性为type

  3. 设置eventbubblescancelable属性为false。

  4. changedListdeletedList为对changes执行准备列表的结果。

  5. 设置eventchanged属性为changedList

  6. 设置eventdeleted属性为deletedList

  7. target上分发event

要对changes准备列表,执行以下步骤:

  1. changedList为一个新的列表

  2. deletedList为一个新的列表

  3. changes中的每个change,执行:

    1. item为用change的cookie执行创建CookieListItem得到的结果。

    2. 如果change的类型是changed,则追加itemchangedList

    3. 否则,执行:

      1. 设置item["value"] 为undefined。

      2. 追加itemdeletedList

  4. 返回changedListdeletedList

8. 安全性考量

除服务工作线程上下文中的cookie访问外,本API并不打算向Web开放任何新能力。

8.1. 注意事项!

尽管浏览器的cookie实现正在朝着更高安全性和更少“坑点”的默认值演进,目前对于cookie数据安全性仍难以给出强保证。

因此,解释任何cookie值时都应谨慎,绝不要将cookie值作为脚本、HTML、CSS、XML、PDF或其他可执行格式执行。

8.2. 限制?

本API可能有让cookie更易用、从而鼓励进一步使用的副作用。如果导致在非安全上下文中进一步使用,可能让Web环境对用户更不安全。因此,本API已限制为仅可用于安全上下文

8.3. 安全cookie

本节为非规范性内容。

本API只允许写入Secure cookie,以促进更安全的决策。但API仍允许读取非Secure cookie,以便迁移到Secure cookie。副作用是:用API读取或修改非Secure cookie时,该cookie会自动变为Secure

8.4. 意外行为

一些现有cookie行为(尤其是域而非origin的定向、非安全上下文可设置安全上下文可读的cookie,以及脚本可设置脚本不可读的cookie)在Web安全角度来看可能非常意外。

其它意外行为见Cookies § Introduction,例如一个cookie可设置到超级域(如app.example.com可设置整个example.com域的cookie),且cookie可被同一域名下所有端口读取。

进一步复杂化的是各大浏览器cookie处理的历史差异,虽然部分(如端口处理)现已趋于一致。

8.5. 前缀

在可行情况下,示例使用__Host-__Secure-前缀,这会让某些浏览器禁止非安全上下文覆盖、禁止无Secure标志覆盖,并且__Host-前缀下禁止显式Domain或非'/' Path属性(实际上强制同源语义)。这些前缀在实现安全cookie的浏览器中提供重要安全益处,并在其他浏览器中平滑降级(即特殊语义未强制但cookie正常工作,且异步cookie API会强制写入操作的安全语义)。本API的主要目标是与现有cookie兼容,因此也有部分不带这些前缀的cookie名示例。

这些前缀规则也会被本API的写入操作强制,但其他API可能未强制。故依赖其强制性并不明智,除非广泛采用。

8.6. URL作用域

虽然服务工作线程脚本目前无法直接访问cookie,但已经可以通过受控渲染范围内的HTML和脚本资源,注入cookie监控代码,从而实现服务工作线程脚本的远程控制。这意味着在服务工作线程作用域内访问cookie在技术上已经可行,只是不太方便。

当服务工作线程作用域比/更窄时,仍可能通过猜测或构造一个允许IFRAME的404页面URL,借助脚本在该页面内运行,读取超出作用域路径空间的cookie。相同技术也可扩展到整个origin,但精心构建的网站(即所有超出作用域页面都不可IFRAME)实际上可拒绝路径作用域服务工作线程此能力,因此在未进一步讨论影响前,我不愿取消该限制。

8.7. Cookie规避

为降低开发复杂度并避免临时测试cookie,本异步cookie API会显式拒绝写入或删除操作被忽略的情况。同时会拒绝读取操作被忽略实际cookie数据、模拟空cookie jar的情况。在这些上下文下,观察cookie变更的尝试仍会“工作”,但仅当读访问允许时(如站点权限变更)才会调用回调。

目前在脚本发起cookie写入被禁止的上下文下写document.cookie通常什么也不做。但许多cookie写入脚本和框架总是写测试cookie再检查其存在,以判断脚本能否写cookie。

同理,在脚本发起cookie读取被禁止的上下文下读document.cookie通常返回空字符串。但配合Web服务器可验证服务端写入和读取cookie是否生效,并将结果报告给脚本(脚本仍看到空字符串),脚本据此可推断脚本发起的cookie读取被禁止。

9. 隐私考量

9.1. 清除cookie

本节为非规范性内容。

当用户清除某origin的cookie时,User Agent需要清除该origin的所有存储,包括服务工作线程和该origin的DOM可访问存储,防止网站在用户操作后通过持久化存储恢复用户标识。

致谢

感谢Benjamin Sittler,他提出了本API的初步方案。

特别感谢 Adam Barth, Alex Russell, Andrea Marchesini, Andrew Williams, Anne van Kesteren, Ayu Ishii, Ben Kelly, Craig Francis, Daniel Appelquist, Daniel Murphy, Domenic Denicola, Elliott Sprehn, Fagner Brack, Idan Horowitz, Jake Archibald, Joel Weinberger, Joshua Bell, Kenneth Rohde Christiansen, Lukasz Olejnik, Marijn Kruisselbrink, Mike West, Raymond Toy, Rupin Mittal, Tab Atkins,以及 Victor Costan, 对本标准的制定给予了帮助。

本标准由Dylan Cutler (Google, dylancutler@google.com) 编写。

知识产权

本Living Standard最初由W3C WICG开发,并遵循W3C软件与文档许可协议发布。

版权所有 © WHATWG (Apple, Google, Mozilla, Microsoft)。本作品采用知识共享署名4.0国际许可协议(CC BY 4.0)许可。若部分内容被纳入源代码,则这些部分遵循BSD三条款许可证

索引

本规范定义的术语

引用定义的术语

参考文献

规范性引用

[DOM]
Anne van Kesteren. DOM 标准. Living Standard. URL: https://dom.spec.whatwg.org/
[ENCODING]
Anne van Kesteren. 编码标准. Living Standard. URL: https://encoding.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch 标准. Living Standard. URL: https://fetch.spec.whatwg.org/
[HR-TIME-3]
Yoav Weiss. 高分辨率时间. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; 等. HTML 标准. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 标准. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC6265BIS-14]
S. Bingler; M. West; J. Wilander. Cookies: HTTP 状态管理机制. Internet-Draft. URL: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-14
[Service-Workers]
Yoshisato Yanagisawa; Monica CHINTALA. 服务工作线程. URL: https://w3c.github.io/ServiceWorker/
[URL]
Anne van Kesteren. URL 标准. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 标准. Living Standard. URL: https://webidl.spec.whatwg.org/

参考性引用

[ECMAScript]
ECMAScript 语言规范. URL: https://tc39.es/ecma262/multipage/

IDL 索引

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStore : EventTarget {
  Promise<CookieListItem?> get(USVString name);
  Promise<CookieListItem?> get(optional CookieStoreGetOptions options = {});

  Promise<CookieList> getAll(USVString name);
  Promise<CookieList> getAll(optional CookieStoreGetOptions options = {});

  Promise<undefined> set(USVString name, USVString value);
  Promise<undefined> set(CookieInit options);

  Promise<undefined> delete(USVString name);
  Promise<undefined> delete(CookieStoreDeleteOptions options);

  [Exposed=Window]
  attribute EventHandler onchange;
};

dictionary CookieStoreGetOptions {
  USVString name;
  USVString url;
};

enum CookieSameSite {
  "strict",
  "lax",
  "none"
};

dictionary CookieInit {
  required USVString name;
  required USVString value;
  DOMHighResTimeStamp? expires = null;
  USVString? domain = null;
  USVString path = "/";
  CookieSameSite sameSite = "strict";
  boolean partitioned = false;
};

dictionary CookieStoreDeleteOptions {
  required USVString name;
  USVString? domain = null;
  USVString path = "/";
  boolean partitioned = false;
};

dictionary CookieListItem {
  USVString name;
  USVString value;
};

typedef sequence<CookieListItem> CookieList;

[Exposed=(ServiceWorker,Window),
 SecureContext]
interface CookieStoreManager {
  Promise<undefined> subscribe(sequence<CookieStoreGetOptions> subscriptions);
  Promise<sequence<CookieStoreGetOptions>> getSubscriptions();
  Promise<undefined> unsubscribe(sequence<CookieStoreGetOptions> subscriptions);
};

[Exposed=(ServiceWorker,Window)]
partial interface ServiceWorkerRegistration {
  [SameObject] readonly attribute CookieStoreManager cookies;
};

[Exposed=Window,
 SecureContext]
interface CookieChangeEvent : Event {
  constructor(DOMString type, optional CookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary CookieChangeEventInit : EventInit {
  CookieList changed;
  CookieList deleted;
};

[Exposed=ServiceWorker]
interface ExtendableCookieChangeEvent : ExtendableEvent {
  constructor(DOMString type, optional ExtendableCookieChangeEventInit eventInitDict = {});
  [SameObject] readonly attribute FrozenArray<CookieListItem> changed;
  [SameObject] readonly attribute FrozenArray<CookieListItem> deleted;
};

dictionary ExtendableCookieChangeEventInit : ExtendableEventInit {
  CookieList changed;
  CookieList deleted;
};

[SecureContext]
partial interface Window {
  [SameObject] readonly attribute CookieStore cookieStore;
};

partial interface ServiceWorkerGlobalScope {
  [SameObject] readonly attribute CookieStore cookieStore;

  attribute EventHandler oncookiechange;
};