36. Concurrency Support

在大多数环境中,安全性是按Thread进行存储的。这意味着当在新的Thread上完成工作时,SecurityContext丢失了。 Spring Security 提供了一些基础架构来帮助用户轻松实现这一点。 Spring Security 提供了用于在多线程环境中使用 Spring Security 的底层抽象。实际上,这就是 Spring Security 与第 15.2.4 节“ AsyncContext.start(Runnable)”第 37.4 节“ Spring MVC 异步集成”集成的基础。

36.1 DelegatingSecurityContextRunnable

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

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

尽管非常简单,但可以无缝地将 SecurityContext 从一个线程传输到另一个线程。这很重要,因为在大多数情况下,SecurityContextHolder 会基于每个线程进行操作。例如,您可能已经使用 Spring Security 的第 41.4.1 节“<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创建线程

  • 启动我们创建的线程

由于用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 的事实。

36.2 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 s 的委托 Actuator

  • 最后,我们创建一个DelegatingSecurityContextExecutor,它负责使用DelegatingSecurityContextRunnable来包装传递给 execute 方法的所有 Runnable。然后,它将包装的 Runnable 传递给委托 Actuator。在这种情况下,提交给我们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。在此示例中,使用同一用户执行每个线程。如果我们想在调用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

36.3 Spring 安全性并发类

有关与 Java 并发 API 和 Spring Task 抽象的其他集成,请参考 Javadoc。一旦理解了先前的代码,它们就非常不言自明。

  • DelegatingSecurityContextCallable

  • DelegatingSecurityContextExecutor

  • DelegatingSecurityContextExecutorService

  • DelegatingSecurityContextRunnable

  • DelegatingSecurityContextScheduledExecutorService

  • DelegatingSecurityContextSchedulingTaskExecutor

  • DelegatingSecurityContextAsyncTaskExecutor

  • DelegatingSecurityContextTaskExecutor