为了安全起见,使用无状态JWT令牌时可以使用短时限TTL(1分钟)策略,然后这些令牌会在其生存时间内及时刷新。如果服务器不知道用户何时注销,那么可以继续刷新已注销用户的令牌。本文将提供针对这个问题的一种解决方案,使之在保持水平扩展性的同时确保安全性能不受影响。
架构设计
从图中展示的体系架构可见,每个微服务都有自己的数据库。被撤销的令牌和用户都需要单一(身份)信息源(Single Source of Truth,简称“SSOT”)。数据库需要具有高可用性,包括多主机、热备份及数据库的其他功能。其中,撤销的令牌数据库只需要两个表:一个用于用户注销时来缓存撤销的令牌,此令牌由负责缓存撤销令牌表中内容的微服务每90秒调用一次;另一个用于用户登录。每次注销后,微服务都会在定义的行生存时间内更新撤销的令牌表,并且登录是有速度限制的。因此,上述体系结构减少了吊销令牌数据库的负载,使其能够扩展到更大的部署。被撤销令牌的单一(身份)信息源要求是必要的,因为每个用户请求都可以在任何微服务上处理,并且需要在那里检查撤销的令牌。需要通过用户表来支持微服务登录用户。这样一来就可以将安全检查的负载分散到各个微服务。该架构中,JWT令牌是在微服务的内存中进行检查的,而且只增加了一点CPU损耗,不要求使用IO负载。
实现代码分析
为了验证上述结论,我开发了一个MovieManager项目来实现撤销令牌的处理。首先,我们来看登录部分实现代码。
登录操作
为了支持已撤销的令牌,登录时首先要检查用户当前已撤销令牌的数量,并降低登录速度,以限制用户可以生成的已撤销令牌的数量。这一部分功能是在UserDetailsMgmt服务中完成的,其中关键部分代码如下:
private UserDto loginHelp(Optional<User> entityOpt, String passwd) { UserDto user = new UserDto(); Optional<Role> myRole = entityOpt.stream() .flatMap(myUser -> Arrays.stream(Role.values()) .filter(role1 -> Role.USERS.equals(role1)) .filter(role1 -> role1.name().equals(myUser.getRoles()))).findAny(); if (myRole.isPresent() && entityOpt.get().isEnabled() && this.passwordEncoder.matches(passwd, entityOpt.get().getPassword())) { Callable<String> callableTask = () -> this.jwtTokenService .createToken(entityOpt.get() .get