1. 前言
最近在搞一个鉴权认证服务器,其中有个问题就是token的无感刷新。Token无感刷新是一种在用户不感知的情况下自动更新访问令牌(Token)的机制,以维持用户的登录状态。
一般是使用一个短期的token来做权限认证,而更长时间的refreshToken
来做短token的刷新,而在实现的过程中就有各种问题出来比如:
- Q1: 是要在服务器端实现还是能在客户端实现?
- Q2: token过期后无法解析,怎么获取到其中的过期时间?
- Q3: 无感刷新即是需要在获取到新token后重发原来的request请求,并将二次请求的结果返回给原调用者,如何实现?
下面我就对上面这些问题给出我自己的拙见,希望能对读者有所帮助😁
2. 客户端实现
2.1 初始版本
想法:每次客户端发起的请求会被服务器端gateway拦截,此时在gateway中判断token是否无效(过期):
- 过期则返回一个特定的状态码(可以自定义也可以用HTTPStatus)告诉客户端当前token失效
- 没过期则放行,继续原本的业务逻辑
而前端处可以拦截到当前服务器返回的响应状态码,根据状态码来执行对应的操作,也就是下面要引出的axios
2.1.1 服务器端gateway实现拦截器
注意环境springboot3+java17,通过继承GlobalFilter
来实现对应的filter逻辑
2.1.1.1 问题Q2解决
正常情况下解析的token会报错,那么就在解析的时候拦截错误,如果catch 到JwtException
,此时就认为该token无效已经过期了返回true
否则则执行正常逻辑获取并返回token中的过期时间与当前时间比较的结果
2.1.2 axios拦截器
在拦截器中,我们使用判断响应码,如果是401则清空用户数据回退到登录页面,而如果是511则使用refreshToken
再请求刷新一次(其他的情况在这里就不做分析,感兴趣的读者可以自行研究)
2.1.3 refresh刷新token方法实现
这里实现是重新用axios原生发异步请求,而不是使用在request.ts
中导出的请求方法(因为里面定义了请求拦截,每次请求之前都会取出token并放到请求头,这就又变成请求头中携带的token无效了,导致重复发送刷新请求进入死循环,所以不能这样做)
2.1.4 正常和刷新情况下的console输出信息分析
细心的读者可以注意到上边的代码有很多地方有控制台的输出,加上这些可以更方便的读懂代码的逻辑,下面我们就运行代码跑跑看看结果返回情况,这里建议各位结合代码分析看看我做输出的地方是在哪里。
下图是正常情况下的返回结果,注意这里的token是以hizFIGg
结尾,而refreshToken
是以suvm-EgQ
结尾(这两个注意与异常的来比对)正常情况下返回的结果肯定是200即ok
“
注意>>>>>处输出的结果是点击该按钮后点击事件返回的结果,对应着Q3的思考,具体分析会结合失败的例子来演示
下面来看异常情况的分析,由于token太长了,所以拆分两张图片更容易看一点,从左边的图开始分析
- 在发起第一次请求后,后端gateway拦截器报错 511 (是不是就是对应上面case 511 此时应该用
refresh token
刷新) in ?>>
进来refresh方法的逻辑,成功打印出refreshToken
以suvm-EgQ结尾(是不是跟上面refreshToken
相同)- 紧接着就是 输出 刷新token成功 此时返回的是刷新后的token,将其覆盖新的token并重新发送请求
到这里左图分析完毕,进入右图的分析(肯定有读者疑惑你这黄色的warn咋不讲)别急这块我会和右图的红色error一起讲解
- 紧接上面,用新的token发送请求,此时在请求拦截器处捕获到的token是不是就是更新好的 以
V0dYcMA
结尾,而refreshToken
则以suvm-EgQ
结尾(得出结论refreshToken
用做刷新,但本身并不刷新) - 此时捕获到
Uncaught error status 511
这不就是我们一开始的报错吗? 其实就是这样的,原来的按钮点击事件调用getAllUser
方法已经结束!!! 返回的结果是error 即是这里的511(把左右三个有颜色的块拼起来一起看就懂了)而由于refresh方法是异步调用的所以其执行顺序穿插在其中
最后返回结果可以看到已经没有上面注意部分提到的>>>>>输出内容,令通过更新好的token发送二次请求得到的结果记作data,此时的data已经不能返回原来的getAllUser
方法调用处,因为原来的方法已经结束,通俗点话说就是这样的二次调用结果毫无意义,用户还是需要刷新网页或者二次点击以获取资源
这就是Q3提出的思考,由于异步调用而非阻塞式的调用方式导致原方法提前终止,可以考虑换成阻塞式的调用refresh方式刷新token,但是这样又会导致该次点击的响应变慢,用户体验差(有更好想法的读者可以在评论区一起讨论)
2.2 改进版本
既然异步方法不得行,那能不能换种思路?不要在失败的时候发送,而是提前检查存在本地的token有没有过期,当检查token过期时间小于一个临界点,则异步调用刷新token方法,更新现有的token信息,此时是不是就解决上面的问题,只要是服务器端gateway拦截到token失效的请求我都要求重新登录。此时就引出一个定时器的概念
“
在
TypeScript
中,定时器主要是指通过setInterval
和setTimeout
这两个函数来实现的周期性或延时执行代码的功能。首先,
setInterval
是一个可以按照指定的时间间隔重复执行某段代码或函数的方法。它接受两个参数:第一个参数是你想要周期性执行的函数或代码块,第二个参数是时间间隔,单位为毫秒。
由于当setInterval
被调用时,它会在指定的时间间隔后执行给定的函数或代码块。这个时间间隔是以毫秒为单位的,而且它是从调用setInterval
的那一刻开始计算的。这意味着一旦setInterval
被调用,定时器就会立即启动,并在每个指定的时间间隔后重复执行。所以该定时器的设定应该放在login方法登录返回结果处
2.2.1 定义定时器类
通过该定时器类,可以实现MyTimer.start
方法调用setInterval
间隔delay 时间步执行,判断当前的token过期时间是否小于我们设置的minCheck , 如果小于则使用refreshToken
异步刷新token
2.2.2 修改Login点击事件
只用看新增的方法,其他的都是一些权限跟token等的存储
2.2.3 测试
按理来说测试时候应该没有问题,能正确解析token,而实际运行时候却报错,无法正确解析token报错
“
InvalidTokenError: Invalid token specified: invalid json for part #2
而后续换成jwt.verify()
使用密钥来解码同样报错,甚至无法加载出页面
半天这token解析不了就很奇怪了,后面在网上查阅资料的过程中总结出来,由于后端生成的token是通过jjwt这个依赖实现的,对于不同的库底层的编码实现逻辑会有差异导致a库加密生成的token并不能完全被b库的方法来解密
找到了原因,那我们应该如何获取token中的过期时间呢?可以使用与jjwt相同的实现逻辑库来解码该token或者不妨换个思路,从服务器端下发token的时候我就带上这个过期时间,这样就省去了前端解码这个步骤,所以就引出了如下最终实现版本
2.3 最终定时器版本(实现可以直接看这里)
2.3.1 服务器端修改
2.3.1.1 根据token获取其过期时间
2.3.1.2 发放token处携带过期时间
同样在refreshToken
处也就不是只返回token,也需要带上其过期时间,代码与上面相同就不重复写了
2.3.2 修改监控器类MyTimer
最终版本该类中包含这三个属性,分别是
- timerId: 定时器的唯一ID
- delay: 定时器执行的间隔时间
- minCheck: 判断token过期时间是否小于该值,小于则需执行
refresh()
方法来刷新token。
同时使用单例模式全局导出唯一的实例方便管理,对于上面的token无法解析问题,直接从服务器端获取token的过期时间expire然后与当前时间比较就好啦。
2.3.3 onPageRender 使用
需要注意最后一个方法onPageRender
,由于在测试中发现当通过导航栏访问的页面情况下会导致定时器给kill掉了,无法刷新token,发送新请求的时候就会报错,所以最好的方法是在每个页面上添加onPageRender
方法,该方法也很简单就是重启一下定时器,只要给定时器刷新token就能解决上面的问题
在页面中添加的代码如下:
3. 服务器端实现
这种实现方法是在gateway处做拦截判断当前的token是否过期,如果过期则通过WebClient携带refreshToken
异步发起请求到认证服务器更新,下面代码实现了发起请求到获取数据的过程,但是没有实现原来请求的再发送(偷个懒,后面再来填坑)
4. 怎么选择
在服务器端实现的好处如下:
- 安全性: 在服务器端进行token刷新可以更好地控制和保护token的安全性,避免将敏感信息暴露给客户端
- 减少客户端逻辑: 客户端无需过多关注token刷新逻辑,降低了客户端的复杂性和维护成本。
- 集中管理: 所有用户的token刷新逻辑集中在服务器端,方便统一管理和调整。
- 解决一致性问题: 用户端刷新token可能导致不同客户端之间的状态不一致,比如一个设备刷新了token而另一个设备未刷新,可能会出现异常情况。
而在客户端实现的好处又如下:
- 即时性: 客户端自动监控可以实现实时监测token的有效性,并及时触发刷新,确保用户操作的流畅性和体验。
- 离线支持: 对于需要离线访问或长时间不与服务器通信的应用场景,客户端自动监控可以更好地处理token失效情况。
- 灵活性: 某些特定场景下,客户端可能更容易实现对token状态的监控和处理,例如需要根据用户行为动态调整token刷新策略等。
- 减轻服务器压力: 用户端刷新token可以减少服务器负担,尤其对于大量用户同时刷新token时,可分散处理压力。
可见在不同的场景下实现的方法有所不同,要根据实际需求来决定,往往在一些高精度高安全性的系统中适合在服务器端做token的刷新,其他场景(例如移动端应用或简单的 Web 应用等)下可以尝试客户端实现的方法分担服务器压力。