乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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_INHERITABLETHREADLOCALInheritableThreadLocal需在子线程中访问安全信息的场景子线程创建时继承父线程值
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 的继承时机是什么?父线程后续修改会影响子线程吗?
上一页
SecurityContext
下一页
UsernamePasswordAuthenticationToken