1. 引言
在这个教程中,我们将专注于Spring Security 原则在使用@Async
时的传播。
默认情况下,Spring Security 的身份验证绑定到一个ThreadLocal
——因此,当执行流在@Async
的新线程中运行时,那将不是一个已认证的上下文。
这并不理想,让我们来修复它。
2. Maven 依赖
为了在 Spring Security 中使用异步集成,我们需要在pom.xml
的dependencies
部分包含以下内容:
<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上获取。