Cookie、Session、JWT
1. HTTP
HTTP 协议本身是无状态的:
- 无状态意味着每个 HTTP 请求都是独立的,服务器不会默认保留之前的请求信息。
- 例如,即使你连续发送两个请求(比如先登录,再访问个人主页),服务器不会自动知道这两个请求来自同一个用户。
虽然 HTTP 本身无状态,但实际应用(如网站登录、购物车)需要状态,你肯定不想在登录淘宝后,因为需要打开购物车而再次登录。
因此开发者使用额外技术实现状态的持久,其核心思想都是 存储。
2. Cookie(HTTP Cookie)
2.1 Cookie 解决了什么
在客户端发送第一次 HTTP 请求之后,服务器就检查请求是否合法(用户名和密码是否正确),请求合法后服务器会通过 Set-Cookie
头部向客户端(浏览器)发送 Cookie,之后客户端每次请求都携带这个 Cookie,这样以后的 HTTP 请求就不用重复验证了。
以需要登陆的场景为例,服务器会将客户端的账号密码加密并返回 Cookie,浏览器保存 Cookie,之后在后续 HTTP 请求中携带该 Cookie,服务器收到 Cookie 后会对 Cookie 解密得到账号和密码信息,并在数据库中比对。如果浏览器没有收到 Cookie,就会让客户端重新登陆。
2.2 Cookie 的问题
在 Cookie 策略中,Cookie 完全由客户端存储,服务器无状态。对于非敏感数据,比如用户语言偏好、主题设置等,Cookie 是一种非常简单且对服务器友好的策略。
但是对于一些敏感数据,例如用户的账号和密码信息,Cookie 是直接存储在客户端(浏览器)当中的,尽管服务器可能对其进行了加密处理,这依然是很不安全,只要电脑被黑,在 Cookie 中的重要信息就会被泄露。
其次,除了第一次 HTTP 请求,后续每一次请求都要携带 Cookie,但实际上多数请求并不需要验证 Cookie,这就很不灵活,还会造成大量浪费。
3. Session
3.1 Session 是如何解决 Cookie 的问题的
将(客户端)浏览器与服务器的通信视为 会话(Session),每个会话有一个唯一的 SessionID 来标识,服务器在第一次收到客户端的请求并验证通过后,会与客户端建立 Session,生成 SessionID 保存在服务器中,然后将 SessionID 通过 Set-Cookie
以 Cookie 的形式返回给客户端,并且包含这个 Cookie 的有效期等信息。
除了 Cookie,还有别的方法可以保存这个 SessionID,例如 URL 参数,隐藏域。但是 Cookie 已经被证明是这三种方式中最方便和最安全的。从安全的观点,如果不是全部也是绝大多数针对基于 Cookie 的会话管理机制的攻击对于 URL 或是隐藏域机制同样适用,但是反过来却不一定,这就让 Cookie 成为从安全考虑的最佳选择。
这个 SessionID 就起到了账号+密码的功能,之后客户端每次发送 HTTP 请求都会包含这个 SessionID 而无需包含账号和密码信息,服务器则验证这个 SessionID 是否存在且合法。
相较于传统的 Cookie,Cookie-Session 策略中,客户端不需要直接保存账号和密码信息,而是保存服务器生成的 SessionID,这样即使黑客得到了我们的 Cookie,也不能从中得到一些敏感信息。
3.2 解决了但没完全解决
一个问题是,如果黑客窃取了我们的 SessionID,那么它是否能仿冒我们的身份与服务器通信?
答案是可以的,这就是所谓的 会话劫持(Session Hijacking)。这是使用 Cookie 无法避免的问题,HTTP 协议是无状态的,为了维持状态,我们别无选择。
攻击方式:
- 网络嗅探:通过中间人攻击(如公共Wi-Fi)截获未加密的 HTTP 流量。
- XSS 攻击:通过注入恶意脚本窃取文档中的 Cookie(
document.cookie
)。 - 物理访问:直接获取用户设备或浏览器 Cookie 文件。
- 预测或伪造:弱 Session-ID 生成算法可能被破解。
如何防范 Session 劫持?
- 使用 HTTPS
- 加密整个通信过程,防止网络嗅探获取 Session-ID。
- 设置 Cookie 安全属性
Secure
:仅通过 HTTPS 传输 Cookie。HttpOnly
:禁止 JavaScript 访问 Cookie,防范 XSS。SameSite=Strict/Lax
:阻止跨站请求伪造(CSRF)。
- Session 管理增强
- 短期有效性:设置较短的 Session 过期时间,或定期更新 Session-ID。
- 绑定用户特征:Session 与用户 IP、User-Agent 等绑定(需权衡灵活性)。
- 服务端主动清理:用户登出时立即销毁 Session。
- 防御 XSS 和 CSRF
- 对用户输入严格过滤,避免 XSS 漏洞。
- 结合 CSRF Token 增强敏感操作验证。
- 监控与日志
- 记录异常 Session 使用行为(如多地登录、频繁更换 IP)。
其他替代方案:
- Token-Based 验证(如 JWT):
- 无状态,但需妥善处理 Token 存储和刷新逻辑。
- 仍需防范 XSS 和 Token 泄露风险。
- 多因素认证(MFA):
- 即使 Session-ID 泄露,攻击者仍需第二因素(如短信验证码)才能登录。
3.3 安全性之外呢
虽然 Cookie-Session 在安全性方便已经做好的比较好了,但这个策略在 拓展性 方面还不够理想。
这主要是因为服务器需要保存 SessionID。如果我们将服务器从一台拓展为多台以拓展服务器的访问能力(这在当前环境下这很常见了),不同服务器之间共享 SessionID 是一个很大的问题。
这是一个比较经典的分布式环境下如何处理共享数据的问题了。
由于客户端对服务器的访问往往不是固定的,由于负载均衡,访问速度和其它因素的影响,你可能经常在不同的服务器间进行访问,你肯定不希望在不同服务器间进行访问时需要重新进行登录吧,毕竟客户端是感知不到当前是在访问那台服务器的,我们只知道,有时候莫名其妙需要重新登录,因为此时我们访问的服务器发生了变化,所以客户端需要重新在当前服务器获取 SessionID。
如果我们选择在不同服务器之间共享 SessionID,但这显然不是一个很好的办法:
(1)性能瓶颈
- 内存占用高:每台服务器需保存全量 Session 数据,浪费资源。
- 同步延迟:Session 数据的变更(如用户权限更新)需实时同步到所有服务器,网络开销大,可能引发一致性问题。
- 例如:用户退出登录,服务器 A 删除了 Session,但服务器 B 未及时同步,导致用户仍能通过 B 访问。
(2)扩展性差
- 新增服务器成本高:每加入一个新节点,需从其他服务器全量复制 Session 数据,启动时间随集群规模增长而增加。
- 跨机房/地域问题:如果服务器分布在不同的地理位置,Session 同步的延迟和丢包率会显著上升。
(3)可靠性风险
- 单点故障:若某台服务器 Session 数据损坏,可能污染整个集群。
- 垃圾回收困难:分散的 Session 数据可能导致过期数据清理不及时(如用户退出后 Session 未被所有节点删除)。
你可能会想,那好办,将会话数据从服务器内存剥离,集中存储到高性能的共享存储中(如 Redis、Memcached),这样客户端请求依然通过服务器进行,我们也可以正常对服务器进行拓展,然后所有服务器都访问这个共享存储来管理会话数据。
不过,聪明的你可能已经发现问题了,如果所有服务器都依赖于这一个共享存储,如果该存储宕机,例如 Session 雪崩,就会导致全站不可用,一个解决方案是 Redis 哨兵/集群 + 本地降级策略。
但不管怎么解决,依然是面多加水,水多加面,没有从根本上解决问题。
4. JWT
4.1 JWT 是什么
JWT 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息(通常是身份验证或声明信息)。它以紧凑的、URL 安全的 JSON 对象形式表示,可以被签名(如使用 HMAC 或 RSA)以确保数据的完整性和真实性。
JWT 的核心理念是:将用户状态信息直接存储在客户端(如浏览器或移动端),而不是存储在服务器端(如 Session)。服务器只需验证 Token 的合法性,无需维护会话状态,从而实现无状态(Stateless)认证。
因此在 JWT 策略下,服务器不需要保存会话数据,避免了 Cookie-Session 策略在分布式环境下保存会话数据上的问题。
4.2 JWT 的结构
一个 JWT 通常由三部分组成,用 .
分隔:
Header.Payload.Signature
例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
这三个部分当中,Header
和 Payload
都是直接经过 Base64Url
编码得到的,这意味着它们可以被很容易的经过解码得到,但单单得到这些数据并没有太大的作用。JWT 的核心部分在于 signature,服务器根据 Header
和 Payload
,通过Header
中指定的加密算法生成 signature,返回给客户端。由于在加密时需要借助服务器的私钥,因此这是很难破解的。
其实也不必须是服务器的私钥,只要是只有服务器知道的数据,用这个数据来辅助加密即可,这个过程就类似于加盐。
下次接收到 Token 时只需要验证 signature 即可。验证通过后,直接信任 Payload
中的数据(如用户 ID、角色等)。
(1)Header(头部)
描述 Token 的 类型 和 签名算法,如:
{
"alg": "HS256", // 签名算法(如 HS256、RS256)
"typ": "JWT" // Token 类型
}
→ 经过 Base64Url 编码后形成第一部分。
(2)Payload(负载)
存放实际的数据(称为 Claims),例如用户 ID、角色、过期时间等。
{
"sub": "1234567890", // 用户标识(标准声明)
"name": "John Doe", // 自定义声明
"iat": 1516239022 // 签发时间(Issued At)
}
→ 经过 Base64Url 编码后形成第二部分。
Payload 的三种声明类型:
类型 | 说明 |
---|---|
Registered Claims | 预定义标准字段(如 iss 签发者、exp 过期时间、sub 主题)。 |
Public Claims | 公开的自定义字段(需避免冲突,建议使用命名空间如 com.example.name )。 |
Private Claims | 私有的自定义字段(用于业务数据,如 user_id )。 |
(3)Signature(签名)
用于验证 Token 是否被篡改,具体的就是 Header
和 Payload
两部分,计算方式为:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
服务器在收到客户端发送的 Token 之后:
- 解码
Header
和Payload
(Base64Url 解码)。 - 用预存的密钥(Secret Key)或公钥(Public Key)重新计算签名,与 JWT 中的
Signature
比对。
4.3 JWT 的工作原理
- 用户登录:客户端提交用户名/密码,服务器验证后生成 JWT 并返回。
- 客户端存储:浏览器将 JWT 存入
localStorage
或Cookie
(推荐使用HttpOnly
Cookie 防 XSS)。 - 后续请求:客户端在
Authorization
请求头携带 JWT(如Bearer <token>
)。 - 服务器验证:服务器检查签名是否有效,并解析 Payload 获取用户信息。
客户端 (Browser/App) 服务器 (API)
| |
|--- 用户名/密码 ------>|
| |
|<----- JWT ------------|
| |
|--- 请求 + JWT -------->|
| |
|<----- 数据 -----------|
其中,最核心的部分就是对 JWT 中 signature 的验证。
4.4 JWT 的问题
由于在 JWT 策略中,服务器不保存任何会话状态,因此服务器就没有能力主动让客户端下线或注销的,也即一旦服务器对某个客户端产生了 Token,这个 Token 就是在其过期之前一直可用的。
其次就是,我们前面提到过,JWT 的数据部分 Header
和 Payload
在传输过程中仅仅只是通过 Base64Url
进行了编码,实际上和明文没什么区别,这意味着这部分数据在传输过程中并不是安全的,因此我们不能在 Token 中包含敏感数据。
还有就是,Token 包含了三个字段,每个字段又包含了很多信息,因此它的占用是比较大的,在网络传输的过程中比 Cookie-Session 的方式占用更多网络资源。