跳到主要内容
版本:7.0.2

并发支持

DeepSeek V3 中英对照 Java’s Concurrency APIs Concurrency Support

在大多数环境中,安全信息是基于每个 Thread 存储的。这意味着当在新 Thread 上执行任务时,SecurityContext 会丢失。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();
}
}

虽然非常简单,但它使得将 SecurityContext 从一个线程无缝传输到另一个线程成为可能。这一点很重要,因为在大多数情况下,SecurityContextHolder 是基于每个线程进行操作的。例如,你可能已经使用了 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 创建一个线程

  • 启动我们创建的线程

由于使用 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);

代码执行以下步骤:

  • 创建用于 DelegatingSecurityContextExecutorSecurityContext。请注意,在本示例中我们仅手动创建了 SecurityContext。但无论我们通过何种方式或从何处获取 SecurityContext 都无关紧要(例如,如果需要,我们可以从 SecurityContextHolder 中获取)。

  • 创建一个负责执行所提交 Runnable 的 delegateExecutor。

  • 最后我们创建一个 DelegatingSecurityContextExecutor,它负责将传入 execute 方法的任何 Runnable 用 DelegatingSecurityContextRunnable 进行包装,然后将包装后的 Runnable 传递给 delegateExecutor。在此示例中,提交给 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) 时,SecurityContext 会首先由 SecurityContextHolder 获取,然后该 SecurityContext 被用来创建我们的 DelegatingSecurityContextRunnable。这意味着我们正在使用与调用 executor.execute(Runnable) 代码相同的用户来运行我们的 Runnable

Spring Security 并发类

有关与Java并发API及Spring任务抽象的其他集成,请参阅Javadoc文档。只要理解了前面的代码,这些内容基本不言自明。

  • DelegatingSecurityContextCallable
  • DelegatingSecurityContextExecutor
  • DelegatingSecurityContextExecutorService
  • DelegatingSecurityContextRunnable
  • DelegatingSecurityContextScheduledExecutorService
  • DelegatingSecurityContextSchedulingTaskExecutor
  • DelegatingSecurityContextAsyncTaskExecutor
  • DelegatingSecurityContextTaskExecutor
  • DelegatingSecurityContextTaskScheduler