SecurityContextHolder
定义与作用
SecurityContextHolder 是 Spring Security 中管理安全上下文存储策略的工具类。它本身不存储数据,而是通过策略模式将 SecurityContext 的存取委托给底层实现。整个认证成功后的信息流转都依赖它:
// 获取当前线程的安全上下文
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
手写过滤器的痛点
手写登录系统时,登录成功后的用户信息存放位置五花八门:
// 痛点1:存入 HttpSession,后续代码里到处 request.getSession().getAttribute("user")
// 痛点2:存入 ThreadLocal,但子线程和异步方法里拿不到
// 痛点3:存入全局 Map,Key 用 SessionId,并发高时内存泄漏
// 痛点4:每种存法各写一套,过滤器、Controller、Service 层获取方式不一致
// 痛点5:请求结束后不清理,线程池复用时出现用户信息串台
session.setAttribute("currentUser", user);
// 另一处
ThreadLocalUserHolder.set(user);
// 又一处
UserContext.setSessionUser(sessionId, user);
SecurityContextHolder 提供统一入口和多种策略,让同一线程内的任何代码都能以相同方式获取当前用户,同时支持按场景切换存储策略。
核心原理
三种存储策略
| 策略 | 存储方式 | 适用场景 | 注意事项 |
|---|---|---|---|
MODE_THREADLOCAL(默认) | ThreadLocal | 标准 Servlet 应用 | 同一线程内共享,子线程不继承 |
MODE_INHERITABLETHREADLOCAL | InheritableThreadLocal | 需在子线程中访问安全信息的场景 | 子线程创建时继承父线程值 |
MODE_GLOBAL | 全局静态变量 | 独立控制台应用、非 Web 场景 | 多线程共用,存在并发风险 |
默认策略的工作流程
策略切换方式
// 方式1:JVM 启动参数(全局)
java -Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
// 方式2:代码设置(应用启动时)
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
// 方式3:Spring Security 配置(Lambda DSL)
http.securityContext(context -> context
.securityContextHolderStrategy(new InheritableThreadLocalSecurityContextHolderStrategy())
);
示例一:异步方法中传递 SecurityContext
场景说明
Controller 中调用 @Async 异步方法处理耗时任务,异步方法中需要获取当前用户ID记录操作日志。
操作前配置
默认 MODE_THREADLOCAL,异步方法中 SecurityContextHolder.getContext() 返回空上下文:
@Service
public class ReportService {
@Async("taskExecutor")
public void generateReport() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// auth == null!因为异步方法在新线程中执行
String username = auth != null ? auth.getName() : "anonymous"; // 总是 anonymous
// 操作日志无法记录真实操作人
}
}
操作后配置
切换为 MODE_INHERITABLETHREADLOCAL,使子线程继承父线程的安全上下文:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
);
SpringApplication.run(Application.class, args);
}
}
@Configuration
@EnableWebSecurity
@EnableAsync
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.securityContext(context -> context
.securityContextHolderStrategy(
new InheritableThreadLocalSecurityContextHolderStrategy()
)
);
}
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
public class ReportService {
@Async("taskExecutor")
public void generateReport() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 现在能拿到父线程的认证信息了
String username = auth.getName();
System.out.println("用户 " + username + " 触发了报表生成");
// 记录操作日志...
}
}
结果分析
MODE_INHERITABLETHREADLOCAL使用InheritableThreadLocal,子线程创建时自动复制父线程的SecurityContext引用- 异步方法中可以直接通过
SecurityContextHolder.getContext()获取当前用户 - 注意:如果父线程在子线程执行前调用了
clearContext()或修改了Authentication,子线程不会感知(复制发生在子线程创建时)
示例二:手动传递 SecurityContext 到自定义线程
场景说明
不使用 @Async 的线程池,而是手动 new Thread() 或 CompletableFuture,需要将安全上下文显式传递到新线程。
操作前配置
直接在新线程中访问 SecurityContextHolder,返回 null:
@RestController
public class JobController {
@GetMapping("/trigger-job")
public String triggerJob() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println("主线程用户: " + auth.getName());
new Thread(() -> {
Authentication authInThread = SecurityContextHolder.getContext().getAuthentication();
System.out.println("子线程用户: " + (authInThread == null ? "null" : authInThread.getName()));
// 输出 null
}).start();
return "已触发";
}
}
操作后配置
手动创建 SecurityContext 副本并设置到子线程:
@RestController
public class JobController {
@GetMapping("/trigger-job")
public String triggerJob() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
System.out.println("主线程用户: " + auth.getName());
// 1. 创建 SecurityContext 的副本(避免引用被修改)
SecurityContext contextCopy = SecurityContextHolder.createEmptyContext();
contextCopy.setAuthentication(auth);
new Thread(() -> {
// 2. 在子线程中设置副本
SecurityContextHolder.setContext(contextCopy);
try {
Authentication authInThread = SecurityContextHolder.getContext().getAuthentication();
System.out.println("子线程用户: " + authInThread.getName());
// 执行需要用户身份的业务...
} finally {
// 3. 必须清理,防止线程池复用串台
SecurityContextHolder.clearContext();
}
}).start();
return "已触发";
}
}
使用 DelegatingSecurityContextExecutor 简化传递:
@Bean
public Executor securityContextExecutor() {
ThreadPoolTaskExecutor delegate = new ThreadPoolTaskExecutor();
delegate.setCorePoolSize(5);
delegate.initialize();
// 包装器自动传递 SecurityContext
return new DelegatingSecurityContextExecutor(delegate);
}
@Service
public class JobService {
@Autowired
@Qualifier("securityContextExecutor")
private Executor executor;
public void submitJob(Runnable task) {
// 自动传递 SecurityContext,无需手动 setContext
executor.execute(task);
}
}
结果分析
- 手动传递时,必须在线程执行完毕后调用
clearContext(),否则线程池复用会导致用户串台 DelegatingSecurityContextExecutor是 Spring Security 提供的包装器,自动完成setContext→run→clearContext的全生命周期管理- 对于
CompletableFuture等异步框架,也可以手动包装Supplier/Runnable
易错场景:请求结束后 SecurityContext 未清理导致串台
问题描述
使用非标准 Servlet 容器(如 Netty、自定义线程池)或测试环境时,发现用户A登录后,用户B的请求显示的是用户A的身份信息。
原因分析
SecurityContextHolderFilter 默认会在请求结束时调用 SecurityContextHolder.clearContext(),但如果:
- 自定义了过滤器链,遗漏了清理逻辑
- 使用了响应式编程(WebFlux),线程切换频繁
- 手动设置了
SecurityContext但未手动清理
错误代码
public class BadFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
SecurityContextHolder.getContext().setAuthentication(someAuth);
chain.doFilter(req, res);
// 忘记 clearContext()!
}
}
正确做法
public class CustomFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
try {
// 设置上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(req, res);
} finally {
// 必须清理,即使 chain.doFilter 抛出异常也要执行
SecurityContextHolder.clearContext();
}
}
}
Spring Security 的标准过滤器已经内置了清理逻辑,不要轻易绕过 SecurityContextHolderFilter 的默认行为。
面试考点:
SecurityContextHolder的三种策略是什么?默认策略是什么?@Async异步方法中为什么拿不到SecurityContext?如何解决?- 手动传递
SecurityContext到子线程时,为什么必须clearContext()?MODE_INHERITABLETHREADLOCAL的继承时机是什么?父线程后续修改会影响子线程吗?