Spring Security 3.2 M1 亮点,Servlet 3 API 支持

工程 | Rob Winch | 2012 年 12 月 17 日 | ...

上周我宣布了 Spring Security 3.2 M1 的发布,其中包含了改进的 Servlet 3 支持。在这篇帖子中,我将介绍 3.2 M1 版本中一些更精彩的功能。具体来说,我们将看看以下 Spring Security 新功能:

并发支持

您可能会问:“为什么在这个以 Servlet 3 为主题的版本中会有并发支持?”原因是并发支持为该版本中的所有其他功能提供了基础。虽然并发支持被 Servlet 3 集成所使用,但它也可以作为构建块,在任何应用程序中支持并发和 Spring Security。现在让我们来看看 Spring Security 的并发支持。

DelegatingSecurityContextRunnable

Spring Security 并发支持中最基本的构建块之一是 `DelegatingSecurityContextRunnable`。它包装了一个委托的 `Runnable`,以便使用指定的 `SecurityContext` 来初始化 `SecurityContextHolder`。然后,它调用委托的 `Runnable`,并确保之后清除 `SecurityContextHolder`。`DelegatingSecurityContextRunnable` 看起来如下:

public void run() {
  try {
    SecurityContextHolder.setContext(securityContext);
    delegate.run();
  } finally {
    SecurityContextHolder.clearContext();
  }
}

虽然很简单,但它能够无缝地将 `SecurityContext` 从一个 `Thread` 转移到另一个 `Thread`。这很重要,因为在大多数情况下,`SecurityContextHolder` 是基于每个 `Thread` 进行操作的。例如,您可能已经使用了 Spring Security 的 <global-method-security> 支持来保护您的某个服务。现在,您可以轻松地将当前 `Thread` 的 `SecurityContext` 转移到调用受保护服务的 `Thread`。下面的示例展示了如何做到这一点:


Runnable originalRunnable = new Runnable() {
  public void run() {
    // invoke secured service
  }
};

SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable, context);

new Thread(wrappedRunnable).start();

上面的代码执行以下步骤:

  • 创建一个将调用我们受保护服务的 `Runnable`。请注意,它不知道 Spring Security。
  • 从 `SecurityContextHolder` 获取我们想要使用的 `SecurityContext`,并初始化 `DelegatingSecurityContextRunnable`。
  • 使用 `DelegatingSecurityContextRunnable` 来创建一个 `Thread`。
  • 启动我们创建的 `Thread`。

由于使用 `SecurityContextHolder` 中的 `SecurityContext` 来创建 `DelegatingSecurityContextRunnable` 非常常见,因此提供了一个快捷构造函数。以下代码与上面的代码相同:


Runnable originalRunnable = new Runnable() {
  public void run() {
    // invoke secured service
  }
};

DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable);

new Thread(wrappedRunnable).start();

我们提供的代码使用起来很简单,但仍然需要我们了解正在使用 Spring Security。在下一节中,我们将看看如何利用 DelegatingSecurityContextExecutor 来隐藏我们正在使用 Spring Security 的事实。

DelegatingSecurityContextExecutor

在上文中,我们发现 DelegatingSecurityContextRunnable 使用起来很简单,但并不理想,因为在使用它时我们需要了解 Spring Security。让我们看看 DelegatingSecurityContextExecutor 如何使我们的代码免于了解我们正在使用 Spring Security。

DelegatingSecurityContextExecutor 的设计与 DelegatingSecurityContextRunnable 非常相似,不同之处在于它接受一个委托的 Executor 而不是委托的 Runnable。您可以在下面看到一个如何使用它的示例


SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = 
    new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"));
context.setAuthentication(authentication);

SimpleAsyncTaskExecutor delegateExecutor =
    new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
    new DelegatingSecurityContextExecutor(delegateExecutor, context);

Runnable originalRunnable = new Runnable() {
  public void run() {
    // invoke secured service
  }
};

executor.execute(originalRunnable);

代码执行以下步骤

  • 创建将用于我们的 DelegatingSecurityContextExecutorSecurityContext。请注意,在此示例中,我们只是手动创建了 SecurityContext。但是,我们从哪里或如何获得 SecurityContext 并不重要(也就是说,如果我们愿意,可以从 SecurityContextHolder 中获取)。
  • 创建一个负责执行已提交的 Runnable 的 delegateExecutor
  • 最后,我们创建一个 DelegatingSecurityContextExecutor,它负责将任何传递到 execute 方法的 Runnable 包装到 DelegatingSecurityContextRunnable 中。然后,它将包装的 Runnable 传递给 delegateExecutor。在此实例中,将使用相同的 SecurityContext 来处理提交到我们的 DelegatingSecurityContextExecutor 的每个 Runnable。这对于运行需要以特权用户运行的后台任务很有用。

这时您可能会问自己:“这如何使我的代码免于了解 Spring Security?”与其在自己的代码中创建 SecurityContextDelegatingSecurityContextExecutor,不如注入一个已初始化的 DelegatingSecurityContextExecutor 实例。


@Autowired
private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor

public void submitRunnable() {
  Runnable originalRunnable = new Runnable() {
    public void run() {
      // invoke secured service
    }
  };
  executor.execute(originalRunnable);    
}

现在,我们的代码不知道 SecurityContext 已传播到 Thread,然后执行了原始的 Runnable,最后清除了 SecurityContextHolder。在此示例中,使用相同的用户来执行每个 Thread。如果我们想使用调用 executor.execute(Runnable)SecurityContextHolder 中的用户(即当前登录的用户)来处理原始的 Runnable 怎么办?这可以通过从我们的 DelegatingSecurityContextExecutor 构造函数中删除 SecurityContext 参数来实现。例如


SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
    new DelegatingSecurityContextExecutor(delegateExecutor);

现在,每次执行 executor.execute(Runnable) 时,SecurityContext 会首先由 SecurityContextHolder 获取,然后该 SecurityContext 用于创建我们的 DelegatingSecurityContextRunnable。这意味着我们将使用调用 executor.execute(Runnable) 代码时使用的相同用户来执行我们的 Runnable

Spring Security 并发类

有关与 Java 并发 API 和 Spring 任务抽象的更多集成,请参阅 Javadoc。一旦您理解了前面的代码,它们就会非常直观。

Servlet 3 API 集成

Spring Security 长期以来一直支持 Servlet API 集成。然而,直到 3.2 M1 才支持 Servlet 3 中添加的新方法。在本节中,我们将讨论 Spring Security 集成的每种方法。如果您想在实践中看到这一点,可以使用 Gradle 插件将 Spring Security 导入 Spring Tool Suite,并运行 servletapi 示例应用程序

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse)

Spring Security 现在与 HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse) 集成。简而言之,我们可以使用此方法来确保用户已通过身份验证。如果他们未通过身份验证,将使用配置的 AuthenticationEntryPoint 来要求用户进行身份验证(即重定向到登录页面)。

HttpServletRequest.login(String,String)

Spring Security 现在与 HttpServletRequest.login(String,String) 集成。用户可以使用此方法使用 Spring Security 验证用户名和密码。如果身份验证失败,将抛出包装原始 Spring Security AuthenticationExceptionServletException。这意味着如果您允许 ServletException 传播,Spring Security 的 ExceptionTranslationFilter 将为您处理它。或者,您可以捕获 ServletException 并自行处理。

HttpServletRequest.logout()

Spring Security 现在通过调用配置的 LogoutHandler 实现来与 HttpServletRequest.logout() 集成。通常这意味着 SecurityContextHolder 将被清除,HttpSession 将被失效,任何“记住我”身份验证都将被清理掉,等等。但是,配置的 LogoutHandler 实现将根据您的 Spring Security 配置而有所不同。请注意,在调用 HttpServletRequest.logout() 之后,您仍然负责编写响应。通常,这包括重定向到欢迎页面。

AsyncContext.start(Runnable)

AsynchContext.start(Runnable) 方法确保您的凭据将传播到新的 Thread。使用 Spring Security 新添加的并发支持,Spring Security 会覆盖 AsyncContext.start(Runnable) 以确保在处理 Runnable 时使用当前的 SecurityContext

Servlet 3 异步支持

Spring Security 现在支持 Servlet 3 和异步请求。那么如何使用它呢?

第一步是确保您已将 web.xml 更新为使用 3.0 架构,如下所示


<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0">

</web-app>

接下来,您需要确保您的 springSecurityFilterChain 已设置为处理异步请求。


<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>

就是这样!现在 Spring Security 将确保您的 SecurityContext 在异步请求上也能得到传播。

有什么变化?Spring Security 内部的重构将确保在响应已在另一个 Thread 上提交时,您的 SecurityContext 不再被清除,从而导致用户看起来已注销。此外,您可以使用 Spring Security 的并发支持和 Spring Security 的 AsyncContext.start(Runnable) 集成来帮助您处理 Servlet 请求。

Spring MVC 异步集成

[callout title=将 SecurityContext 与 Callable 关联] 更技术性地说,Spring Security 与 WebAsyncManager 集成。用于处理 CallableSecurityContext 是在调用 startCallableProcessing 时存在的 SecurityContextHolder 上的 SecurityContext。 [/callout]

正如 Rossen 在之前的博客文章中演示的那样,Spring Web MVC 3.2 拥有 出色的 Servlet 3 异步支持。无需额外配置,Spring Security 将自动将 SecurityContext 设置为执行控制器返回的 CallableThread。例如,以下方法将自动以创建 Callable 时可用的 SecurityContext 执行其 Callable


@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {
 
  return new Callable<String>() {
    public Object call() throws Exception {
      // ...
      return "someView";
    }
  };
}

对于控制器返回的 DeferredResult,没有自动集成。这是因为 DeferredResult 由用户处理,因此没有自动集成的方法。但是,您仍然可以使用 并发支持 来提供与 Spring Security 的透明集成。

反馈

希望这能让您更好地了解 Spring Security 3.2 M1 中提供的更改,并让您对下一个里程碑感到兴奋。作为社区成员,我鼓励您试用新的里程碑,并在 JIRA 中报告任何错误/增强功能。这些反馈是回馈社区的简单但非常重要的方式!

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有