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,并确保之后清除SecurityContextHolderDelegatingSecurityContextRunnable 看起来像这样

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

虽然非常简单,但它使从一个Thread到另一个Thread传输SecurityContext变得无缝。这很重要,因为在大多数情况下,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获取它)。
  • 创建一个委托的Executor,负责执行提交的Runnable
  • 最后,我们创建一个DelegatingSecurityContextExecutor,它负责使用DelegatingSecurityContextRunnable包装传递给execute方法的任何Runnable。然后,它将包装的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,然后执行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 并发类

请参阅Javadoc,了解与Java并发API和Spring Task抽象的更多集成。一旦你理解了之前的代码,它们就非常容易理解。

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上执行,并使用创建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 社区中所有即将举行的活动。

查看全部