SpringBoot中JWTtoken管理策略详解在其生命周期内及时刷新。如果服务器不知道用户何时注销,则它可以继续刷新注销用户的令牌。本文将针对这一问题提供解决方案,使其在保持水平可扩展性的同时,保证安全性能不受影响。架构设计从图中所示的架构可以看出,每个微服务都有自己的数据库。被撤销的令牌和用户都需要单一事实来源(“SSOT”)。数据库需要高可用,包括多主、双机热备等数据库特性。其中,撤销令牌数据库只需要两张表:一张用于在用户注销时缓存撤销令牌,该令牌由负责缓存撤销令牌表中内容的微服务每90秒调用一次;用户登录的其他用途。每次注销后,微服务都会在定义的行生命周期内更新已撤销的令牌表,并且登录是有速率限制的。因此,上述架构减少了撤销令牌数据库的负载,使其能够扩展到更大的部署。撤销令牌的单一真实来源(身份)要求是必要的,因为每个用户请求都可以在任何微服务上处理,并且需要在那里检查撤销的令牌。需要user表支持微服务登录用户。通过这种方式,可以将安全检查的负载分配给各个微服务。在这个架构中,JWTtoken是在微服务的内存中校验的,只增加了一点CPU开销,不需要IO负载。实现代码分析为了验证以上结论,我开发了一个MovieManager项目来实现对revocationtokens的处理。首先我们看一下登录部分的实现代码。登录操作为了支持撤销令牌,登录首先检查用户当前撤销令牌的数量,并减慢登录速度以限制用户可以生成的撤销令牌数量。这部分功能在UserDetailsMgmt服务中完成,关键部分代码如下:privateUserDtologinHelp(OptionalentityOpt,Stringpasswd){可选<角色>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())){CallablecallableTask=()->这个。jwtTokenService.createToken(entityOpt.get().getUsername(),Arrays.asList(myRole.get()),Optional.empty());try{StringjwtToken=executorService.schedule(callableTask,3,TimeUnit.SECONDS).get();user=this.jwtTokenService.userNameLogouts(entityOpt.get().getUsername())>2?用户:this.userMapper.convert(entityOpt.get(),jwtToken,0L);}catch(InterruptedException|ExecutionExceptione){LOG.error("登录失败。",e);}}returnuser;}上面代码中,先过滤掉用户实体User的可选角色,然后检查用户实体User是否存在,是否有Users角色,是否启用,密码是否匹配。然后,创建一个Callable来为用户创建一个JWT令牌。令牌包含用户名和UUID,用于在注销时识别每个令牌。在不同的线程池上以3秒的延迟执行Callable,以限制用户在更新已撤销的令牌缓存之间可以进行的注销次数。接下来,检查是否为用户缓存了2个以上的已撤销令牌。如果为真,则拒绝登录。以上两项检查共同确保了用户可以生成的已撤销令牌的数量以及登录时的负载可以受到限制。为了实现横向扩展,必须将数据表移动到RevokedToken数据库中。注销操作注销操作是在UserDetailsMgmt服务中实现的,代码如下:publicBooleanlogout(StringbearerStr){if(!this.jwtTokenService.validateToken(this.jwtTokenService.resolveToken(bearerStr).orElse(""))){thrownewAuthenticationException("无效令牌");}Stringusername=this.jwtTokenService.getUsername(this.jwtTokenService.resolveToken(bearerStr).orElseThrow(()->newAuthenticationException("无效的承载字符串。")));Stringuuid=this.jwtTokenService.getUuid(this.jwtTokenService.resolveToken(bearerStr).orElseThrow(()->newAuthenticationException("无效的承载字符串。")));this.userRepository.findByUsername(username).orElseThrow(()->newResourceNotFoundException("找不到用户名:"+username));longrevokedTokensForUuid=this.revokedTokenRepository.findAll().stream().filter(myRevokedToken->myRevokedToken.getUuid().equals(uuid)&&myRevokedToken.getNa我().equalsIgnoreCase(用户名)).count();if(revokedTokensForUuid==0){this.revokedTokenRepository.save(newRevokedToken(username,uuid,LocalDateTime.now()));}else{LOG.warn("用户{}重复注销",username);}returnBoolean.TRUE;}在上面的代码中,首先检查JWTtoken是否有效接下来,从JWTtoken中读取用户名和UUID。然后,使用令牌中的用户名检查用户表用户中的用户数据。接下来,使用相同的UUID和UserID调用revokedTokens进行记录检查。如果找到相应的记录,将记录有关重复注销尝试的警告。如果JWT令牌是第一次被撤销,则会在已撤销的令牌表中创建一个包含当前用户名、UUID和当前时间等数据的新RevokedToken实体。为了实现横向扩展,数据表也必须移动到RevokedToken数据库中。撤销令牌缓存更新问题撤销令牌缓存任务使用CronJobs组件更新:@Scheduled(fixedRate=90000)publicvoidupdateLoggedOutUsers(){LOG.info("更新注销用户。");这个。用户服务。updateLoggedOutUsers();}每90秒从表中读取一次数据。更新操作在UserDetailsMgmt服务中进行处理:this.jwtTokenService.updateLoggedOutUsers(revokedTokens.filter()myRevokedToken->myRevokedToken.getLastLogout()==null||!myRevokedToken.getLastLogout().isBefore(LocalDateTime.now().minusSeconds(LOGOUT_TIMEOUT))).toList());this.revokedTokenRepository.deleteAll(revokedTokens.stream().filter(myRevokedToken->myRevokedToken.getLastLogout()!=null&&myRevokedToken.getLastLogout().isBefore(LocalDateTime.now().minusSeconds(LOGOUT_TIMEOUT))).toList());读取中所有已撤销的令牌。然后,删除早于LOGOUT_TIMEOUT(185秒)的记录;其他缓存在JwtTokenService中。JwtTokenService负责管理已撤销令牌的缓存:publicrecordUserNameUuid(StringuserName,Stringuuid){}privatefinalListloggedOutUsers=newCopyOnWriteArrayList<>();publicvoidupdateLoggedOutUsers(ListrevokedTokens){this.loggedOutUsers.clear();this.loggedOutUsers.addAll(revokedTokens.stream().map(myRevokedToken->newUserNameUuid(myRevokedToken.getName(),myRevokedToken.getUuid())).toList());}上面代码中,UserNameUuid记录数据包含具有身份令牌的值。注销用户或撤销令牌的用户名UUID在loggedOutUsers列表中提供。其中,CopyOnWriteArrayList是线程安全的。接下来,UpdateLogeDoutUsers方法获取当前已撤销令牌列表,清除并更新loggedOutUsers列表-此列表用于令牌验证。JWT令牌验证JWT令牌包含需要验证的用户名和哈希值。现在还会根据loggedOutUsers列表检查JWT令牌以检查注销。这部分任务在下面的JWT令牌过滤器部分完成:token!=null&&jwtTokenProvider.validateToken(token)){身份验证auth=token!=null?jwtTokenProvider.getAuthentication(令牌):空;SecurityContextHolder.getContext().setAuthentication(auth);}filterChain.doFilter(req,res);}在上面的代码中,在处理请求之前调用JwtTokenFilter。首先,从HTTP标头数据中读取令牌。然后,检查令牌是否已找到且有效(validateToken(...)。最后,在SecurityContextHolder中创建并设置身份验证。实现token验证的Java代码如下:Stringsubject=Optional.ofNullable(claimsJws.getBody().getSubject()).orElseThrow(()->newAuthenticationException("无效的JWT令牌"));Stringuuid=Optional.ofNullable(claimsJws.getBody().get(JwtUtils.UUID,String.class)).orElseThrow(()->newAuthenticationException("无效的JWT令牌"));返回this.loggedOutUsers.stream().noneMatch(myUserName->subject.equalsIgnoreCase(myUserName.userName)&&uuid.equals(myUserName.uuid));}catch(JwtException|IllegalArgumentExceptione){thrownewAuthenticationException("过期或无效的JWT令牌",e);}}上面代码中,首先解析token,检查signingkey,ReadStatement:否则抛出异常。然后,读取主题(用户名)和UUID。接下来,根据loggedOutUsers检查令牌。如果所有检查都正常,则令牌有效并处理请求。总结在上面的方案中,令牌的生命周期是60秒。撤销令牌写入撤销令牌表后,缓存每90秒更新一次。撤销的令牌在表中保留185秒。这意味着,每个令牌都需要在所有缓存中刷新。然后刷新将失败并且令牌不再有效。登录速率限制确保用户可以在已撤销令牌表中创建的条目数受到限制。所有这些都限制了RevokedToken数据库的负载,从而增加了它可以处理的微服务的数量。因此,拥有这样的架构可以降低代币丢失的风险。同时,基于JWTtoken认证的分布式安全检查让大部分的可扩展性优势得以保持。作为补充,对于微服务中的同步时钟,可以使用NTP技术。《Ubuntu中的同步技术》文章提供了实施此技术的操作指南。此外,文章《基于Spring+Angular的JWT自刷新解决方案》还展示了基于Angular的前端如何处理令牌的示例。译者介绍朱宪忠,社区编辑,专家博主,讲师,潍坊某高校计算机教师,自由编程资深人士。早期专注于各种微软技术(编译成三本与ASP.NETAJX和Cocos2d-X相关的技术书籍)。/ESP32/RaspberryPi等物联网开发技术和Scala+Hadoop+Spark+Flink等大数据开发技术。原标题:ScalableJWTTokenRevokationinSpringBoot,作者:SvenLoesekann