1. 介绍
本节为非规范性内容。
这是一个提议,旨在为运行在 HTML 文档和服务工作者中的脚本引入异步的 Cookie API。
HTTP Cookies 自其在 Netscape 起源以来(archive.org 保存的文档),为 Web 提供了有价值的状态管理机制。
同步的、单线程的脚本级 document.cookie
接口一直是复杂性和性能问题的来源,尤其是在许多浏览器从以下模型转变时:
-
单一浏览器进程,
-
单线程事件循环模型,以及
-
在处理 Cookie 操作时对脚本事件处理的响应性没有普遍预期。
……到现代 Web,追求平滑响应的高性能:
-
在多个浏览器进程中,
-
采用多线程、多事件循环模型,以及
-
在人类反应时间尺度上对响应性有明确预期。
在现代 Web 中,Web 应用程序某一部分的 Cookie 操作不能阻塞:
-
Web 应用程序的其他部分,
-
同一 Web 源的其他部分,或
-
整个浏览器。
构建在服务工作者中的较新 Web 部分也需要访问 Cookies,但无法使用同步的、阻塞的
document.cookie
接口,因为它们既没有
document
,也不能阻塞事件循环,否则会干扰无关事件的处理。
1.1. 提议变更概览
尽管完全重新思考 Cookies的想法很有吸引力,但当今的网站仍然严重依赖它们,而用于操作 Cookies 的脚本 API 在其最初几十年的使用中几乎没有变化。
如今,写入一个 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( 'It worked!' ); } else { console. error( 'It did not work, and we do not know why' ); }
如果你可以这样写会怎么样:
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( 'It worked!' ); }, function ( reason) { console. error( 'It did not work, and this is why:' , reason); }); // 同时我们可以在等待 Cookie 存储处理写入时做其他事情...
这还具有不依赖 document
且不阻塞的优势,这使得它可以从服务工作者中使用,而服务工作者脚本中通常无法访问 Cookies。
该提议还包括一个节能的监控 API,用于用 Cookie 更改观察器替代基于 setTimeout
的轮询 Cookie 监控。
1.2. 摘要
本提案概述了一个使用 Promises/async 函数的异步 API,用于以下 Cookie 操作:
-
写入(或“设置”)和删除(或“过期”)Cookies
-
-
……包括在服务工作者上下文中指定范围内的请求路径
-
-
监控 脚本可见的 Cookies 变化,使用
CookieChangeEvent
-
……在长时间运行的脚本上下文中(例如
document
) -
……在服务工作者上下文中由脚本提供的范围内请求路径
-
1.3. 查询 Cookies
文档和服务工作者通过全局对象上的 cookieStore
属性访问相同的查询 API。
get()
和 getAll()
方法用于查询 Cookies。
两个方法都返回 Promise
。
两个方法接受相同的参数,可以是:
-
一个名称,或
-
一个选项字典(对于
getAll()
是可选的)
get()
方法本质上是 getAll()
的一种形式,只返回第一个结果。
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} ` ); }
try { const cookies= await cookieStore. getAll( 'session_id' }); for ( const cookieof cookies) console. log( `结果: ${ cookie. name} = ${ cookie. value} ` ); } catch ( e) { console. error( `Cookie 存储错误: ${ e} ` ); }
服务工作者可以获取将在其范围内的任何 URL 上通过fetch发送的 Cookies 列表。
文档只能获取其当前 URL 的 Cookies。换句话说,在文档上下文中,url 的唯一有效值是文档的 URL。
get()
和 getAll()
返回的对象包含 Cookie 存储中的所有相关信息,而不仅仅是旧 document.cookie
API 中的名称和值。
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。
使用 set()
方法创建或修改(写入)Cookies。
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: '/' });
使用 delete()
方法删除(过期)Cookies。
try { await cookieStore. delete ( 'session_id' ); } catch ( e) { console. error( `删除 Cookie 失败: ${ e} ` ); }
实际上,删除一个 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
为了避免轮询,可以观察 Cookies 的变化。
在文档中,会为所有相关的 Cookie 更改触发 change
事件。
change
事件:
cookieStore. addEventListener( 'change' , event=> { console. log( ` ${ event. changed. length} 个 Cookie 已更改` ); for ( const cookiein event. changed) console. log( `Cookie ${ cookie. name} 更改为 ${ cookie. value} ` ); console. log( ` ${ event. deleted. length} 个 Cookie 已删除` ); for ( const cookiein 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| 属性,与文档事件具有相同的语义。 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 subof subscriptions) { console. log( sub. name, sub. url); }
2. 概念
2.1. Cookie
Cookie 在规范中由用户代理的 Cookies § 用户代理要求 定义。
当 Cookie 在范围内且没有 HttpOnly
标志时,它是 脚本可见 的。这在处理模型中更正式地执行,该模型在适当的点参考 Cookies §
检索模型。
Cookie 还受某些大小限制。根据 Cookies § 存储模型:
注意: Cookie 属性值存储为 字节序列,而不是字符串。
2.2. Cookie 存储
Cookie 存储 在规范中由用户代理的 Cookies § 用户代理要求 定义。
当以下任一条件发生在 Cookie 存储时,执行 处理 Cookie 更改 的步骤。
2.3. 服务工作者的扩展
[Service-Workers] 定义了 服务工作者注册,本规范对此进行了扩展。
一个 服务工作者注册 具有一个关联的 Cookie 更改订阅列表,它是一个 列表; 每个成员是一个 Cookie 更改订阅。一个 Cookie 更改订阅 是 一个 元组,包含 名称 和 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 = "strict";
sameSite boolean =
partitioned false ; };dictionary {
CookieStoreDeleteOptions required USVString ;
name USVString ?=
domain null ;USVString = "/";
path boolean =
partitioned false ; };dictionary {
CookieListItem USVString ;
name USVString ;
value USVString ?;
domain USVString ;
path DOMHighResTimeStamp ?;
expires boolean ;
secure CookieSameSite ;
sameSite boolean ; };
partitioned typedef sequence <CookieListItem >;
CookieList
3.1. get()
方法
- cookie = await cookieStore .
get
(name)- cookie = await cookieStore .
get
(options) - cookie = await cookieStore .
-
返回一个 Promise,解析为给定 Cookie 名称(或其他选项)的第一个在范围内的 脚本可见值。 在服务工作者上下文中,默认路径为服务工作者注册范围的路径。 在文档中,默认路径为当前文档的路径,不受
replaceState()
或document.domain
的更改影响。
get(name)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
返回 p。
get(options)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
如果 options 为空,则返回 一个被拒绝的 Promise,原因是
TypeError
。 -
如果 options["
url
"] 存在,则运行以下步骤:-
令 parsed 为使用 settings 的 API 基础 URL 解析 options["
url
"] 的结果。 -
如果 this 的 关联全局对象(relevant global object)是
Window
对象,且 parsed 与 url 在 忽略片段(exclude fragments)设为 true 的情况下不相等,则返回 一个以TypeError
拒绝的 promise。 -
如果 parsed 的 来源 和 url 的 来源 不 相同, 则返回 一个被拒绝的 Promise,原因是
TypeError
。 -
将 url 设置为 parsed。
-
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
返回 p。
3.2. getAll()
方法
- cookies = await cookieStore .
getAll
(name)- cookies = await cookieStore .
getAll
(options) - cookies = await cookieStore .
-
返回一个 Promise,解析为给定 Cookie 名称(或其他选项)的所有在范围内的 脚本可见值。 在服务工作者上下文中,默认路径为服务工作者注册范围的路径。 在文档中,默认路径为当前文档的路径,不受
replaceState()
或document.domain
的更改影响。
getAll(name)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
令 list 为运行 查询 Cookies 的结果,参数为 url 和 name。
-
否则,解析 p 为 list。
-
-
返回 p。
getAll(options)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
如果 options["
url
"] 存在,则运行以下步骤:-
令 parsed 为使用 settings 的 API 基础 URL 解析 options["
url
"] 的结果。 -
如果 this 的 关联全局对象(relevant global object)是
Window
对象,且 parsed 与 url 在 排除片段(exclude fragments)设为 true 的情况下不相等,则返回 一个以TypeError
拒绝的 promise。 -
如果 parsed 的 来源 和 url 的 来源 不 相同, 则返回 一个被拒绝的 Promise,原因是
TypeError
。 -
将 url 设置为 parsed。
-
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
令 list 为运行 查询 Cookies 的结果,参数为 url 和 options["
name
"] (如果存在)。 -
否则,解析 p 为 list。
-
-
返回 p。
3.3. set()
方法
set(name, value)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
令 domain 为 null。
-
令 path 为 "/".
-
令 sameSite 为
strict
。 -
令 partitioned 为 false。
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
返回 p。
set(options)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
返回 p。
3.4. delete()
方法
delete(name)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
返回 p。
delete(options)
方法的步骤如下:
-
令 origin 为 settings 的 来源。
-
如果 origin 是 不透明来源,则返回 一个被拒绝的 Promise,原因是 "
SecurityError
"DOMException
。 -
令 url 为 settings 的 创建 URL。
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
返回 p。
4. CookieStoreManager
接口
CookieStoreManager
具有一个关联的 注册,它是一个 服务工作者注册。
CookieStoreManager
接口允许 服务工作者 订阅 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()
方法
subscribe(subscriptions)
方法的步骤如下:
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
令 subscription list 为 registration 的关联 Cookie 更改订阅列表。
-
对于 subscriptions 中的每个 entry,运行以下步骤:
-
令 name 为 entry["
name
"]。 -
令 url 为使用 settings 的 API 基础 URL 解析 entry["
url
"] 的结果。 -
如果 url 不以 registration 的 范围 URL 开头, 则 拒绝 p,原因是
TypeError
并中止这些步骤。 -
令 subscription 为 Cookie 更改订阅 (name, url)。
-
如果 subscription list 尚未 包含 subscription,则 追加 subscription 到 subscription list。
-
-
解析 p 为 undefined。
-
-
返回 p。
4.2.
getSubscriptions()
方法
- subscriptions = await registration . cookies .
getSubscriptions()
-
此方法返回一个 Promise,该 Promise 解析为为此服务工作者注册所创建的 Cookie 更改订阅列表。
getSubscriptions()
方法的步骤如下:
4.3.
unsubscribe()
方法
- await registration . cookies .
unsubscribe
(subscriptions) -
调用此方法将停止已注册的服务工作者接收先前订阅的事件。subscriptions 参数应以与传递给
subscribe()
或从getSubscriptions()
返回的形式相同的方式列出订阅。
unsubscribe(subscriptions)
方法的步骤如下:
-
令 p 为 一个新的 Promise。
-
并行运行以下步骤:
-
令 subscription list 为 registration 的关联 Cookie 更改订阅列表。
-
对于 subscriptions 中的每个 entry,运行以下步骤:
-
令 name 为 entry["
name
"]。 -
令 url 为使用 settings 的 API 基础 URL 解析 entry["
url
"] 的结果。 -
如果 url 不以 registration 的 范围 URL 开头, 则 拒绝 p,原因是
TypeError
并中止这些步骤。 -
令 subscription 为 Cookie 更改订阅 (name, url)。
-
-
解析 p 为 undefined。
-
-
返回 p。
4.4.
ServiceWorkerRegistration
接口
ServiceWorkerRegistration
接口被扩展以通过 CookieStoreManager
提供对 cookies
的访问,该接口提供订阅 Cookie 更改的功能。
[Exposed =(ServiceWorker ,Window )]partial interface ServiceWorkerRegistration { [SameObject ]readonly attribute CookieStoreManager cookies ; };
每个 ServiceWorkerRegistration
都有一个关联的 CookieStoreManager
对象。
CookieStoreManager
的 注册等于 ServiceWorkerRegistration
的 服务工作者注册。
cookies
getter 的步骤是返回 this 的关联 CookieStoreManager
对象。
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
changed
和 deleted
属性必须返回它们被初始化时的值。
5.2.
ExtendableCookieChangeEvent
接口
ExtendableCookieChangeEvent
在 ServiceWorkerGlobalScope
对象上分派,当发生任何与 Cookie 更改订阅列表
匹配的 脚本可见的 Cookie 更改时。
注意: ExtendableEvent
被用作所有服务工作者事件的祖先接口,以便在执行异步操作时保持工作者本身的存活。
[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
changed
和 deleted
属性必须返回它们被初始化时的值。
6. 全局接口
CookieStore
可通过全局对象中的属性访问,例如 Window
和 ServiceWorkerGlobalScope
。
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. 算法
注意: 这与 时间值在 [ECMAScript] 中的表示方式相同。
DOMHighResTimeStamp
millis,
令 dateTime 为从 1970 年 1 月 1 日 00:00:00 UTC 开始经过 millis 毫秒后的日期和时间
(假设每天恰好有 86,400,000 毫秒),
并返回一个 字节序列,该序列对应于根据 Cookies § Dates 的
cookie-date
表示形式的最接近值。
7.1. 查询 Cookies
要使用 url 和可选的 name 查询 Cookies,运行以下步骤:
-
执行 Cookies § 检索模型 中定义的步骤,以使用 url 作为 request-uri 计算“从给定的 Cookie 存储中生成的 cookie-string”。 忽略 cookie-string 本身,但在后续步骤中使用中间的 cookie-list。
在这些步骤中,cookie-string 是为“非 HTTP”API 生成的。
-
令 list 为一个新的 列表。
-
对于 cookie-list 中的每个 cookie,运行以下步骤:
-
断言:cookie 的 http-only-flag 为 false。
-
如果提供了 name,则运行以下步骤:
-
令 cookieName 为对 cookie 的 name 运行 UTF-8 无 BOM 解码 的结果。
-
如果 cookieName 不等于 name,则 继续。
-
-
令 item 为对 cookie 运行 创建 CookieListItem 的结果。
-
追加 item 到 list。
-
-
返回 list。
要从 cookie 创建一个 CookieListItem
,运行以下步骤:
-
令 name 为对 cookie 的 name 运行 UTF-8 无 BOM 解码 的结果。
-
令 value 为对 cookie 的 value 运行 UTF-8 无 BOM 解码 的结果。
-
令 domain 为对 cookie 的 domain 运行 UTF-8 无 BOM 解码 的结果。
-
令 path 为对 cookie 的 path 运行 UTF-8 无 BOM 解码 的结果。
-
令 expires 为 cookie 的 expiry-time(作为时间戳)。
-
令 secure 为 cookie 的 secure-only-flag。
-
根据 cookie 的 same-site-flag 进行切换:
-
令 partitioned 为一个布尔值,指示用户代理是否支持 Cookie 分区,并且 cookie 是否具有分区键。
-
返回 «[ "name" → name, "value" → value, "domain" → domain, "path" → path, "expires" → expires, "secure" → secure, "sameSite" → sameSite, "partitioned" → partitioned ]»
注意: cookie 的 creation-time、last-access-time、persistent-flag、host-only-flag 和 http-only-flag 属性不会暴露给脚本。
7.2. 设置 Cookie
要使用 url、name、value,可选的 expires、domain、path、sameSite 和 partitioned 设置 Cookie,运行以下步骤:
-
如果 name 或 value 包含 U+003B (;),或包含除 U+0009 TAB 外的任意 C0 控制字符,或 U+007F DELETE,则返回失败。
注意,目前还在讨论这些字符限制是否也应适用于 expires、domain、path 和 sameSite。[httpwg/http-extensions Issue #1593]
-
如果 name 包含 U+003D (=),则返回失败。
-
如果 name 的长度为 0:
-
令 encodedName 为 UTF-8 编码后的 name。
-
令 encodedValue 为 UTF-8 编码后的 value。
-
如果 encodedName 的字节序列的长度加上 encodedValue 的字节序列的长度大于最大 name/value 对长度,则返回失败。
-
令 host 为 url 的主机。
-
令 attributes 为一个新的列表。
-
如果 domain 不为 null,则执行以下步骤:
-
如果 path 不为 null:
-
否则,追加 `
Path
`/ U+002F (/) 到 attributes。 -
追加 `
Secure
`/`` 到 attributes。 -
根据 sameSite 的值,执行如下操作:
-
如果 partitioned 为 true,追加 `
Partitioned
`/`` 到 attributes。 -
按照 Cookies § 存储模型中“用户代理接收到 cookie”时的步骤执行, url 作为 request-uri, encodedName 作为 cookie-name, encodedValue 作为 cookie-value, attributes 作为 cookie-attribute-list。
就这些步骤而言,新创建的 cookie 被视为来自“非 HTTP”API。
-
返回成功。
注意:由于 [RFC6265BIS-14] 中的要求,存储 cookie 可能仍然会失败, 但这些步骤将视为已成功。
7.3. 删除 Cookie
要使用 url、name、domain、path 和 partitioned 删除 Cookie,运行以下步骤:
7.4. 处理更改
要 处理 Cookie 更改,运行以下步骤:
-
对于每个
Window
window,运行以下步骤:-
令 changes 为 url 的 可观察更改。
-
在全局任务队列中排队一个任务,使用 window,以 触发一个更改事件,事件名称为 "
change
",并将 changes 传递给 window 的CookieStore
。
-
对于每个 服务工作者注册 registration,运行以下步骤:
-
令 changes 为一个新的 集合。
-
对于 registration 的 可观察更改 中的每个 change,运行以下步骤:
-
令 cookie 为 change 的 Cookie。
-
对于 registration 的 Cookie 更改订阅列表 中的每个 subscription,运行以下步骤:
-
令 cookieName 为对 cookie 的 UTF-8 无 BOM 解码 的结果。
-
如果 cookieName 等于 subscription 的 名称,则将 change 添加到 changes 并中断循环。
-
-
如果 changes 为空,则继续。
-
令 changedList 和 deletedList 为运行 准备列表 的结果,使用 changes。
-
触发一个功能事件,事件名称为 "
cookiechange
",使用ExtendableCookieChangeEvent
,在 registration 上,具有以下属性:
-
可观察的更改 是针对 url 的 集合,包含对 Cookie 的 更改,这些更改满足 Cookies § 检索算法 第 1 步的要求,用于计算给定 Cookie 存储的 "cookie-string",其中 url 作为 request-uri,适用于 "非 HTTP" API。
Cookie 更改 是一个 Cookie 和一个类型(更改 或 删除):
要 触发更改事件,事件名称为 type,更改为 changes,目标为 target,运行以下步骤:
-
令 event 为使用
CookieChangeEvent
创建的 事件。 -
将 event 的
type
属性设置为 type。 -
将 event 的
bubbles
和cancelable
属性设置为 false。 -
令 changedList 和 deletedList 为运行 准备列表 的结果,使用 changes。
-
将 event 的
changed
属性设置为 changedList。 -
将 event 的
deleted
属性设置为 deletedList。 -
分派 event 到 target。
要从 changes 中 准备列表,运行以下步骤:
8. 安全注意事项
除了服务工作者上下文中的 Cookie 访问外,此 API 并不打算向 Web 暴露任何新功能。
8.1. 注意事项
尽管浏览器的 Cookie 实现正在朝着更好的安全性和更少的意外和易出错的默认值方向发展,但目前对 Cookie 数据安全性几乎没有保证。
-
非安全来源通常可以覆盖安全来源使用的 Cookie
-
超级域通常可以覆盖子域看到的 Cookie
-
跨站脚本攻击和其他脚本与头部注入攻击也可以用来伪造 Cookie
-
Cookie 读取操作(无论是来自脚本还是 Web 服务器)都不会指示 Cookie 的来源
-
浏览器有时会以令人惊讶且违反直觉的方式截断、转换或驱逐 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 时,该非 Secure
Cookie
将自动修改为 Secure
。
8.4. 意外行为
某些现有的 Cookie 行为(尤其是基于域而非来源的取向、非安全上下文 能够设置在 安全上下文 中可读的 Cookie,以及脚本能够设置在脚本上下文中不可读的 Cookie)从 Web 安全的角度来看可能非常令人意外。
其他意外行为记录在 Cookies § Introduction 中,例如,可以为超级域设置 Cookie(例如,app.example.com 可以为整个 example.com 域设置 Cookie),并且 Cookie 可以跨给定域名的所有端口号读取。
进一步复杂化的是主要浏览器在 Cookie 处理上的历史差异,尽管其中一些(例如端口号处理)现在比以前更一致。
8.5. 前缀
在可行的情况下,示例使用了 __Host-
和 __Secure-
名称前缀,这会导致某些当前浏览器禁止从 非安全上下文 覆盖,禁止没有 Secure
标志的覆盖,并且在 __Host-
的情况下,禁止具有显式 Domain
或非 '/' Path
属性的覆盖(有效地强制执行同源语义)。这些前缀在实现安全 Cookie
的浏览器中提供了重要的安全优势,并在其他浏览器中优雅降级(即,特殊语义可能不会在其他 Cookie API 中强制执行,但 Cookie 正常工作,异步 Cookie API 在写操作中强制执行安全语义)。不过,此
API 的一个主要目标是与现有 Cookie 的互操作性,因此也提供了一些未使用这些前缀的 Cookie 名称示例。
前缀规则也由此 API 在写操作中强制执行,但可能不会在同一浏览器的其他 API 中强制执行。因此,在这些规则被更广泛采用之前,不建议过于依赖它们的强制执行。
8.6. URL 范围
尽管服务工作者脚本今天无法直接访问 Cookie,但它已经可以使用受控渲染的范围内 HTML 和脚本资源,在服务工作者脚本的远程控制下注入 Cookie 监控代码。这意味着在服务工作者范围内访问 Cookie 在技术上已经可能,只是并不十分方便。
当服务工作者的范围比 /
更窄时,它仍然可以通过成功猜测/构造允许 IFRAME 的 404 页面 URL 并在其中运行脚本来读取其范围路径空间之外的路径范围
Cookie。同样的技术可以扩展到整个来源,但精心构建的网站(一个没有超出范围页面可被 IFRAME 的网站)实际上可以今天拒绝路径范围服务工作者的这种能力,而我不愿意在没有进一步讨论其影响的情况下移除这种限制。
8.7. Cookie 厌恶
为了减少开发人员的复杂性并消除对临时测试 Cookie 的需求,此异步 Cookie API 将明确拒绝在操作将被忽略时写入或删除 Cookie 的尝试。同样,它将明确拒绝在操作将忽略实际 Cookie 数据并模拟空 Cookie Jar 时读取 Cookie 的尝试。在这些上下文中尝试观察 Cookie 更改仍然“有效”,但不会调用回调,直到读取访问被允许(例如,由于站点权限更改)。
今天,在脚本启动的 Cookie 写入被禁止的上下文中写入 document.cookie
通常是无操作的。然而,许多 Cookie 写入脚本和框架总是写入一个测试 Cookie,然后检查其存在性以确定是否可以进行脚本启动的 Cookie 写入。
同样,今天在脚本启动的 Cookie 读取被禁止的上下文中读取 document.cookie
通常返回一个空字符串。然而,一个合作的 Web 服务器可以验证服务器启动的 Cookie 写入和读取是否有效,并将此报告给脚本(脚本仍然看到空字符串),脚本可以使用此信息推断脚本启动的 Cookie 读取被禁止。
9. 隐私注意事项
9.1. 清除 Cookie
本节为非规范性内容。
当用户清除某个来源的 Cookie 时,用户代理需要清除该来源的所有存储,包括该来源的服务工作者和 DOM 可访问的存储。这是为了防止网站在用户执行此操作后,通过持久存储恢复任何用户标识符。
10. 致谢
感谢 Benjamin Sittler,他为此 API 创建了初始提案。
特别感谢以下人员帮助完善此提案:
Adam Barth, Alex Russell, Andrea Marchesini, Andrew Williams, Anne van Kesteren, Ben Kelly, Craig Francis, Daniel Appelquist, Daniel Murphy, Domenic Denicola, Elliott Sprehn, Fagner Brack, Jake Archibald, Joel Weinberger, Kenneth Rohde Christiansen, Lukasz Olejnik, Marijn Kruisselbrink, 和 Mike West。
特别感谢 Tab Atkins, Jr. 创建并维护了 Bikeshed,这是用于创建本文档的规范编写工具,并感谢他提供的通用编写建议。