![]()
全球每天有超过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换取性能?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.