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,以便为委托初始化 SecurityContextHolder,并指定 SecurityContext。然后它调用委托 Runnable,确保之后清除 SecurityContextHolderDelegatingSecurityContextRunnable 看起来像这样

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

虽然非常简单,但它可以无缝地将 SecurityContext 从一个 Thread 转移到另一个 Thread。这很重要,因为在大多数情况下,SecurityContextHolder 是基于每个 Thread 工作的。例如,您可能使用过 Spring Security 的 <global-method-security> 支持来保护您的一个服务。现在您可以轻松地将当前 ThreadSecurityContext 转移到调用受保护服务的 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 方法的任何 RunnableDelegatingSecurityContextRunnable 包装起来。然后它将包装后的 Runnable 传递给 delegateExecutor。在这种情况下,提交给我们的 DelegatingSecurityContextExecutor 的每个 Runnable 都将使用相同的 SecurityContext。如果我们正在运行需要由具有提升权限的用户运行的后台任务,这将非常有用。

此时您可能正在问自己:“这如何使我的代码免于了解 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,然后 originalRunnable 被执行,接着 SecurityContextHolder 被清除。在此示例中,同一个用户被用来执行每个 Thread。如果我们想在调用 executor.execute(Runnable) 时使用 SecurityContextHolder 中的用户(即当前登录的用户)来处理 originalRunnable 怎么办?这可以通过从我们的 DelegatingSecurityContextExecutor 构造函数中移除 SecurityContext 参数来实现。例如


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

现在,无论何时执行 executor.execute(Runnable),首先会通过 SecurityContextHolder 获取 SecurityContext,然后使用该 SecurityContext 创建我们的 DelegatingSecurityContextRunnable。这意味着我们正在使用与调用 executor.execute(Runnable) 代码时相同的用户来执行我们的 Runnable

Spring Security 并发类

有关与 Java 并发 API 和 Spring Task 抽象的更多集成信息,请参阅 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 现在集成了 HttpServletRequest.logout(),它通过调用配置的 LogoutHandler 实现来工作。这通常意味着 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 将自动为执行控制器返回的 CallableThread 设置 SecurityContext。例如,以下方法返回的 Callable 将自动使用创建 Callable 时可用的 SecurityContext 来执行


@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 社区即将举行的所有活动。

查看全部