DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Spring @Async 线程读不到 SecurityContext,检查选中的执行器

异步任务需要包装实际选中的执行器,按提交时传播SecurityContext,并验证多用户复用线程的身份隔离。

HTTP 请求已经认证成功,进入 @Async 方法后,SecurityContextHolder 却没有认证身份,受保护方法报 Access forbidden: not authenticated。先检查异步任务由哪个执行器接收,以及提交任务时是否传播了调用线程的安全上下文。

Spring Security 默认按线程保存上下文。另一个线程不会自动继承当前请求的身份。改执行器 Bean 名称、换成虚拟线程,或者让入口通过 permitAll(),都不能代替身份传播。

包装实际接收任务的 AsyncTaskExecutor

DelegatingSecurityContextAsyncTaskExecutor 可以包装已有的异步执行器,在提交任务时捕获当前 SecurityContext,执行任务时安装上下文,并在结束后恢复或清理工作线程的状态。

例如项目已经有受 Spring 管理的 AsyncTaskExecutor,Bean 名称为 businessExecutor,可增加一个包装 Bean。下面是配置类中的方法片段,需导入对应类型:

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor;

@Bean("securedExecutor")
public AsyncTaskExecutor securedExecutor(
        @Qualifier("businessExecutor") AsyncTaskExecutor delegate) {
    return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
}

异步方法明确选择包装后的执行器:

@Async("securedExecutor")
public void handleJob() {
    // 项目原有的、需要调用方身份的任务逻辑
}

该片段假定项目已启用异步支持,方法通过 Spring 管理的代理调用,原执行器由容器负责初始化与关闭。它没有创建另一套不受管理的线程池,也没有更改原池的容量、队列或拒绝策略。@Async 需要导入 org.springframework.scheduling.annotation.Async。

如果代码仍指定 @Async("businessExecutor"),就绕过了包装。使用不带限定符的 @Async 时,也要检查实际选择规则,不能因新 Bean 已注册就认定所有任务都已使用它。任务没有异步生效时,应先核对代理调用,再检查上下文传播。

捕获的是任务提交时的上下文

上面的单参数构造方法按每次任务提交取得当前身份。若任务提交发生在请求上下文已经清理之后,包装器也不能找回先前的用户。记录提交点和执行点的受控身份标识即可,不要把访问令牌或完整认证对象写入日志。

另一种构造方式可以指定固定 SecurityContext,适用于明确以某个后台身份执行的任务;不能给面向多个用户的执行器固定设置一个用户,随后把所有任务都按那个身份处理。

传播使用的是上下文引用,不是会话刷新或身份对象的深复制。任务排队期间用户权限可能变化,业务需要按自己的授权策略决定是否重新校验;包装器不会自动更新已捕获的身份。

线程复用要验证两个用户之间的隔离

全局 MODE_INHERITABLETHREADLOCAL 不能作为线程池身份传播的通用修复。继承发生在创建线程时,池线程后来可能服务另一位用户;保留旧身份会让任务以错误用户执行。虚拟线程也不能作为已经正确传播认证的证据。

使用同一执行器,在隔离环境依次提交两位用户的任务,再提交没有认证身份的受控任务,核对它们分别看到的身份。还要覆盖任务抛异常、任务拒绝和线程复用,确认没有残留上一个任务的认证。

该包装主要处理安全上下文,不会一并传播 MDC、请求对象或数据库事务。确实需要其他上下文时,应分别明确它们的生命周期,不能把整个 Web 请求引用长期留在后台队列。requireExplicitSave(false) 则涉及跨请求保存安全上下文,不是这条异步传播路径的修复。

保留原执行器配置和限定符,回退时一起恢复。验收除了任务成功,还应确认错误身份不能通过授权;不应以放宽方法权限来掩盖上下文丢失。