Spring Security SecurityContext无法传递到Kotlin协程,调用含suspend方法的Controller接口返回UNAUTHORIZED
我之前也遇到过几乎一模一样的问题,核心原因其实很清晰:SecurityContext默认存在ThreadLocal里,但Kotlin协程的挂起/恢复会切换线程,普通ThreadLocal没法跨协程线程正确传递上下文。你的Filter确实在Servlet线程里成功设置了认证上下文,但协程控制器执行时可能被调度到另一个线程,此时ThreadLocal里的SecurityContext已经丢了,Spring Security自然会返回401。
下面是我亲测有效的解决步骤,适配Spring Boot 3.3.3 + Spring Security 6.3.x的环境:
1. 先补全协程支持依赖
Spring Security专门提供了协程上下文传播的模块,必须先添加这个依赖,否则上下文根本传不进协程:
// build.gradle.kts 中新增 dependencies { // 已有的其他依赖... implementation("org.springframework.security:spring-security-coroutines") }
2. 配置SecurityContext的传播策略
在你的SecurityConfig里,把SecurityContextHolder的策略改成MODE_INHERITABLETHREADLOCAL,这样Servlet线程的ThreadLocal上下文能被后续的协程线程继承,配合上面的依赖就能完成跨线程的上下文传递:
@Configuration @EnableWebSecurity class SecurityConfig( private val firebaseAuthenticationProvider: FirebaseAuthenticationProvider, private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, private val customAccessDeniedHandler: CustomAccessDeniedHandler ) { // 新增:配置上下文传播策略 @PostConstruct fun configureSecurityContextPropagation() { SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL) } // 你的其他Bean配置(authenticationManager、filterChain等)保持不变 }
3. 优化Filter的SecurityContext设置
建议在Filter里创建新的SecurityContext实例,避免复用旧上下文导致的意外问题:
override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { val authHeader = request.getHeader("Authorization") if (authHeader != null && authHeader.startsWith("Bearer ")) { val token = authHeader.substring(7) try { val authRequest = FirebaseAuthenticationToken(token) val authResult = authenticationManager.authenticate(authRequest) // 改用createEmptyContext创建新上下文,避免污染旧的ThreadLocal val securityContext = SecurityContextHolder.createEmptyContext() securityContext.authentication = authResult SecurityContextHolder.setContext(securityContext) logger.info("Authentication successful for user: ${authResult.name}") } catch (e: Exception) { SecurityContextHolder.clearContext() logger.error("Authentication failed: ${e.message}") } } filterChain.doFilter(request, response) }
4. 额外排查点(可选但有用)
- 可以在你的
CustomAuthenticationEntryPoint里加一行日志,打印SecurityContextHolder.getContext().authentication,确认是不是真的因为上下文丢失触发的401; - 如果你有自定义的ThreadLocal变量,别让它们干扰SecurityContext的传播;
- 协程控制器默认用
Dispatchers.IO/Dispatchers.Default调度,这些调度器都会正确传播协程上下文,不用额外配置。
为什么移除suspend就正常?
因为去掉suspend后,控制器方法会在Servlet线程里同步执行,ThreadLocal里的SecurityContext还在,所以认证能通过。但加上suspend后,方法会被调度到协程线程池,没有正确的传播策略的话,ThreadLocal上下文自然就丢了。
内容来源于stack exchange
相关产品推荐
相关产品推荐

