并发支持
在大多数环境中,安全性是按Thread级别存储的。
这意味着当在新Thread上工作时,SecurityContext会丢失。
Spring Security 提供了一些基础架构,使管理变得更加容易。
Spring Security 提供了在多线程环境中使用 Spring Security 的低级抽象。
事实上,这正是 Spring Security 用于与AsyncContext.start(Runnable)和Spring MVC 异步集成进行集成的基础。
委托安全上下文可运行对象
Spring Security 并发支持中最基本的构建块之一是 DelegatingSecurityContextRunnable。
它包装一个委托的 Runnable,在执行委托任务前,使用指定的 SecurityContextHolder 初始化 SecurityContext。
随后,它会调用该委托的 Runnable,并在执行完成后确保清除 SecurityContextHolder。
DelegatingSecurityContextRunnable 的大致实现如下所示:
public void run() {
try {
SecurityContextHolder.setContext(securityContext);
delegate.run();
} finally {
SecurityContextHolder.clearContext();
}
}
虽然非常简单,但它使得在Thread之间无缝传输SecurityContext成为可能。
这一点很重要,因为在大多数情况下,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 的存在。 -
从
SecurityContext中获取我们希望使用的SecurityContextHolder,并初始化DelegatingSecurityContextRunnable。 -
使用
DelegatingSecurityContextRunnable来创建一个Thread。 -
启动我们创建的
Thread。
由于通常会使用来自 DelegatingSecurityContextRunnable 的 SecurityContext 来创建一个 SecurityContextHolder,因此提供了一个快捷构造函数。
以下代码与前面的代码具有相同的效果:
Runnable originalRunnable = new Runnable() {
public void run() {
// invoke secured service
}
};
DelegatingSecurityContextRunnable wrappedRunnable =
new DelegatingSecurityContextRunnable(originalRunnable);
new Thread(wrappedRunnable).start();
我们现有的代码使用起来很简单,但仍需要了解我们正在使用 Spring Security。
在下一节中,我们将探讨如何利用 DelegatingSecurityContextExecutor 来隐藏我们正在使用 Spring Security 这一事实。
委托安全上下文执行器
在上一节中,我们发现使用 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,它负责将传入 execute 方法的任何 Runnable 用 DelegatingSecurityContextRunnable 进行包装。
随后,它将包装后的 Runnable 传递给 delegateExecutor。
在这种情况下,每个提交到我们 DelegatingSecurityContextExecutor 的 Runnable 都使用相同的 SecurityContext。
如果我们运行需要由具有提升权限的用户执行的后台任务,这将非常有用。
* 此时,您可能会问自己:“这如何使我的代码对 Spring Security 一无所知?”与其在我们的代码中创建 SecurityContext 和 DelegatingSecurityContextExecutor,不如注入一个已经初始化好的 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。
这意味着我们运行 Runnable 时所使用的用户,与调用 executor.execute(Runnable) 代码的用户是相同的。
Spring Security 并发类
有关与 Java 并发 API 和 Spring 任务抽象的更多集成,请参阅Javadoc。 一旦您理解了前面的代码,这些内容就很容易理解了。