← 中文资源

简体中文版本

构建带刷新轮换的 JWT 鉴权服务

一个完整的 JWT 鉴权服务:短期访问令牌、带重用检测的轮换刷新令牌、密钥轮换与吊销测试。

阅读英文原文

项目概述

本项目实现带刷新轮换的 JWT 鉴权服务:签发短时效访问令牌与轮换的刷新令牌,把刷新令牌的重复使用视为失窃证据,并据以吊销整个令牌家族。访问令牌保持无状态以便校验,刷新状态存库以便吊销,这一分工让热路径廉价而吊销仍然可行。

架构与关键决策

登录签发由当前密钥签名的访问 JWT 与一个以哈希形式存于家族中的刷新令牌;每次刷新轮换令牌并把上一个标记为已用;再次出现已用令牌即判定复用并吊销整族。家族是吊销单位,把一次失窃限制在一段会话而非整个账户。访问令牌以 JWKS 无状态校验,因此普通请求不访问数据库;密钥轮换先发布新钥再以新钥签发,并保留旧钥至少一个访问令牌生命周期。

实施与测试

登录、刷新一次,再重放第一个刷新令牌,断言整族被吊销且后续刷新失败;断言轮换窗口内旧钥仍可验证有效令牌;断言刷新令牌只以哈希存储;断言过期或被篡改的访问令牌被拒。

限制与后续

  • 无发送方约束绑定,令牌为纯持有者型。
  • 家族存单一数据库,多区域需复制或分区。
  • 无逐用户会话列表或远程登出界面。

代码示例

pnpm init
pnpm add jose pg express
pnpm add -D typescript tsx vitest
jwt-refresh-service/
├── src/login.ts
├── src/refresh.ts
├── src/keys.ts
├── src/revoke.ts
├── tests/refresh.test.ts
└── package.json
new SignJWT({ sub }).setExpirationTime('10m').setProtectedHeader({ alg: 'RS256', kid }).sign(key);
await db.insert({ familyId, hash: sha256(refresh), used: false });
await db.markUsed(hash); const next = randomHex(32); await db.insert({ familyId, hash: sha256(next) });
if (row.used) { await db.revokeFamily(familyId); return res.status(401).end(); }
jwks = { keys: [current.public, previous.public] };
pnpm vitest run