别再混淆Cookie、Session和Token!深入分布式登录与SSO设计。

74 阅读14分钟

别再混淆Cookie、Session和Token!深入分布式登录与SSO设计。

关于这个技术点,是很容易混淆而且可能开发个几年都是稀里糊涂的,但作为开发者的我们是非常有必要深入理解的。

这篇文章,星星将带你深入理解单点登录原理,并深入讲解那些把人头都给弄晕了的Cookie、Session、Token,以及它们之间的演进,最主要就是学这个演进,这是非常值得去理解的。

我将会从下面这些点依次展开。

1.单点登录到底是啥?

2.Cookie、Session、Token又分别是啥?它们是怎么从HTTP一步步演进过来的呢?有啥优缺点?

3.老是说状态状态的,这个“状态”到底是啥?

那接下来,我将会由浅到深依次展开。

1.单点登录(SSO)到底是啥?

核心定义(仁者见仁,智者见智哈,技术没有标准答案,实践出真知):在多个相互信任的分布式应用系统中,用户只需要登录一次,就可以访问所有其他受信任的应用。

那为什么分布式项目需要单点登录呢?

在没有SSO的分布式项目中,每个应用都有自己的登录模块,这会带来非常糟糕的用户体验和安全隐患:

  1. 用户体验差:用户需要记住多套用户名和密码,访问不同应用时需要反复登录。
  2. 安全性难以保障:用户可能会因为记不住而使用简单密码或在多个系统使用相同密码,增加安全风险。同时,登录状态分散,登出操作繁琐,容易留下安全隐患。
  3. 运维成本高:每个系统都需要独立实现和维护一套认证逻辑,代码重复,修改密码策略或用户管理时会非常麻烦。

SSO就是为了解决这些问题而生的。

那说完SSO是啥,就不得不介绍如何实现了。

2.Cookie、Session、Token又分别是啥?它们是怎么从HTTP一步步演进过来的呢?有啥优缺点?

这是一个演进过程,主线是:如何在不安全的HTTP协议上,安全、高效地管理用户状态,并适应日益复杂的应用架构。

HTTP阶段:

这是最简单、最原始的认证方式,由HTTP协议本身定义。

  • 工作原理
    1. 客户端访问受保护资源。
    2. 服务器返回401 Unauthorized,并在WWW-Authenticate头部指定认证方式为Basic
    3. 客户端将用户名和密码用冒号连接,进行Base64编码(注意:仅是编码,非加密),然后在Authorization头部发送给服务器。例如:Authorization: Basic dXNlcjpwYXNz
    4. 服务器解码验证,通过则返回资源。
  • 优点
    • 极其简单:协议层面支持,无需复杂逻辑。
  • 缺点
    • 安全性极差:Base64编码可轻松解码,等同于明文传输密码。必须配合HTTPS使用。
    • 无状态性差:客户端每次请求都必须携带凭证,用户体验糟糕。
    • 客户端控制力为零:无法主动登出,除非关闭浏览器或等待服务器拒绝。
  • 本质:这是一种 “凭证随请求携带” 的模型,简单粗暴,且无状态,但完全不适用于现代Web应用。

我觉得你在看完这个缺点的时候,心里就已经有想法了,”这HTTP肯定不能去做我们的登录校验的呀!谁用谁xx“

所以cookie应运而生了。

Cookie阶段:

Cookie 是一个由服务器发送到用户浏览器并保存在本地的一小块数据。它是 HTTP 协议的一个组成部分,是客户端的存储机制

Cookie 的工作原理(服务器视角)

  1. 首次请求(无 Cookie): 浏览器第一次访问网站。

  2. 服务器响应(设置 Cookie): 服务器在处理请求后,在 HTTP 响应头中通过 Set-Cookie 字段,将一个键值对(如 user_id=123)发送给浏览器。

    text

    HTTP/1.1 200 OK
    Content-Type: text/html
    Set-Cookie: user_id=123; Path=/
    
  3. 浏览器存储: 浏览器收到这个指令后,会将这个 user_id=123 的 Cookie 保存在本地。

  4. 后续请求(携带 Cookie): 此后,浏览器向同一域名发出的每一个 HTTP 请求,都会自动在请求头中通过 Cookie 字段,将之前存储的所有相关 Cookie 发送给服务器。

    text

    GET /profile HTTP/1.1
    Host: example.com
    Cookie: user_id=123
    
  5. 服务器识别: 服务器从 Cookie 头中读取到 user_id=123,就知道这个请求来自用户 123。

Cookie 的优缺点

  • 优点:
    • 简单自动: 由浏览器自动管理,发送和携带无需前端 JavaScript 干预。
    • 实现了初步的状态保持: 让服务器有能力区分不同用户。
  • 缺点:
    • 极不安全:
      • 明文传输: Cookie 内容在请求和响应中直接可见,容易被拦截窃听。
      • 客户端可篡改: 黑客可以直接在浏览器中修改 Cookie 的值。如果把用户权限 level=admin 存在 Cookie 里,用户完全可以把自己改成 level=super_admin,服务器无法信任。
    • 容量限制: 每个域名下的 Cookie 有数量和大小限制(通常 4KB 左右)。

此时的核心矛盾: Cookie 解决了“识别用户”的问题,但无法解决“信任用户”的问题。服务器需要一个安全的地方来存储敏感信息。

你发现了Cookie的缺点,因为信息没存在服务端,明文存储会带来极大的安全问题,所以Session来了。

Session阶段:

为了解决 Cookie 的安全性信任问题,Session 机制被设计出来。

什么是 Session?

Session(会话)是服务器端的一种机制,用于在服务器上存储特定用户的状态信息。它本质上是服务器内存(或数据库、文件)中的一块数据存储区域

Session 的巧妙之处在于,它利用安全的 Cookie 来传递一个“钥匙”

  1. 登录请求: 用户提交用户名和密码。
  2. 创建 Session: 服务器验证凭证后,在服务器内存中创建一个 Session 对象,为其分配一个全局唯一的 Session ID,并在这个对象中存储用户的敏感信息(如用户ID、登录名、角色等)。
  3. 关联与响应: 服务器通过 HTTP 响应头的 Set-Cookie 字段,将 这个 Session ID(而且仅仅是ID) 发送给浏览器。
  4. 携带“钥匙”: 浏览器保存这个名为 JSESSIONID 的 Cookie,并在后续请求中自动携带它。
  5. 验证与信任: 服务器收到请求后,解析出 JSESSIONID=abcDeF123456,然后用这个 ID 去服务器的 Session 存储中查找对应的 Session 对象。找到了,就认为用户已登录,并且可以安全地从 Session 对象中读取用户信息。

分工明确:

  • Cookie 只负责安全地运输“钥匙”(Session ID)。
  • Session 负责在服务器保险柜里保管“真实财产”(用户数据)。

Session 的优缺点

  • 优点:
    • 安全: 敏感数据存储在服务器,客户端无法篡改。
    • 可信: 服务器完全掌控会话数据,可以随时让其失效(如登出)。
    • 减轻客户端负担: 客户端只需保存一个简单的 ID。
  • 缺点:
    • 服务器负担重: 每个活跃用户都会占用服务器内存。用户量巨大时,对服务器是巨大压力。
    • 扩展性差(分布式环境下的致命缺陷):
      • 在集群部署中,用户第一次请求落在 服务器A 并创建了 Session。
      • 第二次请求通过负载均衡可能落到 服务器B,而 服务器B 的内存中没有这个 Session,导致用户需要重新登录。
      • 解决方案(Session 复制、持久化到集中式 Redis 等)都增加了架构的复杂度和延迟。

此时的核心矛盾: Session 解决了信任问题,但引入了服务器状态,导致了扩展性瓶颈,无法适应云原生和分布式架构。

这时候你看到了Session的缺点,分布式架构下,不是凉凉了吗?这时候Token来了,这是一次“革命兴起”

Token阶段:

为了彻底解决 Session 的扩展性问题,并更好地适应移动端和 API 经济,Token 方案(尤其是 JWT)成为主流。

什么是 Token(以 JWT 为例)?

Token(令牌)是一个包含自身信息、经过数字签名的字符串。它是 “去中心化”的凭证,其核心是自包含可验证

Token 的工作原理

  1. 登录请求: 用户提交用户名和密码。
  2. 生成 Token: 服务器验证凭证后,不再创建 Session,而是使用一个密钥,生成一个 JWT。这个 JWT 的载荷(Payload)部分包含了用户身份信息(如用户ID、角色),并附有签名。
  3. 返回 Token: 服务器将 JWT 返回给客户端(通常通过响应体,而不是 Set-Cookie)。客户端将其保存起来(如 LocalStorage)。
  4. 携带 Token: 客户端在后续请求中,手动地在 HTTP 请求头的 Authorization 字段中携带 Token。例如:Authorization: Bearer eyJhbGci...
  5. 验证 Token: 服务器收到请求后:
    • 使用相同的密钥对 JWT 的签名进行验证。
    • 如果签名有效,证明此 Token 是自家发行的,且内容未被篡改。
    • 服务器便直接信任 Token 载荷(Payload)中的用户信息,无需去任何数据库查询会话状态。

Token 的优缺点

  • 优点:
    • 无状态与极致扩展性: 服务器不需要存储任何会话信息。Token 自身就是全部。这使得水平扩展变得轻而易举,任何服务器实例都能独立验证 Token。
    • 跨域与多端友好: 不依赖 Cookie,完美适配前后端分离、移动 App、第三方 API 授权等场景。
    • 自包含: 减少了查询数据库的次数。
  • 缺点:
    • 无法主动失效: Token 在过期前一直有效。实现“登出”需要额外手段(如维护一个黑名单,但这又引入了状态)。
    • 安全性依赖客户端存储: 存储在 LocalStorage 有 XSS 风险。
    • 带宽占用稍大。

你看到了token,是不是已经很牛了,但token存在的缺点无法主动失效确实是一个很头疼的问题,那有没有解决思路呢?哎,所以token+Redis这种方案又又又来了。

Token+Redis方案:

为解决纯 Token(如JWT)的致命短板,所以它来了。

  1. 无法主动失效(登出问题):这是JWT在企业内部系统中最无法忍受的缺点。想象一下,员工离职后,其Token在有效期内依然可以访问系统,这是巨大的安全漏洞。
  2. 缺乏实时控制能力:无法实时冻结某个用户、无法强制用户重新认证、无法动态更新用户权限(需等到Token过期)。
  3. 黑名单的笨重:维护一个JWT黑名单虽然可行,但本质上是在重建一个「失效Token列表」,这与「无状态」的初衷背道而驰,而且列表会不断膨胀,直到Token过期。

核心思想:“中心化管控”与“无状态验证”的折衷

此方案不再追求极致的无状态,而是引入一个轻量级的中心化控制点(Redis),在保留Token大部分优点的同时,重新获得对会话的精细控制权。

流程是怎么样的呢?

  1. 登录与令牌签发
    • 用户登录,服务器验证身份。
    • 服务器生成一个Token(可以是JWT,也可以是一个不透明的随机字符串)。
    • 关键操作: 服务器将这条Token作为Key,将对应的用户信息(如userId, role等)作为Value,存入Redis,并设置一个过期时间(如2小时)。
  2. 令牌校验(与纯JWT的核心区别)
    • 客户端携带Token访问接口。
    • 服务器收到Token后,不再仅仅验证JWT签名,而是必须多做一个动作:去Redis中查询此Token是否存在。
    • 只有在「Redis中存在该Token」且「JWT签名有效(如果是JWT的话)」 这两个条件同时满足时,认证才算通过。
  3. 主动登出与安全管控
    • 登出: 用户点击登出,服务器直接从Redis中删除(DEL)对应的Token。此后,即使用户持有Token,校验时也会因Redis中找不到而被拒绝。
    • 强制下线: 管理员可以随时从Redis中删除某个用户的Token,实现全局强制下线。
    • 权限实时更新: 用户权限变更时,可以更新Redis中该Token对应的用户信息,下次请求即刻生效。

优缺点

  • 优点:
    • 完美的可控性: 实现了灵活的会话管理,解决了JWT无法主动失效的痛点。
    • 保持良好扩展性: 虽然引入了Redis这个中心节点,但Redis本身是高性能、高可用的内存数据库,集群能力极强,不会成为瓶颈。
    • 安全性高: 提供了快速响应安全事件(如盗号、员工离职)的能力。
    • 支持信息富化: Redis中存储的Value可以比JWT的Payload更丰富、更敏感,且服务端可随时修改。
  • 缺点:
    • 牺牲了部分无状态性: 服务端(Redis)需要存储Token状态,每次请求都需要一次Redis查询,相比纯JWT的密码学验证,多了一次网络IO
    • 架构复杂度增加: 引入了对Redis的依赖,需要保证Redis的高可用,否则整个认证系统会瘫痪。

所以Redis加Token这种思路就是牺牲一部分状态,来弥补纯Token带来的缺点。是可以接收的。

3.老是说状态状态的,这个“状态”到底是啥?

关于这个状态,我之前在某站上回答过一个评论,就是讲这个状态的。

我建议我的这段话你可以深刻理解一下,因为这相当于把登录校验整个演进流程给串起来了。

“我觉得这个状态你可以这么理解,状态的有无取决于服务端是否存了校验信息也就是用户的身份信息啊等等,那么对于像传统的http可以被叫做无状态,因为它没法存储用户信息,每一次请求都是独立的,没法校验,所以我们有了更好的方法,把信息放在cookie中,这里对于服务端来讲也是无状态的,因为信息没存在服务端,因为一些致命的缺点比如明文存储的不安全性,所以我们引入session,让服务端来存储session ,那么此时服务端就是有状态的了,服务端存放了用户信息,不交给客户端存放了,只是给客户端一个sessionid,那么客户端每一次请求打来,都会那这个id去找对应的session,这样就可以进行校验,但是又有一个致命的缺点就是,在集群模式下是没法确保每一次请求都打到应该去进行校验的那个“session”上的,这也很好理解,所以我们用了token,这个token就完全不用服务端来存储了,这时候又可以说服务端无状态了,而是用密文方式传给客户端,客户端下一次来就带着这个token然后服务端解密就ok了,但是还有一个问题就是token是没法直接被干掉的,只有过期时间,这个问题就很简单了我就不说了,所以说后来就用了redis+token的方式嘛,token你还是正常加密给客户端,但是存在redis中了,下一次请求下来服务端会去redis里面比对,比如说redis里面如果存了token黑名单,那么一旦这个token在黑名单里,就不放行。所以总的来说,我个人的理解就是,状态是相对服务端而言的,有状态因为一些原因比如集群,分布式下而缺点暴露出来,所以这时候我们就用一点点状态,也就是放redis里面,但这又不是纯粹的服务端状态”

今天的内容到这里就结束了,如果对你有帮助的话,可以点个小星星,关注一下,祝你事业有成,我们明天再见!