JWT 与 Cookie/Session:登录凭证的两种形态

2 阅读7分钟

前言

做 jwt-demo 登录鉴权练习时,我在 readme 里写下的第一行笔记是:HTTP 是无状态的 Stateless,用户身份?你是谁?——这就是整个鉴权问题的起点。服务器每次收到请求都不记得你,必须靠"凭证"自证身份。这篇把这条线讲透:JWT 是什么、签名怎么算、cookie 到底扮演什么角色、为什么现代架构偏爱 JWT(以及它不完美在哪里)

一、身份问题的由来

HTTP 是无状态协议——一次请求结束后,服务器就"忘"了你。于是每次请求,客户端都得向服务器证明"我是谁"。证明的方式分两条路线:

  • 路线 A:服务器记住你。 登录后服务器建一个"会话档案",发给你一把钥匙,你每次带钥匙来开档案。→ 这就是 Cookie + Session
  • 路线 B:你自己带身份。 登录后服务器签发一张"身份卡",卡上写着你是谁,你每次出示卡片。→ 这就是 Token / JWT

两条路线的分水岭只有一个问题:身份信息存在哪? 存在服务器 = 有状态;写在客户端凭证里 = 无状态。后面的所有优劣,都从这一点派生。

二、JWT 是什么:一段三段凭证

JWT(JSON Web Token) 是一串用 . 分成三段的字符串,把"你是谁"的 JSON 身份信息签进凭证里。长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3ODE3MjM0NTZ9.Xy9aQ8vD7...
内容说明
Header{alg, typ}声明签名算法(如 HS256)与类型(JWT)
Payload{user, role, exp, iat...}身份信息 + 过期时间等注册声明
Signature一串哈希对前两段做的签名,防篡改

对应你两个动作——sign 和 verify

  • sign:身份 JSON 对象 → 签名 → 生成 token 颁发给登录者
  • verify:客户端带回的 token → 验签 + 检查过期 → 还原成身份对象
// 后端(模拟在 mock/user.js 里)
const token = jwt.sign(
  { user: body.username, role: 'admin' },  // 身份信息
  secret,                                   // 签名密钥("盐")
  { expiresIn: 86400 }                      // 24 小时过期
);
// 下次请求带回来
const decoded = jwt.verify(token, secret);  // 验签通过 → {user, role}

关键认知:签名 ≠ 加密

Header 和 Payload 只是 Base64 编码,不是加密——任何人拿到 token 都能解码看到内容(user: admin 一览无余)。所以敏感信息(密码等)绝不能放进 payload。Signature 的作用只有一件事:防篡改——内容被改一个字节,签名就对不上,verify 直接抛错。

加粗记住:JWT 是无状态的——身份就在 token 里,服务器不用存任何会话记录。

三、签名到底怎么算的

第三段签名不是魔法,就是一步哈希,任何人都能自己复现:

Signature = base64url( HMAC-SHA256( Header + "." + Payload, secret ) )

用 Node 的 crypto 几行就能算出来,结果和 jwt.sign 完全一致。它证明了三点:

  1. 它是哈希,不是加密——只保证"没被改过",不保证"看不到"
  2. 改任何一个字节 → 签名对不上——这就是防篡改的原理
  3. 只有持有同一把 secret 的人才能签发合法签名——所以 secret 是 JWT 的安全命门,泄露 = 全线失守

四、Cookie 的本质:不是身份证明,是运输机制

很多人(包括从前的我)把 cookie 误当成与 JWT 并列的"另一种身份证明方式"——这是最大的认知误区。Cookie 不是身份证明,它是"浏览器自动携带数据"的机制。

用快递员比喻:浏览器是快递员,服务器公司给快递员发了一张工牌,规定每次进楼自动出示。这张工牌就是 cookie。但——工牌上印什么,才决定你是谁

工牌上印的内容保安(服务器)怎么做身份方案
一个编号拿编号去档案室查档案Cookie + Session
姓名/部门/照片直接看内容,验防伪Cookie 传 JWT
不挂工牌,掏防伪卡看防伪卡Header 传 JWT(你项目现在这样)

Cookie 只是运输工具,身份证是 sessionId 或 JWT——这层才是"两种身份证明方式"的真正并列对象。

身份证明方式 = sessionId  vs  JWT        ← 这层才是"证明"
运输载体    = cookie    vs  Header      ← cookie 在这层

四行 Set-Cookie 的用途差别,一目了然:

Set-Cookie: language=zh-CN;        // 记偏好
Set-Cookie: cart=3,2,5;            // 记购物车
Set-Cookie: sessionId=abc123;      // 记会话钥匙
Set-Cookie: token=eyJxxx.yyy.zzz;  // 记 JWT 凭证

只有后两行和身份有关。"cookie 请求每次都会带上 sessionId"——这句话恰好点破关键:cookie 带的是那把钥匙(sessionId),身份是服务器靠钥匙查出来的 session 对象。

五、Cookie+Session vs JWT:优劣全对比

维度Cookie + SessionJWT
状态存储服务端(内存/Redis),有状态客户端 token 里,无状态
分布式/多服务器❌ session 不在别的机器上,要 session 共享/粘性会话✅ 任何服务器同一密钥即可验签,天然水平扩展
主动失效/踢人✅ 删 session 立刻下线❌ 签出去就管不住,只能等过期或黑名单
移动端 APP❌ 无浏览器,无 cookie✅ header 随便带
前后端分离/跨域❌ cookie 受同源/跨域限制✅ 无跨域包袱
CSRF 攻击❌ cookie 自动携带,需额外防护✅ header 手动携带,天然免疫
体积✅ 一个 id,极小❌ 含 payload,每请求都带
信息暴露✅ 内容在服务端❌ payload 明文可解码

一句话总结这场对决:session 是"钥匙 + 保险柜"(能锁能开,但柜子只在固定地点),JWT 是"自带信息的 ID 卡"(随处可验,但签出去就管不住)。

六、为什么"大都"用 JWT?——场景变了,不是 JWT 更高级

"现在大多选 JWT"不是因为它技术上更优,而是技术架构变了

  1. 前后端分离:React/Vue 独立部署,API 跨域、多后端,cookie 的自动携带和同源策略成了包袱
  2. 移动端爆发:APP 里没有 cookie 机制,token 是通用方案
  3. 微服务化:服务一多,session 共享成本高,无状态 JWT 让每个服务独立验签
  4. 第三方登录/OAuth:token 模式成了行业标准

但**"大都"要打个折**:银行、电商等对安全要求高的系统反而坚持 session——因为能主动踢人、吊销会话;而 JWT 一旦泄露,在过期前谁都拦截不了。JWT 的三个硬伤:无法主动失效、payload 明文、密钥风险。

七、现在的最佳实践:其实是混合

单极是刻板的。现代方案往往是组合拳:

登录 → 签发 JWT
     → 存进 httpOnly cookie    ← 防 XSS 偷 token(JS 读不到)
     → 加 CSRF token            ← 补 cookie 自动携带的漏洞
     → JWT 有效期 15 分钟       ← 配合 refresh token 无感续签
     → 用户改密码 → 刷新密钥版本,旧 token 立即失效

这样各取所长:cookie 解决了"JWT 被 JS 偷"(httpOnly),JWT 解决了"session 不能分布式"(无状态)。 这也就是为什么"cookie 装 JWT"才是比"localStorage + header"更正统的姿势——尽管教程里后者更常见。

小结

  • HTTP 无状态 → 客户端必须自证身份,两条路线:服务器记住你(session)/ 你自带身份(token)
  • JWT 三段:Header.Payload.Signaturesign 签发、verify 验签
  • 签名 ≠ 加密:payload 明文可解码,Signature 只防篡改
  • Cookie 是运输机制,不是身份证明:装 sessionId 是 Cookie+Session,装 JWT 是 cookie 传 JWT,装进 header 是你项目的方案
  • session 有状态可踢人、JWT 无状态可扩展,优劣全由"状态存哪"派生
  • 现代架构(前后端分离/移动端/微服务)偏 JWT,但最佳实践是 httpOnly cookie 装 JWT + 短时效 + refresh token 的混合方案

一句话:JWT 把身份从"服务器档案"搬进了"客户端凭证",用签名换来了无状态和可扩展,代价是放弃了主动失效;Cookie 只是负责搬运凭证的快递车。