JWT服务端主动失效方案
引言: 使用JWT时,有一个十分头疼问题就是:用户主动注销、强制登出(禁止登陆)、忘记密码、修改密码、JWT续签、踢出下线时,服务器不能让token主动失效!
本文将探索关于这个问题的解决方案。
# 目录:
[toc]
- 什么是JWT
- JWT的特点
- JWT主动失效方案
- JWT主动失效最佳实践
- 进阶优化
- 总结
# 1.什么是JWT
JWT(JSON Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
# 2.JWT的特点
无状态: JWT包含认证信息,token只需要保存在客户端,服务端不需要保存会话信息,所以JWT是无状态的。 这一点使得服务器压力大大降低,增加了系统的可用性和可扩展性。 但是无状态同时也是JWT最大的缺点,服务器无法主动让token失效。
安全性:客户端每次请求都会携带token,可以有效避免CSRF攻击,同时token会自动过期,可以减少token被盗用的情况,服务器会通过jwt签名验证token,可以避免token被篡改。
可见性: JWT的payload部分可以直接通过Base64解码,所以可存储一些其他业务逻辑所必要的非敏感信息。
# 3.JWT主动失效方案
由JWT的特点可知,JWT是无状态的,并且JWT的过期时间是签发的时候确定的,无法动态修改, 所以服务端如何让JWT主动失效成为了一个问题。 下面是几种让JWT主动失效的方案
- 服务器生成token时,将每一个token都保存在Redis中,每次请求都会直接对比用户携带的token和redis中的token,如果token相同,则通过验证,否则判定为token失效。
- 版本号校验:token和redis中保存用户的token版本号,每次请求对比版本号,一致就通过,不一致就拒绝,主动失效时直接使redis中该用户token版本号+1.
- 服务端保存一个token黑名单,服务器将需要失效的token放入黑名单,每次请求判断携带的token是否在黑名单之中。
- redis保存主动过期时间,每次请求获取token的签发时间,和redis中的过期时间对比,小于过期时间则失效。
- 签名校验,数据库中每个用户保存一个token签名,签发token时携带签名信息,redis中保存过期签名信息黑名单。每次比对签名,在黑名单,则失效。
# 4.JWT主动失效最佳实践
通过了解上面的JWT主动失效方案,下面结合实际业务需求, 总结一种可以应用于生产的最佳实践方案:
- 首先保证token过期时间不要太长,一般120分钟比较合适。
- token必须携带签发时间
- 通过redis保存一个黑名单,key包含用户id,value为主动失效时间戳。
- 每次请求,通过token基本校验之后,查询黑名单中是否有该用户id,如果有,那么比较token签发时间和黑名单中的主动失效时间,如果签发时间早于失效时间,那么表示token失效,拒绝请求,否则允许访问。
# 关于黑名单的实现:
可以直接使用redis保存key-value,key中携带用户id,value为失效时间,这样的话就可以直接使用redis的自动过期,这个过期时间设置为token有效时长(如前面的120分钟)
# 实现主动失效:
当需要主动让某用户token失效时, 可以组合当前用户id和当前时间戳保存在redis中,并设置有效时长。
# 5.进阶
对于上面这种方案, 每次让用户token失效,则会使得该用户在失效前签发的所有token都失效,无法更加精确的控制token的失效性。
# 优化1:
如果实际需求是,有多个应用产品,比如Android、IOS、Windows和Web多端产品,业务要求可以每次控制失效的具体是哪个产品申请的token。
这种情况,我们可以在JWT的payload中保存一个请求签发的token的具体客户端应用标识,比如(android), 而redis过期黑名单中的key值,除了携带用户id,再加上具体想失效的应用标识即可。
当然,如果要具体到每一个token,只能采用上面的方案1了。
# 优化2:
如果实际应用对访问速度和性能要求比较高,用户每次请求都要连接redis读取信息,是比较浪费的。 实际上,redis作为内存数据库,其速度已经相当的快了,不过实际场景可能会是分布式缓存系统,redis连接和读取还是会有一定的ttl延迟。 优化办法就是:需要做JWT校验的服务器应用,在启动时,访问分布式缓存中的token过期黑名单,将其保存在应用的本地内存当中。同时订阅分布式缓存的消息推送,在黑名单信息发生变化是,进行数据同步。
# 6.总结
通过上面的解决方案实践和优化 我们就可以实现服务器端主动让token失效的功能了。 而在具体的应用场景有:
用户主动注销、强制登出(禁止登陆)、忘记密码、修改密码、JWT续签、踢出下线等
实现的代价就是需要使用redis缓存黑名单, 在一定程度上破坏了JWT无状态的特性, 但是实际上需要主动使JWT失效的情况只占整个活动用户的很小一部分, 所以相比较于分布式Session,JWT实现的方式需要的存储空间很小。
对于进阶优化2,使用了分布式缓存和应用本地同步,增加了方案的复杂性, 不过对于大部分的应用场景来说,都具有完整的分布式缓存、消息分布订阅组件,只需要直接使用现成的即可,花费的代价并不多。
究竟采用哪种方案实现,都应该结合具体的业务需求的,并不存在真正完美的方案。