JWT的安全缺陷
JWT很好用,在RTheme的使用场景中,登录操作可以由两种方式完成,分别是账号+密码或者使用JWT来更新登录信息,其中后者可用于实现“连续n小时未登录则退出登录状态”的效果。 但很遗憾,这就表明一旦你泄露了自己的JWT,丢失的JWT就可以被用于直接登录,直接更改账号密码也无济于事。 这主要是因为一个用户可以同时使用多个JWT导致的,而这很明显并不合理:
- 旧的JWT无法被手动禁用,只能在过期的时候失效
- 旧的JWT可能无法同步用户的最新信息
传统的解决办法是通过缩短JWT的过期间隔来实现的,我在RTheme中为了解决这个问题在使用JWT作为凭据登录的时候额外做了一步校验,主逻辑函数位于
src/app/api/user/authorize/route.js
:
// 登录模式分发
if (typeof infoJSON.token !== 'undefined') {
// JWT 刷新登录
// 检查传入的token
let tokenInfo;
try {
tokenInfo = token.verify(infoJSON.token);
} catch (err) {
if (err.name == 'TokenExpiredError') {
return Response.json(
{
message: 'TOKEN已过期,请重新登录',
},
{ status: 410 }
);
} else {
return Response.json(
{
message: 'TOKEN无效',
},
{ status: 400 }
);
}
}
// TOKEN有效,刷新TOKEN
if (tokenInfo) {
// 请求新信息
let result = await prisma.user.findUnique({ where: { uid: tokenInfo.uid } });
// 检查此Token是否为最新
if (result.lastUseAt == tokenInfo.lastUseAt + '') {
updateTime(result.uid, startTime);
return Response.json(
{
message: '登录成功',
info: pack(result, startTime),
token: token.sign(pack(result, startTime), infoJSON.expiredTime || '7d'),
},
{ status: 200 }
);
} else {
return Response.json(
{
message: 'TOKEN未处于激活状态',
},
{ status: 420 }
);
}
}
}
其中的关键位置是
if (result.lastUseAt == tokenInfo.lastUseAt + '') {
updateTime(result.uid, startTime);
// ...
}
这里我实际上在数据库中使用lastUset 存储了上次登录时的时间戳,此时间戳也会打包进JWT里面,在执行敏感操作时就可以这样核验此JWT是否为最新的JWT。因此就算JWT泄露,你通过重新使用密码登录的方式也可以让丢失的JWT无法用于继续登录。
后记
这篇文章实际上是因为有个朋友正好问到相关的问题所以我稍微展开讲的,大家2025新年快乐。