并发支持
在大多数环境中,安全信息是按Thread存储的。这意味着当在新Thread上执行任务时,SecurityContext会丢失。Spring Security 提供了一些基础设施来帮助简化这一管理过程。Spring Security 为多线程环境中的安全操作提供了底层抽象。实际上,Spring Security 正是基于此来集成 AsyncContext.start(Runnable) 和 Spring MVC 异步集成的。
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 =
UsernamePasswordAuthenticationToken.authenticated("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);
这段代码:
请注意,在此示例中,我们手动创建了 SecurityContext。然而,无论我们从何处或以何种方式获取 SecurityContext(例如,我们可以从 SecurityContextHolder 获取)都无关紧要。* 创建一个负责执行提交的 Runnable 对象的 delegateExecutor。* 最后,我们创建一个 DelegatingSecurityContextExecutor,它负责使用 DelegatingSecurityContextRunnable 包装传递给 execute 方法的任何 Runnable,然后将包装后的 Runnable 传递给 delegateExecutor。在这种情况下,提交给 DelegatingSecurityContextExecutor 的每个 Runnable 都使用相同的 SecurityContext。如果我们需要运行需要由具有提升权限的用户执行的背景任务,这非常有用。* 此时,您可能会问自己:“这如何使我的代码免受 Spring Security 相关知识的影响?” 我们可以在自己的代码中注入一个已经初始化的 DelegatingSecurityContextExecutor 实例,而不是创建 SecurityContext 和 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 却被清空了。在这个例子中,每个线程都使用相同的用户来运行。如果我们想要在调用 executor.execute(Runnable) 处理 originalRunnable 时,使用 SecurityContextHolder 中的用户(即当前登录用户)该怎么办呢?你可以通过从 DelegatingSecurityContextExecutor 构造函数中移除 SecurityContext 参数来实现:
SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
new DelegatingSecurityContextExecutor(delegateExecutor);
现在,每当执行 executor.execute(Runnable) 时,SecurityContext 会首先通过 SecurityContextHolder 获取,然后该 SecurityContext 被用来创建我们的 DelegatingSecurityContextRunnable。这意味着我们正在使用与调用 executor.execute(Runnable) 代码相同的用户来运行我们的 Runnable。
Spring Security 并发类
请参阅 Javadoc 以了解与 Java 并发 API 及 Spring 任务抽象的其他集成方式。一旦理解了前面的代码,这些内容便不言自明。