JWT 鉴权之踢人下线:如何实现用户强制退出登录

昨天在轮换jwt密钥时发现了一个问题,refresh token并不会失效,于是就有了今天这一系列的操作,好累...
先阐述一下我之前的鉴权流程:
用户进行登录
-> 签发 access token,并生成随机的哈希字符串用作 refresh token(JWT Secret 不参与),
并存到 Redis 里,把这个设置到客户端的 Cookies 中,客户端无法操作 access token
-> 客户端携带 token 访问需要鉴权的接口,通过 JWT Secret 解密,如果通过,则正常访问接口;
过期或者不通过则返回 401。在这里,前端请求前先判断 token 的过期时间,如果临近过期前 30s,
则需要调用接口刷新 token
-> 服务端接到刷新 token 请求后,从 Cookies 里拿 refresh token 的哈希,去 Redis 中找,
如果找到了,执行一次查库,判断用户状态,成功则返回新 token这其实算是一个比较标准的 JWT 鉴权流程,但这里暴露了 2 个问题。
一个是修改 JWT Secret 后,之前签发的 token 仍然可以继续使用,这时候只能手动去清理 Redis 的缓存。
二是在禁用或者删除用户后,这个 access token 还有一段窗口期。虽然我设置的是 10 分钟,已经将影响降到最低,而且这也符合 JWT 的预期,但是涉及到修改用户密码的情况,改完用户密码后用户不会立即被踢下线,这点就很恼火了。
于是今天引入了一个新的字段:token_version。
这个方案也不算什么很新的方案,但却能解决实际问题。
具体流程和之前差不多,要做的是在用户表中新增一个字段,并且在用户的两个 token 里也加上 token_version 字段。在鉴权时,比对 token 中的版本号和数据库中的版本号,如果不一致,则代表鉴权失败。
有两个地方需要进行比对,首先是刷新 token 的接口,这里比对 refresh token 中的 version(如果不考虑 token 的窗口期,这里做完其实就够了)。再就是鉴权的中间件,这里比对的是 access token 中的 version。因为每个鉴权接口都要经过这里,所以这里再像刷新 token 接口那里查库就不可能了,于是就把用户的 token version 和 status 放到 Redis 里去了,每次都走 Redis 查询。
这样一来,修改完密码或者禁用用户后,下次请求就能立马感知到,不会存在那一段窗口期。
缺点是加了一组缓存,维护起来相对麻烦。虽然修改完用户的版本号之后,可以不用清理之前残留的旧 token,下次用户拿失效 token 请求时可以惰性删除,或者等它自然过期,但我还是尽量在需要全清的地方把缓存清理掉,这样看着也舒服点。
后面看有没有空把 token 轮换也做了。JWT 推荐的是在刷新 token 时,两个 token 最好都轮换掉,防止重放攻击。我现在 access token 还是长效的,不过明天得开始准备面试了,这几天也没投简历,又混了十几天……