图 1:一个把状态留在服务端,一个把状态签进令牌随身带。
先说明属性:这是一篇经典题解析,题目出自本地历史面经资料整理,与任何公司的当前招聘无关。「基于 token 的后台鉴权和用户名密码加 session 有什么具体不同」是后端面试的经典对比题。模型 API 网关的 API Key、JWT 设计也直接复用这套框架。差别不止「一个存服务端一个存客户端」一句话,要按状态、分布式、撤销、安全四个维度逐项拆开。
先把两套流程各走一遍。
Session 流程:客户端提交账密,服务端验证通过后建一条会话记录(sessionID 映射到用户),存进内存或外部存储,把 sessionID 通过 Cookie 下发;之后每个请求带 sessionID,服务端查表比对,命中即放行。状态在服务端,会话表是 single source of truth。
Token 流程:登录验证后,服务端用密钥把用户信息、签发时间、过期时间签成一张令牌返回,客户端自持(常见放 Authorization 头);之后每个请求带令牌,服务端只做两件事——本地验签、查过期,不查任何存储。以 JWT 为例,RFC 7519 定义的令牌就是 header.payload.signature 三段:头部声明算法,载荷装声明(用户、过期时间等),签名保证前两段没被改过。
逐项对比,差异才真正显形。
状态保持:session 有状态,服务端要为每个在线用户留一条记录;token 无状态,验证纯靠计算,服务端零存储。这一条决定了后面所有差异。
分布式适配:session 方案上多机就麻烦——请求落到没存这条会话的机器上就不认,要么粘滞会话绑死机器,要么引入共享存储把会话表集中,多一个组件多一份运维。token 天然水平扩展:任何一台机器拿同一个密钥验签即可,扩容缩容无感。这也是微服务和开放 API 场景几乎一边倒选 token 的原因。
撤销能力:这是 token 的短板,也是面试的区分点。session 想踢人,删一条记录立即生效;token 签发后到过期前都有效,服务端想提前作废只能额外维护黑名单(又引回了状态),或者把过期时间压短、配合刷新令牌轮换。所以「token 无状态」和「可撤销」本质上是 trade-off,设计时要在过期时长上找平衡。
安全面:sessionID 走 Cookie,浏览器自动携带,要防 CSRF(配合 SameSite、校验来源);token 手动放请求头,不受 CSRF 影响,但若存 localStorage 就要防 XSS 偷令牌。另外 token 的载荷只是编码不是加密,敏感信息不能往里放——签名只保证没被篡改,不保证别人看不见。
代码实测:签发、验签、篡改、过期。
下面用 Node.js 22.15.0 内置 crypto 模拟 HMAC 签名的令牌全流程(node token.js):
const crypto = require('crypto');
const SECRET = 'demo-secret-key';
const b64u = o => Buffer.from(JSON.stringify(o)).toString('base64url');
function sign(payload) {
const body = `${b64u({ alg: 'HS256', typ: 'JWT' })}.${b64u(payload)}`;
const sig = crypto.createHmac('sha256', SECRET).update(body).digest('base64url');
return `${body}.${sig}`;
}
function verify(token) {
const [h, p, s] = token.split('.');
const expect = crypto.createHmac('sha256', SECRET).update(`${h}.${p}`).digest('base64url');
if (s !== expect) return { ok: false, reason: '签名不符' };
const payload = JSON.parse(Buffer.from(p, 'base64url').toString());
if (payload.exp * 1000 < Date.now()) return { ok: false, reason: '已过期' };
return { ok: true, payload };
}
const now = Math.floor(Date.now() / 1000);
const t = sign({ sub: 'user-42', role: 'admin', iat: now, exp: now + 3600 });
console.log('正常验签:', JSON.stringify(verify(t)));
const [h, , s] = t.split('.');
const forged = `${h}.${b64u({ sub: 'user-42', role: 'guest', iat: now, exp: now + 3600 })}.${s}`;
console.log('篡改后验签:', JSON.stringify(verify(forged)));
输出:
正常验签: {"ok":true,"payload":{"sub":"user-42","role":"admin",...}}
篡改后验签: {"ok":false,"reason":"签名不符"}
注意验签全程没碰任何数据库——这就是无状态的含义;攻击者改掉 payload 里的角色,签名立刻对不上。
图 2:两条时序对照,高亮的那一步就是「不查库」。
怎么选型。
单体应用、后台管理系统、需要强踢人能力:session 简单直接。多实例微服务、开放 API、跨域移动端:token 更合适。现实系统也常混用——对外 API 用 token,内部管理后台用 session。更全面的方案选型(含 OAuth2 授权)是另一篇的题材,这里只把两种机制本身讲透。
答题建议:先各用一句话概括两种流程,然后按状态、分布式、撤销、安全四维度列对比,主动指出「撤销是 token 的代价」,最后给选型结论。你被问过「token 怎么踢人下线」这个追问吗?当时怎么答的?评论区见。