-

参考资料

  1. jwt+shiro+redis实现token的自动刷新和token的可控性
  2. SpringBoot 框架下 shiro 与 jwt 的结合的用户登录
  3. 关于 JWT Token 自动续期的解决方案
  4. JWT【关于刷新和作废的思考】
  5. token 机制和实现方式
  6. 阮一峰-JSON Web Token 入门教程
  7. 授权及认证机制:Session、JWT与OAuth 2
  8. 【Web】关于Session过期/失效的理解

accessToken控制刷新间隔,refreshToken控制最长过期时间

Min过期时间 = refreshToken过期时间 - accessToken过期时间

Max过期时间 = refreshToken过期时间

实际过期时间是一个范围,在(Min过期时间 ~ Max过期时间)之间

如:refreshToken过期时间1小时,accessToken过期时间为5分钟

则实际过期时间是55分钟 ~ 60分钟

因此,refreshToken过期时间和accessToken过期时间相差越大,实际过期时间就越精确

这其实就是Token的续签问题,我们看一下网上提到的一些解决方案:

1.更新Payload里面的过期时间。

JWT的Payload里面可以设置一个过期时间,我们可以在用户每次访问的时候把这个过期时间更新一下。由于JWT的secret加密机制,只要exp变了,整个Token就变了,所以这种机制相当于每次重新颁发了一个新的Token。

这种方案简单粗暴,存在性能问题,还有安全问题,以前的那么多Token咋办?

2.快过期的时候更新Token

比如说离过期时间还有不到1个小时的时候才更新Token,性能上面可能好一点,但是如果一个用户一直在访问,但是恰好最后一个1个小时内没有访问网站,那岂不是也gg了?

3.使用Cache记录Token过期时间

Token本身不设置过期时间,然后我们在redis或memcached等缓存里面单独设置一个有效期,每次访问的时候刷新过期时间。

其实这个方案和使用session机制无异,session也可以保存在redis或者memcached里面的。所以,有人戏说这是重新发明了session 。。。

4.使用refreshToken

借鉴 oauth2 的设计,返回给客户端一个 refreshToken,允许客户端主动刷新JWT。一般而言,jwt 的过期时间可以设置为数小时,而 refreshToken 的过期时间设置为数天。 我对oauth2不太熟悉,不过很明显这个方案更加复杂了,而且为什么不拿旧的Token去刷新JWT呢?

5.推荐方案

最后说一下我觉得比较合适的方案,当服务器接受到一个Token后,如果它已经过期,但是已过期的时间在xx天内,比如说30天,我们就返回一个新的Token。比如说Token的有效期是7天,但是如果过期时间不超过30天就可以用旧的Token换取一个新的Token,如果超过了30天那就需要重新登录。

Token作废问题

当用户退出登录、修改密码之后,讲道理我们是需要作废之前的Token,比如说用户的Token被盗用了,只能通过修改密码来防止账号被盗用。如果使用session机制就很简单了,我们清空服务器session,或者使用新的session替换之前旧的session也行。

由于Token是无状态的,理论上只要不过期就可以一直用,你说这咋办?为了安全,必须得做一些额外的工作!

1.Cache

如果你之前是采用把Token存在cache里面这种方案,那么你只要删除cache里面的key就可以了。不过如果你真的是采用这种方案,还不如直接用session,这时候的Token和sessionid没区别。

2.用户关联

有人说,建一张表把uid和Token关联起来,这样一个用户只有一个有效的Token,或者存cache也行,建立uid和Token的一对一关系,这方案和1差不多。无论是存表还存cache,每次访问都必不可免的需要访问库或cache。

3.黑名单

在数据表或cache里面维护一个黑名单,也避免不了查库或者查cache,为了避免这个库内容过多,可以定期清理数据库,或者给cache设置一个有效期。比如说在上面说的例子里面,有效期应该设置为30天,30天之后就不用管了。

其实我比较喜欢第3种方案,第2种方案如果用户多了对库压力大,而第3种,除非用户经常修改密码或者退出登录,不然这个数据集不会很大。

如果不考虑安全,我们完全可以不考虑Token作废问题,那么我们就必须在防止XSS攻击上面做好工作,比如说使用https,cookies设置httpOnly。。。

是否需要使用JWT Token?

看完之后大家是否发现原来JWT Token并没有那么好用,这也是很多人说的不要采用JWT的原因了: 讲真,别再使用JWT了!、请停止使用 JWT 认证 。。。

仔细看完这些文章其实大家会发现JWT尤其适合那些一次性验证的应用,比如说有些网站的文件下载为了防止盗链,会在url后面追加一些字符串,这些字符串其实就是Token,它里面可能包含了用户信息和过期时间,你发送给别人下载或者想盗链就非常麻烦了。

至于用不用我觉得还是看需求,你觉得呢?

仅仅使用jwt+shiro还不能实现token的可控性,和token的自动刷新。这样就会导致token如果在30min后过期,那么如果用户30min后仍然需要使用系统,那么可能就需要重新登录,这是非常不好的用户体验。其次在token的有效期内,即使用户退出了登录,token依然有效,依然可以使用,这是不安全的,所以需要使用redis来进行可控性操作。

Token缓存策略

AccessToken和RefreshToken

2-1. Shiro + JWT实现无状态鉴权机制

  1. 首先post用户名与密码到login进行登入,如果成功在请求头Header返回一个加密的Authorization,失败的话直接返回10001未登录等状态码,以后访问都带上这个Authorization即可。

  2. 鉴权流程主要是要重写shiro的入口过滤器BasicHttpAuthenticationFilter,在此基础上进行拦截、token验证授权等操作

2-2. 关于AccessToken及RefreshToken概念说明

  1. AccessToken:用于接口传输过程中的用户授权标识,客户端每次请求都需携带,出于安全考虑通常有效时长较短。

  2. RefreshToken:与AccessToken为共生关系,一般用于刷新AccessToken,保存于服务端,客户端不可见,有效时长较长。

2-3. 关于Redis中保存RefreshToken信息(做到JWT的可控性)

  1. 登录认证通过后返回AccessToken信息(在AccessToken中保存当前的时间戳和帐号),同时在Redis中设置一条以帐号为Key,Value为当前时间戳(登录时间)的RefreshToken,现在认证时必须AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中时间戳一致才算认证通过,这样可以做到JWT的可控性,如果重新登录获取了新的AccessToken,旧的AccessToken就认证不了,因为Redis中所存放的的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每个用户就只能使用最新的AccessToken认证。

  2. Redis的RefreshToken也可以用来判断用户是否在线,如果删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户

2-4. 关于根据RefreshToken自动刷新AccessToken

  1. 本身AccessToken的过期时间为5分钟(配置文件可配置),RefreshToken过期时间为30分钟(配置文件可配置),当登录后时间过了5分钟之后,当前AccessToken便会过期失效,再次带上AccessToken访问JWT会抛出TokenExpiredException异常说明Token过期,开始判断是否要进行AccessToken刷新,首先redis查询RefreshToken是否存在,以及时间戳和过期AccessToken所携带的时间戳是否一致,如果存在且一致就进行AccessToken刷新。

  2. 刷新后新的AccessToken过期时间依旧为5分钟(配置文件可配置),时间戳为当前最新时间戳,同时也设置RefreshToken中的时间戳为当前最新时间戳,刷新过期时间重新为30分钟过期(配置文件可配置),最终将刷新的AccessToken存放在Response的Header中的Authorization字段返回。

  3. 同时前端进行获取替换,下次用新的AccessToken进行访问即可。

用户登录分为下面几种情况

  1. 浏览界面时 token 未过期

    正常浏览

  2. 浏览界面时 token 过期,refreshToken 没有过期

    正常浏览,返回新的 token,生成新的 refreshToken

  3. 浏览界面时 token 过期,refreshToken 过期

    返回登录界面,清除 cookie 中的 token

  4. 登录时 token 未过期

    正常登录

  5. 登录时 token 过期,refreshToken 没有过期

    正常浏览,返回新的 token,生成新的 refreshToken

  6. 登录时 token 过期,refreshToken 过期

    通过数据库判断用户信息

  7. 浏览时 token 未过期但与 refreshToken 不匹配

    重新登录

  8. 登录时 token 未过期但与 refreshToken 不匹配

    通过数据库判断用户信息

在前后端分离的开发模式下,前端用户登录成功后后端服务会给用户颁发一个 jwt token。前端(如 vue)在接收到 jwt token 后会将 token 存储到 LocalStorage 中。

后续每次请求都会将此 token 放在请求头中传递到后端服务,后端服务会有一个过滤器对 token 进行拦截校验,校验 token 是否过期,如果 token 过期则会让前端跳转到登录页面重新登录。

因为 jwt token 中一般会包含用户的基础信息,为了保证 token 的安全性,一般会将 token 的过期时间设置的比较短。

但是这样又会导致前端用户需要频繁登录(token 过期),甚至有的表单比较复杂,前端用户在填写表单时需要思考较长时间,等真正提交表单时后端校验发现 token 过期失效了不得不跳转到登录页面,对用户非常不友好。

jwt token 自动续期的实现原理如下:

  1. 登录成功后将用户生成的 jwt token 作为 key、value 存储到 cache 缓存里面 (这时候 key、value 值一样),将缓存有效期设置为 token 有效时间的 2 倍。
  2. 当该用户再次请求时,通过后端的一个 jwt Filter 校验前端 token 是否是有效 token,如果 token 无效表明是非法请求,直接抛出异常即可;
  3. 根据规则取出 cache token,判断 cache token 是否存在,此时主要分以下几种情况:
    • cache token 不存在
      这种情况表明该用户账户空闲超时,返回用户信息已失效,请重新登录。
    • cache token 存在,则需要使用 jwt 工具类验证该 cache token 是否过期超时,不过期无需处理。
      过期则表示该用户一直在操作只是 token 失效了,后端程序会给 token 对应的 key 映射的 value 值重新生成 jwt token 并覆盖 value 值,该缓存生命周期重新计算。

实现逻辑的核心原理:
前端请求 Header 中设置的 token 保持不变,校验有效性以缓存中的 token 为准。

留言

⬆︎TOP