1. 引言

在这个教程中,我们将专注于Spring Security 原则在使用@Async时的传播

默认情况下,Spring Security 的身份验证绑定到一个ThreadLocal——因此,当执行流在@Async的新线程中运行时,那将不是一个已认证的上下文。

这并不理想,让我们来修复它。

2. Maven 依赖

为了在 Spring Security 中使用异步集成,我们需要在pom.xmldependencies部分包含以下内容:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>6.1.5</version>
</dependency>

Spring Security 最新版本的依赖项可以在这里找到。

3. Spring Security 与@Async的传播

首先,我们写一个简单的示例:

@RequestMapping(method = RequestMethod.GET, value = "/async")
@ResponseBody
public Object standardProcessing() throws Exception {
    log.info("Outside the @Async logic - before the async call: "
      + SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    
    asyncService.asyncCall();
    
    log.info("Inside the @Async logic - after the async call: "
      + SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    
    return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

我们想要检查 Spring SecurityContext是否被传播到新线程。 首先,我们在异步调用之前记录上下文,然后运行异步方法,最后再次记录上下文。asyncCall()方法的实现如下:

@Async
@Override
public void asyncCall() {
    log.info("Inside the @Async logic: "
      + SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}

如您所见,这仅有一行代码,将在异步方法的新线程中输出上下文。

4. 默认配置

默认情况下,在@Async方法内的SecurityContext将具有null值。

特别是,如果我们运行异步逻辑,我们可以在主程序中记录Authentication对象,但当我们尝试在@Async内部记录时,它将是null。以下是示例日志输出:

web - 2016-12-30 22:41:58,916 [http-nio-8081-exec-3] INFO
  o.baeldung.web.service.AsyncService -
  Outside the @Async logic - before the async call:
  org.springframework.security.core.userdetails.User@76507e51:
  Username: temporary; ...

web - 2016-12-30 22:41:58,921 [http-nio-8081-exec-3] INFO
  o.baeldung.web.service.AsyncService -
  Inside the @Async logic - after the async call:
  org.springframework.security.core.userdetails.User@76507e51:
  Username: temporary; ...

  web - 2016-12-30 22:41:58,926 [SimpleAsyncTaskExecutor-1] ERROR
  o.s.a.i.SimpleAsyncUncaughtExceptionHandler -
  Unexpected error occurred invoking async method
  'public void com.baeldung.web.service.AsyncServiceImpl.asyncCall()'.
  java.lang.NullPointerException: null

所以,正如预期的那样,由于没有找到Principal,我们在执行器线程中可以看到NPE错误。

5. 异步安全上下文配置

如果我们想在异步线程中像在外部一样访问Principal,我们需要创建DelegatingSecurityContextAsyncTaskExecutor bean:

@Bean 
public DelegatingSecurityContextAsyncTaskExecutor taskExecutor(ThreadPoolTaskExecutor delegate) { 
    return new DelegatingSecurityContextAsyncTaskExecutor(delegate); 
}

这样,Spring 就会在每个@Async调用中使用当前的SecurityContext

现在,让我们再次运行应用程序并查看日志信息,以确保情况确实如此:

web - 2016-12-30 22:45:18,013 [http-nio-8081-exec-3] INFO
  o.baeldung.web.service.AsyncService -
  Outside the @Async logic - before the async call:
  org.springframework.security.core.userdetails.User@76507e51:
  Username: temporary; ...

web - 2016-12-30 22:45:18,018 [http-nio-8081-exec-3] INFO
  o.baeldung.web.service.AsyncService -
  Inside the @Async logic - after the async call:
  org.springframework.security.core.userdetails.User@76507e51:
  Username: temporary; ...

web - 2016-12-30 22:45:18,019 [SimpleAsyncTaskExecutor-1] INFO
  o.baeldung.web.service.AsyncService -
  Inside the @Async logic:
  org.springframework.security.core.userdetails.User@76507e51:
  Username: temporary; ...

正如预期,我们在异步执行器线程中看到了相同的Principal。

6. 使用场景

有几种有趣的情况,我们可能希望确保SecurityContext以这种方式传播:

  • 我们想要并发执行多个外部请求,这些请求可能需要很长时间才能执行
  • 我们有一些本地的重大处理任务,而我们的外部请求可以与之并行执行
  • 其他代表fire-and-forget场景,例如发送电子邮件

7. 总结

在这篇快速教程中,我们介绍了Spring对带有传播的SecurityContext的异步请求的支持。从编程模型的角度来看,新功能看似简单。

请注意,如果之前多个方法调用以前是同步连接在一起的,转换为异步方法可能需要同步结果。

这个示例也可作为Maven项目在GitHub上获取。