众所周知,JWT token 是无状态的,前端请求携带加密后的 token 到后端,后端解密后进行登录态和权限的校验等,整个过程中 token 一旦生成就无法撤销、销毁,直至其到期失效。

这就产生了一个问题,在 token 失效前如果手动点击了“退出登录”,我们能做的只是销毁客户端(浏览器)缓存的 token,然而,如果再以此 token 去调用 API,鉴权结果依然是通过的。尤其在跨域名 SSO 时,这个问题更为突出,如何保证各域名下的登录状态保持一致?

显然,我们需要一种机制,能够在服务端使 token 失效,或者,采取其它可行的策略。

最直接的,可以在 Redis 中缓存一个黑名单 blacklist,其中存储着“失效”的 token,然后在每次请求的鉴权方法中先查询 blacklist 中是否存在此 token,如果存在,则返回未登录的状态,如果不存在,再根据 token 进行登录状态验证和鉴权。

具体实现过程,先在logout方法中缓存 token:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
try { token = token.startsWith('Bearer ') ? token.split(' ')[1] : token; await this.cacheService.set(CACHE_KEY_TOKEN_PREFIX + token, 1, this.tokenExpiresIn); } catch (e) { ... }
try { token = token.startsWith('Bearer ') ? token.split(' ')[1] : token; await this.cacheService.set(CACHE_KEY_TOKEN_PREFIX + token, 1, this.tokenExpiresIn); } catch (e) { ... }

然后,在鉴权方法中进行查询和判断:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
token = token.startsWith('Bearer ') ? token.split(' ')[1] : token; const invalidToken = await this.cacheService.get(CACHE_KEY_TOKEN_PREFIX + token); if (invalidToken) { return false; }
token = token.startsWith('Bearer ') ? token.split(' ')[1] : token; const invalidToken = await this.cacheService.get(CACHE_KEY_TOKEN_PREFIX + token); if (invalidToken) { return false; }

其中,setget方法是对cacheManager的封装,如下:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
public get<T>(key: string): Promise<T> { return this.cacheManager.get(key); } public set(key: string, value: any, options?: number | { ttl: number }): Promise<void> { const ttl = typeof options === 'number' ? options : options ? options.ttl : this.defaultTTL; return this.cacheManager.set(key, value, ttl); }
public get(key: string): Promise { return this.cacheManager.get(key); } public set(key: string, value: any, options?: number | { ttl: number }): Promise { const ttl = typeof options === 'number' ? options : options ? options.ttl : this.defaultTTL; return this.cacheManager.set(key, value, ttl); }

至此,即可解决手动退出登录时的 token “销毁”问题和 SSO 下的登录状态同步问题。[]~( ̄▽ ̄)~*

One more thing

作为替代方案,还有一种实现方式,即:Refresh 机制。

简而言之,通过设置一个较短的有效期(过期时间,如 2h),并在临到期时自动延期的方式管理登录状态。

这种方式的好处是,无需增加缓存及相应的处理时间,但增加了 Refresh 操作及相关开销;不足是,这种方式仍然是伪登出,在 token 到期前的较短时间内,仍然存在登录状态不一致的问题,以及 token 未真正失效的问题。

具体采用何种方式,就看业务需求和性能需求了。