打开网易新闻 查看精彩图片

全球每天有超过2300万次会话劫持尝试,其中67%靠偷刷新令牌(Refresh Token)续命。传统JWT验证像只认签名不认人的门卫——只要令牌没过期,谁拿都能进。

这套新方案把数据库变成了"实时黑名单"。用户点下退出登录的瞬间,黑客手里偷来的令牌就变成了电子垃圾。

从"签名即正义"到"数据库说了算"

从"签名即正义"到"数据库说了算"

旧流程的毛病很直观:JWT自包含验证逻辑,服务端验个签名就放行。令牌没过期?合法。签名对得上?放行。至于用户是不是早就点了退出,系统完全不知道。

新方案加了六道关卡。令牌进来先验签名,这是基本功。接着去数据库拉取该用户所有活跃会话,把传入的令牌和每条记录里的哈希值做比对——用的是bcrypt,故意慢点,防暴力破解。

匹配失败直接拒,匹配成功还要看会话有没有被标记过期。最后更新"最后活跃时间",发新的访问令牌。全程数据库是真相来源,JWT只是张带时效的入场券。

打开网易新闻 查看精彩图片

代码里有个细节:refreshTokenHash字段在实体上标了select: false,平时查询默认不拿出来,只有显式指定才加载。这是防日志泄露的兜底设计。

性能陷阱与破解之道

性能陷阱与破解之道

遍历比对在用户设备少时没问题。但如果有人同时登了手机、平板、笔记本、浏览器、手表——循环bcrypt比较会变成明显延迟。

优化方案写在注释里:把会话ID塞进JWT的payload。这样直接按ID查单条记录,O(1) vs O(n)的区别。代价是payload多几个字节,令牌变长一点点。

还有个隐性成本:每次刷新都要写数据库更新lastUsedAt。高并发场景下,这行代码可能是瓶颈。要不要异步化?要不要批量写?看业务对"最后活跃"精度的容忍度。

退出登录终于"真退出"了

退出登录终于"真退出"了

打开网易新闻 查看精彩图片

以前很多系统的退出只是删客户端cookie,服务端令牌还活着。黑客偷到令牌照样用,用户以为安全了,实际裸奔。

现在退出时把数据库里isActive改成false,或者物理删除记录。下次有人拿这个令牌来刷新,第三步比对就找不到匹配项,直接 UnauthorizedException。偷令牌的黑客和用户同时在线?用户一退出,黑客立刻断网。

这套机制还附赠了一个能力:后台可以强制下线指定设备。发现异常登录?查session表,把那条记录标记失效,不需要改用户密码、不需要全局令牌失效。

实现时有个坑要注意:bcrypt比较是异步的,循环里用了await。如果用户会话极多,这串比较是顺序执行的。Node.js单线程,长时间阻塞会影响同进程其他请求。会话ID优化不只是性能问题,也是稳定性问题。

最后一步发新访问令牌,有效期15分钟(900秒)。这是标准的长短令牌配合:访问令牌短命,泄露窗口小;刷新令牌长命,但受数据库约束。两者结合,鱼和熊掌各得其所。

这套方案已经被用在NestJS的生产环境里。如果你正在设计登录系统,会为了安全性接受每次刷新都查库的开销,还是宁愿用纯JWT换取性能?