JWT引入后的问题及优化方案:无感续签与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存在黑名单中,判断为签名已失效;

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

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

阅读 1.6k
avatarAI BotBETA

引入JWT所带来的问题及优化方案:

问题一:token到期后需要无感续签

解决方案
采用双token机制是一种有效的解决方案。accessToken用于访问资源,而refreshToken用于在accessToken过期时获取新的accessToken,无需用户重新登录。

问题二:已签发的token服务端无法主动将其失效

目前问题

  • 签发出去的accessTokenrefreshToken之间没有关联关系,导致用户退出登录时无法同时将refreshToken失效。
  • 用户密码泄露后,无法使所有已签发的token失效。
  • JWT生成的token较长,直接存到缓存中内存占用大。

优化方案
引入黑名单机制结合token序列号与批次号的方案是可行的,可以解决上述问题。

  1. 全局自增变量作为token序列号:确保每次登录的accessTokenrefreshToken都有唯一的序列号关联。
  2. token批次号管理:当用户修改密码时,更新批次号,这样可以通过批次号来使所有旧的token失效。
  3. 黑名单校验:在每次请求校验token时,除了正常的JWT校验外,还需要校验token是否在黑名单中。

优化方案优势

  • 解决了accessTokenrefreshToken之间的关联失效问题。
  • 通过批次号管理,可以方便地使所有旧的token失效,解决了密码泄露的风险。
  • 使用username_tokenSeqNumusername_tokenBatchNum作为缓存key,可以有效节省内存空间。

注意事项

  • 需要确保Redis等存储黑名单的系统可靠且性能足够。
  • 在黑名单中设置合理的过期时间,避免无效数据长期占用存储空间。
  • 在实现过程中,要确保所有token的签发、校验和失效操作都是线程安全的。

综上所述,您提出的优化方案是可行的,能够解决JWT引入后的一些问题,并有效地管理token的失效。当然,具体实现时还需要根据实际情况进行调整和优化。

1 个回答

你的做法没有问题,黑名单能解决,但是味道变了。

我们先回答为什么要引入JWT

无非是为了它能自验证(不用访问数据库、Redis等存储)、也能携带一些敏感信息(payload中)

引入黑名单后,问题是解决了,但是也畸形了

每次又得去访问数据库或者Redis,然后再自验证,从这个角度讲,那为什么不直接就用传统Token呢?后者更加简单直接

我的实践

假设一个场景,我们是需要多端登录的:App端、H5端、小程序端、PC端等等,一类终端的登出不影响其他类终端,如果修改密码,所有端都强制需要重新验证登录。

  1. 颁发JWT(Payload中有代表用户唯一性的属性,比如手机号或用户ID,加密处理),在Redis上存储:key为token:[userid]:[client],value为 JWT,设置有效期和JWT保持一致。 其中,key中的[client]表示客户端类型,有效值为pch5app等。
  2. 校验阶段:JWT可先自验证,通过后,取出其中userid,再去访问Redis进行验证该Token的合法性。
  3. 如果用户修改密码,无效掉Redis中所有key为 token:[userid]:[client]的项
  4. 如果用户在某一端登出,仅无效掉对应client类型的 Redis key即可。

如果你有refreshToken机制,Redis中无非多了一种key refreshtoken:[userid]:[client],在用户修改密码或登出时,做同样处理。

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