在退出登录 / 修改密码时怎样实现JWT Token失效?

搜索了好多的说是:退出登录, 只要客户端把Token丢弃就可以了,服务器端不需要废弃Token。

问题1:未过期的token还是可以用

要是用户在多个设备登录了,而且本地保存了token。当一个地方丢弃token,但是这个token要是没有过期,那之前token还是可以用的。

我的解决方案是:当用户第一次登录成功后将token存到数据库,每次都对比传过来的token和该用户在数据库的token是否相同。
当是问题2:多个设备会出现死循环

如果我把token存到数据库,当用户退出登录或者修改密码,更新token。当用户下次拿着之前的token来认证时,找到该用户数据库存的token,两个对比一下,如果一样就通过,否则让用户登录。有一个问题,如果这样做,要是有两设备,就死循环了。一个设备登录了,token就会变,导致另一个去登录,然后这个token又失效了,成死循环了。

jwt好像没有提供怎么注销token?请问有什么解决办法吗?TKS!

阅读 120.8k
15 个回答

其实要完美地失效JWT是没办法做到的。

"Actually, JWT serves a different purpose than a session and it is not possible to forcefully delete or invalidate an existing token."
这篇文章写得比较简单易懂:https://medium.com/devgorilla...

有以下几个方法可以做到失效 JWT token:

  1. 将 token 存入 DB(如 Redis)中,失效则删除;但增加了一个每次校验时候都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则(这不就和 session 一样了么?)。
  2. 维护一个 token 黑名单,失效则加入黑名单中。
  3. 在 JWT 中增加一个版本号字段,失效则改变该版本号。
  4. 在服务端设置加密的 key 时,为每个用户生成唯一的 key,失效则改变该 key。

关于多设备token的问题,token肯定是会存用户信息的,比如用户id,然后在存储一个版本号的信息,服务器端可以设计用户id和一个版本号的对应关系,版本号设置成自动增长的整型数字,或者就是一个时间戳,这样用之前的token去登录时,判断版本号不一致就让重新登录,分配新的版本号,服务器端呢,每次登录以后就更新一下对应数据的版本号

登录成功后,记录一个该用户的最近刷新时间 refreshTime,然后 token 中存储一下该 token 的生成时间 createTime。然后 token 校验的时候就去比较这两个时间。这样就能保证一个用户同时只有一个 token 是有效的。

新手上路,请多包涵

说来说去,想实现服务端注销功能,都必须要在服务器上保存一个数据:
1、UID-token对, (比黑名单要好)
2、UID-每个用户独立的盐,注销改盐

当一个地方丢弃token,但是这个token要是没有过期当丢弃的时候,更新一下过期时间为当前时间。另一个设备请求的时候发现已经过期,在重新申请token。

因为我也没具体实现过,只是看过别人文档这样说过。

用数据库来解决上述两问题:

数据库建一个表user_token来记录用户登录成功后的uid,token

当用户注册成功后。应用将用户的uid作为JWT Payload的一个属性,然后将获取的token和uid插入到user_token表中。

当用户退出登录或修改密码或token过期前更新此uid的token为0;

当用户再次登录时。通过一系列任务检查JWT的有效性。例如,检查签名是否正确;检查Token是否过期;通过后,再根据uid查询表user_token,若token不为0,则返回表中的token给该用户。否则,表明该用户此前退出或修改密码或过期,则重新生产token来跟新当前uid的token。

此方法的缺点是利用了数据库来判断。
第一个问题:保证一个uid只能有一个有效token,不会出现多个token同时指向一个uid。
第二个问题:不会出现多个设备重复登录死循环

这是目前我想到的关于JWT解决上述两个问题的解决方案,欢迎指正!TKS!

新手上路,请多包涵

遇到相同的问题,在搜索时看到。
说一下我看了一些博客回答之后的一个解决办法吧。
用户登陆时,redis存储一份token,过期时间和jwt设置的token过期时间相同。
如果用户登出,redis中清除相应token。
这样在每次用户带着token请求时,就可以检查token是否和redis中是否存在且值相同而判断用户请求是否合法了。
新手程序员= =欢迎指正错误!虚心求教

1、用户登录返回两个token,accessToken 1小时有效期(可根据需求调整),refreshToken 7天有效期
2、客户端统一处理token过期请求,如果捕获到这个过期请求,先刷新token,再自动重放请求。
3、用户退出登录,将token放入黑名单
4、并发问题也不存在,如果上一个请求发现token失效需要重新发送请求时,先查本地token是否刷新,刷新过就直接重新请求。

用户首次登陆是生成一个code,code可以放进缓存,同时把code放进token的payload,每次验证同时验证token和code。当用户修改密码等操作时重新生成code。退出登陆时,清除浏览器本地缓存就可以了。

新手上路,请多包涵

是否可以多加一个字段呢,用来存储第一次登录ip,然后根据ip不同判断是否为同一设备。不过总感觉这样还需比较一次的话就失去了token验证的意义了。

新手上路,请多包涵

要我会这么设计:
表:

uid 用户id
token token
token_time 本token的生成时间

假设
用户注册 之后生成token 设置token_time
用户jwtToken payloat存入 uid 和token_time 本地缓存存入 uid:token_time
A,B 2个客户端都登录了(此时A,B用的jwtToken payload 是一样的)
A用户修改密码之后 更新数据库的token 和token_time 本地缓存 存入uid:新token_time


新token的A用户登录 检查缓存的uid对应的 tooken_time和 payload里面的token_time 一样则可以通过(如果缓存失效则查一次数据库)
B用户是登录上来是老的token_time 则不能登录

为什么会说多一个token_time 就是不想把token敏感信息存在payload里面而已

新手上路,请多包涵

如果服务端不保存任何token信息,好像不能主动令token失效,只能退出的时候,请求头就不带这个token了,让这个token自动失效,所以token的有效时间不能过长

新手上路,请多包涵

我认为这个问题的正确的解决方法是:客户端保证删掉这个token,让这个信息永远消失。所以就不可能再使用这个token登录了。既然我们认为token存在客户端是安全的(不然我们也不会采用jwt对吧),那么delete掉这个token也是可以保证的,这两件事情处在同一个信任链上。

新手上路,请多包涵

引入JWT所带来的问题:

  1. token到期后需要无感续签;
    目前主流方案是采用双token机制,accessToken访问令牌、refreshToken刷新令牌;
  2. 已签发的token服务端无法主动将其失效;
    场景1:用户退出登录 (需要将该次登录签发的accessToken和refreshToken同时失效);
    场景2:用户修改密码、服务端修改了某个用户具有的权限或者角色、用户的帐户被封禁/删除。。。。; (需要将该用户签发的所有token都失效);

    目前网上大多数方案是使用黑名单机制,直接将accessToken存到黑名单并设置过期时间;
    但是这样还是有其他问题并未解决。

    首先,签发出去的accessToken和refreshToken之间没有关联关系,用户退出登录时无法同时将refreshToken也失效。
    其次,用户密码泄露被别人恶意登录,用户紧急修改密码。由于不知道当前签发了哪些token,无法使其全部都失效
    (有一个方案是每个用户单独有自己的secretKey,改密码后直接将secretKey变更,但是这种方案验签时每次都要先从数据库查询出secretKey,成本太高)。
    再次,由于JWT生成的token一般都比较长,直接将其存到缓存对内存占用也比较大。

由此对第2点做如下优化:
这里依然使用黑名单机制,不过额外再引入两个字段(token序列号,token批次号),token序列号作为关联此次登录的accessToken和refreshToken,token批次号作为关联密码变更前签发的所有token。

  1. 在系统增加一个全局自增变量(由Redis自增incr)作为token序列号,暂时命名为tokenSeqNum
  2. 在数据库user表中增加一个字段tokenBatchNum作为token批次号。
  3. 用户登录成功时,tokenSeqNum全局incr,从user表中取到当前用户的tokenBatchNum; 将username、tokenSeqNum、tokenBatchNum添加到JWT的payload中。
  4. 用户退出登录时,从accessToken中解析出username、tokenSeqNum,拼接username_tokenSeqNum做为缓存key存入Redis黑名单,并设置过期时间。
  5. 用户修改密码时,从user表中取到了用户当前的tokenBatchNum,更新用户表时同时更新tokenBatchNum = tokenBatchNum + 1,拼接username_tokenBatchNum做为缓存key存入Redis黑名单,并设置过期时间。
  6. 用户请求受保护资源、或请求刷新token时,进行黑名单校验; 首先从token中解析出username、tokenSeqNum、tokenBatchNum;
    如果用户操作了退出登录,则username_tokenSeqNum存在黑名单中,判断为签名已失效;
    如果用户修改了密码,则username_tokenBatchNum存在黑名单中,判断为签名已失效;

如此。既可以解决关联失效问题,又能节省内存空间。

关于以上想法,欢迎大家一起探讨是否可行。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏