从状态管理解读Session和JWT

1,059 阅读9分钟

本文从状态管理的角度对 Cookie 和 Session 进行分析,让读者对 Cookie 和 Session 有更全面的理解。本文介绍了 JWT、JWE、JWS 和 JWA 的核心特征及它们之间的关系,让读者对相关概念有一个简单而且准确的认识。

本文包含如下小知识点:

  • Cookie 和 Session 的命名由来
  • 如何自动删除 Cookie
  • Cookie 存储超过限制时会发生什么
  • 什么是第三方 Cookie
  • Cookie 相关规范的变更历史
  • Session ID 是如何生成的
  • JWT 是怎样融合 JWE 和 JWS 的
  • 如何区分 JWE 和 JWS
  • JWT 与 Session 的优缺点

HTTP 会话

在计算机科学中,会话(Session)指多个通信实体之间的一次临时和互动的信息交换。会话通常是有状态的,这意味着通信的实体中至少有一方需要保存当时的状态信息。

网络应用程序(Web Application)的客户端和服务端一般基于 HTTP 进行通信,之间的交互可以看成会话,即 HTTP 会话。众所周知,HTTP 是无状态的,即没有在协议层实现状态信息的存储和同步,那 HTTP 会话如何实现状态信息的存储和同步?

HTTP 状态管理机制

HTTP 状态管理机制(State Management Mechanism)定义了 HTTP 头字段 Cookie 和 Set-Cookie,以及如果通过这两个头字段去实现状态管理。HTTP 状态管理机制虽然使用了 HTTP 头字段,但并不属于 HTTP 协议范畴。它关注的是,使用 HTTP 协议的通信实体之间如何实现状态管理,所以个人认为它是 HTTP 之上的解决方案。

HTTP 状态管理机制的主逻辑其实比较简单。服务器通过 HTTP 响应头字段 Set-Cookie 把状态信息传送给用户代理(User agent,比如浏览器),用户代理把状态信息存起来,下次发送请求的时候自动把合法的状态信息通过请求头字段 Cookie 传送给服务器。由于 Unix 系统中有个类似功能的东西叫做 Magic cookie,发明这套机制的人就把这些状态信息叫做 Cookie。除了使用 HTTP 头字段,用户代理也会提供 non-HTTP 接口(比如 HTML 的 document.cookie)和 Cookie 管理界面来读写 Cookie。

// HTTP 头示例

== Server -> User Agent ==

Set-Cookie: STATE=31d4d96e407aad42

== User Agent -> Server ==

Cookie: STATE=31d4d96e407aad42

在 HTTP 头字段 Set-Cookie 中,除了可以设置 Cookie 的名称和值外,还可以设置 Cookie 属性,比如 Expires、Max-Age、Domain、Path、Secure 和 HttpOnly。这些属性用来控制用户代理对 Cookie 的存储和访问行为。这些属性都会被用户代理保存起来。除此之外,用户代理还会保存 creation-time、last-access-time 等信息。HTTP 头字段 Cookie 则不包含 Cookie 属性,因为这些属性对服务器没有意义,不需要传送给服务器。

一个小问题是,如何自动删除一个已有的 Cookie 呢?把 Cookie 的 Expires 设置成一个过去的时间,该 Cookie 就会被用户代理删除。

用户代理对 Cookie 的存储有容量限制,规范在这方面的要求是:

  • 每个 Cookie 最小 4 KB 大小(包括 Cookie 的名称、值和属性)
  • 每个域最少 50 个 Cookie
  • 总共最少 3000 个 Cookie

规范并不是在主动限制 Cookie 的大小和数量,相反,是在保障用户代理对 Cookie 存储的最小量的支持。如果 Cookie 的数量超过了这个限制会出现什么情况呢?会无法存储新的 Cookie 吗?不会的。用户代理会按照一定的优先级删除一些 Cookie。可以简单理解为删除最近访问时间最老的那些 Cookie。

什么是第三方 Cookie(Third-Party Cookies)呢?当渲染一个页面时,用户代理通常需要向其他服务器请求资源(比如广告资源)。这些第三方服务器就可以利用 Cookie 机制来追踪用户,即使用户没有直接访问第三方服务器。比如,网站甲和网站乙都接入了广告商 M,用户 A 依次访问了网站甲和网站乙,网站甲和网站乙都会请求广告商 M,广告商 M 在响应时,可以把网站甲和网站乙都写到自己域名下的 Cookie 中,这些 Cookie 都会在下次访问时回传给广告商 M,这样一来,广告商 M 就知道用户 A 在什么时候访问了哪些接入了广告商 M 的网站。

RFC 2109 默认关闭或禁止第三方 Cookie。但 Netscape 和 Internet Explorer 并没有遵从 RFC 2109 的这个建议。Netscape 便起草了新的规范 RFC 6265。RFC 6265 提出了 Cookie 规范版本的概念,引入了 HTTP 头字段 Cookie2 和 Set-Cookie2。然而,RFC 6265 很少被使用,后面被 RFC 6265 所取代。RFC 6265 废除了规范版本概念,不推荐用户代理对第三方 Cookie 做任何特殊处理。RFC 6265 成为了关于 Cookie 的最权威也是使用最广泛的规范。

由于各种历史原因,Cookie 采用明文存储和传输,且不对数据的完整性进行校验,保密性和完整性都比较弱。Cookie 一般用来存储状态信息,而状态信息一般属于敏感数据。这样一来,敏感数据就被暴露在一个不安全的环境中。那怎么办呢?有 2 种解决方案:Session 和 JWT。

Session

Cookie 不安全的一个原因是,用户代理上的数据容易被黑客截获。如果把信息存储在服务器上则会好很多。一般把存储在服务器上的用户数据叫做 Session。可以看到,Cookie 和 Session 都是指状态信息,因为存储的位置不同而有了不同的命名。

关于 Session 的命名,没有找到很官方的解释。我觉得可以这么理解。在 HTTP 上实现会话机制有两种方式,在客户端存储状态数据和在服务端存储状态数据。两种状态数据分别叫做 Client-side session 和 Server-side session。由于 Client-side session 一般叫做 Cookie,Server-side session 就可以简称为 Session。

Session 需要结合上面的 Cookie 机制才能完整地实现状态管理。一般在 Cookie 中存储一个 Session Id ,然后通过 Session Id 寻址对应的 Session 数据。那 Session Id 需要具备怎样的特性呢?首先,Session Id 作为一个 Id,应该具备唯一性。另外,Session Id 作为敏感数据,需要避免被黑客劫持,应该具备不可枚举性和不可预测性。Session Id 一般利用随机数、密钥和时间戳生成。

sessionId = generate(random, secret, timestamp)

JWT

上面讲过,Cookie 的弱保密性和弱完整性会引发安全问题。如果对 Cookie 中的数据进行加密和完整性校验,安全性会不会大幅度提升?JWT(JSON Web Token)就是这样一种解决方案,可以用来解决 Cookie 的安全问题。

JWT 的发音与英文单词 jot 相同。JWT 是对两个实体之间传输的声明(Claim)的一种紧凑的和 URL 安全的表示方法。在 JWT 中,敏感信息被抽象成 Claims。Claim 是指关于某一主题的一条信息(A piece of information asserted about a subject)。一个 Claim 包含名称和值。多个 Claims 用 JSON 对象的形式表示。JWT 还包括 Header(也用 JSON 对象的形式表示),用来定义一些操作类型数据,给 JWT 的反序列化提供必要信息。JWT 有 JWE(JSON Web Encryption) 和 JWS(JSON Web Signature)两种表示方法。JWT 的 Claim 支持嵌套,即一个 Claim 的值可以是一个 JWT 形式的字符串或 JSON 对象,使得一个 JWT 能同时包含 JWS 和 JWE,从而兼具 JWS 和 JWE 的好处。一般的混合做法是,在最外层使用 JWS,在 Claim 中嵌套 JWE。JWS 和 JWE 都支持序列化和反序列化,所以 JWT 也支持序列化和反序列化。

// JWT Claims 示例
{
  "iss":"joe",
  "exp":1300819380,
  "http://example.com/is_root":true
}

JWE

JWE 定义了对 JSON 数据进行加密的表示方法,支持紧凑型和 JOSN 型两种序列化及反序列化方式。紧凑型序列化方式为:

BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)

其中,JWE Ciphertext 是对 Plaintext 进行加密得到的。JWT 中的 Claims 可以作为这里的 Plaintext。

一个紧凑型 JWE 示例:

eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.
OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe
ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb
Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV
mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8
1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi
6UklfCpIMfIjf7iGdXKHzg.
48V1_ALb6US04U3b.
5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji
SdiwkIr3ajwQzaBtQD_A.
XFBoMYUZodetZdvTiFvSkQ

可以看到,一个 JWE 字符串一般有 5 段。

JOSN 型序列化方式与之类似,详情参见 JSON Serialization Syntax

JWE 不包含加密算法的细节。JWE 使用的加密算法的具体定义写在 JWA(JSON Web Algorithms) 文档中。

JWS

JWS 定义了对 JSON 数据进行签名的表示方法。支持紧凑型和 JOSN 型两种序列化和反序列化方式。紧凑型序列化方式为:

BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload) || '.' ||
BASE64URL(JWS Signature)

其中,JWT 的 Claims 可以作为这里的 JWS Payload,JWS Signature 对前两部分数据进行签名计算得到签名字符串,可以保证数据的完整性和生成方身份的可靠性。

一个紧凑型 JWS 示例:

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

可以看到,一个 JWS 字符串一般包含 3 段。因此,可以通过字符串的段数来简单区分 JWE 和 JWS。

JOSN 型序列化方式与之类似,详情参见 JSON Serialization

JWT 与 Cookie 的结合方式

服务器把状态信息通过 JWT 序列化转化成 Token(JWT Token),通过 HTTP 响应头字段 Set-Cookie 传输给用户代理,用户代理通过 HTTP 请求头字段 Cookie 把 JWT Token 传回给服务器,服务器使用 JWT 反序列化得到状态信息。

Session VS JWT

JWT 相对 Session 的优点:

  • 节省了服务器的存储空间
  • 规避了分布式系统中的状态数据的存储同步问题

JWT 相对 Session 的缺点:

  • 增加 Cookie 的体积
  • 耗费了服务器的算力,占用了服务器 CPU 资源

加密和解密是比较耗费 CPU 的计算资源的。大家平时接触比较多的是 HTTPS 协议中数据的加密和解密。SSL 卸载(SSL Offloading)可以降低业务服务器的对 HTTPS 数据的加密和解密负担。

参考文献