Skip to content
crypto2026-07-025 分钟阅读

每个 Web 应用都要做认证,而认证机制的选择往往被简化为"JWT 还是 Session"。这个决定比看起来重要。选错可能导致:无法即时撤销被盗 token、敏感数据意外暴露、微服务间鉴权复杂度爆炸,或者简单的 Web 应用被塞进不适合的无状态架构。

这篇指南拆解两者的工作原理、安全特性和适用场景,帮你避开常见陷阱。

Session Cookie:服务端记忆

Session 是经典的认证方式。流程:

  1. 用户登录,服务端验证凭据
  2. 服务端创建一个 session 对象,存到内存、Redis 或数据库
  3. 服务端把 session ID(一个随机字符串)通过 Set-Cookie 发给浏览器
  4. 后续请求自动带上这个 cookie,服务端用 ID 查 session 存储
  5. 登出时,服务端删除 session 对象
// Express + express-session 的典型用法
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true, sameSite: 'lax' }
}));

app.post('/login', (req, res) => {
  // 验证用户...
  req.session.userId = user.id;
  res.send('logged in');
});

app.post('/logout', (req, res) => {
  req.session.destroy(() => res.send('logged out'));
});

关键特性:

  • 服务端状态:每个活跃 session 占用存储(内存或外部存储)
  • 可即时撤销:从存储里删掉就立即失效,无需等待过期
  • Cookie 自动管理:浏览器处理发送,跨域要配置 SameSite
  • 数据不暴露给客户端:cookie 里只是个 ID,业务数据留在服务端

JWT:自包含 token

JWT(JSON Web Token)把认证状态编码在 token 本身里。结构是三段用 . 分隔的 Base64URL:

header.payload.signature
  • header:算法类型(如 {"alg":"HS256","typ":"JWT"}
  • payload:声明(claims),如用户 ID、角色、过期时间
  • signature:用密钥对 header.payload 的签名,防止篡改
// 生成 JWT
const jwt = require('jsonwebtoken');
const token = jwt.sign(
  { userId: 42, role: 'admin' },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);

// 验证 JWT
const payload = jwt.verify(token, process.env.JWT_SECRET);

关键特性:

  • 无状态:服务端不需要存储 token,签名验证就足够
  • 自包含:payload 里有用户信息,不用每次查库
  • 跨域友好:放在 Authorization: Bearer ... header 里,不受 cookie 跨域限制
  • 难以撤销:在过期前,签发的 token 一直有效

核心权衡

两者的设计哲学是相反的。Session 把信任放在服务端,JWT 把信任放在 token 里。这导致一系列不同的取舍:

| 维度 | Session Cookie | JWT | |------|----------------|-----| | 状态 | 服务端有状态 | 完全无状态 | | 撤销 | 立即生效 | 需要黑名单或短 TTL | | 跨服务 | 共享 session 存储 | 验签就能用 | | 跨域 | 需要 cookie 配置 | header 即可 | | 大小 | cookie ~20 字节 | token ~500 字节 | | 安全默认值 | 较高(HttpOnly cookie) | 取决于实现 | | 数据保密 | 服务端私有 | payload 客户端可读 |

最后一行是最容易被忽略的陷阱。

JWT 的常见安全陷阱

1. payload 不是加密的,只是 Base64 编码

任何人拿到 JWT 都能解出 payload——把它粘进 JWT 解码器,每一条声明都一览无余。把敏感信息(密码、API key、个人信息)放进 payload 等同于明文泄露:

# 任何 JWT 都能这样解(无需密钥)
echo "eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ" | base64 -d
# {"userId":42,"role":"admin"}

要保密,用加密的 JWE(JSON Web Encryption),或者干脆别把敏感数据放进 JWT。

2. alg: none 攻击

早期 JWT 库允许 alg: none 头,意味着签名被跳过。攻击者可以构造任意 token 并声称不需要验证。现代库默认拒绝,但配置不当(比如允许从 header 读取 alg 而不限制白名单)仍会触发。

// 危险:允许任何算法
jwt.verify(token, secret);  // 一些老版本库的默认行为

// 安全:显式指定算法
jwt.verify(token, secret, { algorithms: ['HS256'] });

3. 无法即时撤销

JWT 在过期前一直有效。如果用户登出后 token 被截获,攻击者仍能使用。解决方法:

  • 短 TTL:access token 15 分钟过期,配合 refresh token
  • 黑名单:服务端维护撤销列表(但这就破坏了"无状态"的初衷)
  • 密钥轮换:定期换密钥,让所有旧 token 失效(影响所有用户)

4. 存储位置

JWT 放在 localStorage 容易被 XSS 偷走。放在 HttpOnly cookie 里安全一些,但又要处理 CSRF。没有完美方案,看你优先防哪种攻击。

何时选 Session

  • 传统 Web 应用:服务端渲染、同源、需要即时登出
  • 强安全要求:金融、医疗,任何需要"立刻踢人下线"的场景
  • 简单系统:单体服务,没有跨服务鉴权需求
  • 数据不应被客户端看到:session 数据完全在服务端

何时选 JWT

  • 微服务架构:各个服务只需要验签就能识别用户,无需共享 session 存储
  • SPA + 移动 API:客户端管理 token,跨域、跨平台
  • 第三方 API 调用:服务间调用,token 自包含身份信息(可以用 JWT 生成器 快速构造)
  • 短 TTL 场景:配合 refresh token,access token 短期有效

混合方案

实际项目常常混用。常见模式:

  • JWT 用于服务间调用,Session 用于浏览器:API 网关验证 JWT,但用户登录用 session
  • Refresh token + Access token:refresh token 是有状态的(可撤销),access token 是无状态 JWT(短 TTL)
  • Session ID 换 JWT:登录后服务端发 JWT,但 token 列表存在 session 里,可批量撤销

在本地解码和生成 JWT

无论你是在调试 token 内容、验证签名,还是测试 JWT 生成逻辑,都应该避免把真实 token 发到陌生服务。

JWT 解码器JWT 生成器 都在浏览器本地完成所有操作。token 的 header 和 payload 在你的浏览器里解码,签名验证用 Web Crypto API 在本地完成。你的 token(尤其是生产环境的 token,可能包含用户 ID 和权限信息)不会被发送到任何服务器。

# 命令行快速解码 JWT payload(无需密钥)
echo "eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ" | base64 -d 2>/dev/null || \
  echo "eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ==" | base64 -d

总结:传统 Web 应用优先 session;微服务、跨平台 API 优先 JWT(配短 TTL)。永远不要把敏感数据放进 JWT payload。需要即时撤销就别用纯 JWT,要么用 session,要么用 refresh token 配黑名单。


广告