并发支持

在大多数环境中,安全上下文存储在每个线程的基础上。这意味着当在新线程上执行工作时,安全上下文会丢失。Spring Security 提供了一些基础结构来帮助更轻松地管理此问题。Spring Security 提供了用于在多线程环境中使用 Spring Security 的底层抽象。实际上,这就是 Spring Security 用于与AsyncContext.start(Runnable)Spring MVC 异步集成 集成的基础。

DelegatingSecurityContextRunnable

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

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

虽然非常简单,但它使在不同线程之间传输安全上下文变得非常顺畅。这很重要,因为在大多数情况下,SecurityContextHolder 在每个线程的基础上起作用。例如,您可能已使用 Spring Security 的<global-method-security> 支持来保护您的某个服务。现在,您可以将当前线程的安全上下文传输到调用受保护服务的线程。以下示例展示了如何操作

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

由于通常使用来自 SecurityContextHolderSecurityContext 创建 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 获取它)。* 创建一个 delegateExecutor,负责执行提交的 Runnable 对象。* 最后,我们创建一个 DelegatingSecurityContextExecutor,它负责将传递到 execute 方法的任何 Runnable 包装到 DelegatingSecurityContextRunnable 中。然后,它将包装的 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正在传播到ThreadoriginalRunnable正在运行,并且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