1. 概述

在这个快速教程中,我们将关注Servlet 3对异步请求的支持,以及Spring MVC和Spring Security如何处理这些请求

在Web应用中引入异步性的基本动机是为了处理长时间运行的请求。在大多数情况下,我们需要确保Spring Security的主体(principal)能够传播到这些线程中。

当然,Spring Security也与@Async【注:Spring Security与@Async的集成】在MVC之外的范围以及处理HTTP请求时进行整合。

2. Maven依赖

为了在Spring MVC中使用异步集成,我们需要将以下依赖项添加到我们的pom.xml

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

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

3. Spring MVC与@Async

根据官方文档,Spring Security与WebAsyncManager【注:WebAsyncManager接口】进行了集成。

首先,我们需要确保我们的springSecurityFilterChain设置为处理异步请求。我们可以在Java配置中通过在Servlet配置类中添加以下行来实现:

dispatcher.setAsyncSupported(true);

或者在XML配置中:

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <async-supported>true</async-supported>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ASYNC</dispatcher>
</filter-mapping>

我们还需要在Servlet配置中启用async-supported参数:

<servlet>
    ...
    <async-supported>true</async-supported>
    ...
</servlet>

现在我们可以发送带有传播的SecurityContext的异步请求了。

Spring Security内部机制会确保当在其他线程中提交响应时,我们的SecurityContext不会被清除,从而避免用户登出。

4. 使用场景

让我们通过一个简单的示例来演示这一点:

@Override
public Callable<Boolean> checkIfPrincipalPropagated() {
    Object before 
      = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    log.info("Before new thread: " + before);

    return new Callable<Boolean>() {
        public Boolean call() throws Exception {
            Object after 
              = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            log.info("New thread: " + after);
            return before == after;
        }
    };
}

我们想要检查Spring的SecurityContext是否已传播到新线程。

上述方法将自动在其Callable执行时包含SecurityContext,如日志所示:

web - 2017-01-02 10:42:19,011 [http-nio-8081-exec-3] INFO
  o.baeldung.web.service.AsyncService - Before new thread:
  org.springframework.security.core.userdetails.User@76507e51:
  Username: temporary; Password: [PROTECTED]; Enabled: true;
  AccountNonExpired: true; credentialsNonExpired: true;
  AccountNonLocked: true; Granted Authorities: ROLE_ADMIN

web - 2017-01-02 10:42:19,020 [MvcAsync1] INFO
  o.baeldung.web.service.AsyncService - New thread:
  org.springframework.security.core.userdetails.User@76507e51:
  Username: temporary; Password: [PROTECTED]; Enabled: true;
  AccountNonExpired: true; credentialsNonExpired: true;
  AccountNonLocked: true; Granted Authorities: ROLE_ADMIN

如果没有设置SecurityContext的传播,第二个请求最终将得到null值。

还有其他重要的使用场景需要使用带有传播SecurityContext的异步请求:

  • 我们想要发起多个外部请求,它们可以并行运行,可能需要很长时间才能执行
  • 我们本地有大量处理工作,而外部请求可以与之并行执行
  • 其他代表"fire-and-forget"场景,例如发送电子邮件

需要注意的是,如果我们的多个方法调用以前是以同步方式串联在一起的,那么将它们转换为异步方法可能需要同步结果。

5. 总结

在这篇简短的教程中,我们展示了在认证上下文中的异步请求处理Spring支持【注:原文为authenticated context,此处保留原文,但可理解为有身份验证的上下文】

从编程模型的角度来看,新的功能看似简单,但实际上确实有一些方面需要深入理解。

这个示例也可作为Maven项目在Github上获取【注:链接为https://github.com/eugenp/tutorials/tree/master/spring-security-modules/spring-security-web-rest】。