会话令牌 vs JWT:错误的二分法
Source: Dev.to
(请提供您希望翻译的正文内容,我将为您翻译成简体中文,并保留原有的格式、Markdown 语法以及技术术语。)
“仅 JWT” 方法
// User logs in
const jwt = sign(
{ userId: user.id, role: user.role },
SECRET,
{ expiresIn: '7d' }
);
setCookie('token', jwt);
// Every request
const payload = verify(req.cookies.token, SECRET);
// Done. No database hit.
优点
- 无需数据库查询——每台服务器都可以验证令牌。
- “无状态”认证(不管这到底意味着什么)。
缺点
- 撤销问题——如果用户被删除或解雇,其 JWT 在最长 7 天内仍然有效。
- 角色数据陈旧——更改用户角色不会影响已签发的令牌。
- 没有“全局登出”——被盗的笔记本、遗失的设备等会在令牌过期前保持有效。
结果: 速度快(≈0.97 ms/请求)且吞吐量高(≈5,527 请求/秒),但无法撤销令牌,数据会立即变得陈旧。
“仅会话” 方法
// User logs in
const sessionId = randomBytes(32).toString('hex');
await db.session.create({
id: sessionId,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
setCookie('sessionId', sessionId);
// Every request – database lookup
const session = await db.session.findUnique({
where: { id: req.cookies.sessionId },
include: { user: true }
});
if (!session || session.expiresAt /* … */) {
// handle missing or expired session
}
Result: 安全但慢;数据库成为瓶颈。
为什么混合方法有效
混合方法仅在 访问令牌过期 时触及存储(例如,大约 1 % 的请求)。对于其余 99 % 的请求,你可以获得 JWT 级别的速度,而无需网络调用。
关于 Redis 的简要说明
如果将 PostgreSQL 替换为 Redis 进行会话存储,查询时间会降至约 2–3 ms,但仍然会在 每一次 请求上进行外部调用。混合方法可以消除绝大多数流量中的这一次调用。
“微服务” 反驳
“微服务不共享数据库,所以 JWT 是唯一可以独立验证令牌的方式。”
实际上,微服务已经共享:
- 用于持久化的数据库(或集群)
- 用于共享状态的 Redis(或其他缓存)
- 消息队列、日志、监控等
如果你已经有 Redis 用于缓存,会话验证成本很低。混合方式仍然通过避免 99 % 请求的网络往返而获胜。
解决方案:短期访问令牌 + 长期刷新令牌
它 不是 JWT 与会话的对立,而是 JWT + 会话,各自发挥最擅长的功能。
工作原理
// User logs in
async function login(email, password) {
const user = await authenticateUser(email, password);
// Refresh token – stored in DB (valid for 30 days)
const refreshToken = randomBytes(32).toString('hex');
await db.session.create({
userId: user.id,
refreshToken,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
});
// Access token – NOT stored, just a JWT (valid for 15 minutes)
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
SECRET,
{ expiresIn: '15m' }
);
// Send tokens to the client
res.cookie('accessToken', accessToken, { httpOnly: true });
res.cookie('refreshToken', refreshToken, { httpOnly: true });
}
- 访问令牌(JWT) – 短期有效,本地验证,无需 DB/Redis 查询。
- 刷新令牌(会话) – 长期有效,存储在 DB/Redis 中,仅在访问令牌过期或用户主动登出时使用。
刷新流程(简化版)
// Refresh endpoint
async function refresh(req, res) {
const { refreshToken } = req.cookies;
const session = await db.session.findUnique({
where: { refreshToken },
include: { user: true }
});
if (!session || session.expiresAt /* … */) {
// handle missing or expired refresh token
return;
}
// Issue a new short‑lived access token
const newAccessToken = jwt.sign(
{ userId: session.user.id, role: session.user.role },
SECRET,
{ expiresIn: '15m' }
);
res.cookie('accessToken', newAccessToken, { httpOnly: true });
}
要点: 仅使用会话会把认证系统变成数据库瓶颈。混合方案在保留 JWT 高速验证的同时,加入了会话式的控制能力。
完整的基准测试结果请参见 stat-tests/RESULTS.md,测试代码位于 stat-tests/test-three-auth-strategies。
建议
- 使用混合方案 适用于几乎所有情况。它能提供 JWT 级别的速度和会话级别的安全性。
- 仅使用 JWT 仅在令牌极短生命周期(< 5 分钟)且你真的不在乎撤销(极少情况)时使用。
- 仅使用会话 仅在小型应用(< 10 请求/秒)且简洁性胜过性能考虑时使用。
你不必非此即彼。既享受 JWT 的速度 又 获得会话的控制——只需权衡利弊,而不是盲目崇拜单一方案。
祝玩得开心!