欢迎光临
我们一直在努力

解决Sa-Token在 Spring MVC + WebFlux 混合架构中流式接口报错SaTokenContext 上下文尚未初始化的问题

文章目录

    • 背景
    • 一、 问题复现
    • 二、 深度排查:为什么会报错?
      • 1. 执行流程分析
      • 2. 根本原因
    • 三、 解决方案
    • 四、 安全性思考:这样做会有漏洞吗?
      • 1. ASYNC 无法伪造
      • 2. 逻辑闭环
    • 五、 相关原理知识
    • 六、 总结

背景

  随着 AI 应用的爆发,流式接口在业务开发中变得越来越普遍。在 Spring Boot 3.x 的大背景下,很多项目采用了 Spring MVC + WebFlux 的混合架构:既保留了 Servlet 生态的成熟稳定,又利用了 WebFlux 的响应式编程能力来处理并发与流式传输。

  最近在SpringBoot 3.x项目中引入 Sa-Token(v1.44.0)进行权限认证时,遇到了一个棘手的问题:普通的同步接口一切正常,但在处理 SSE(Server-Sent Events)流式接口时,控制台疯狂报错:

cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化

本文将深度复盘该问题的成因及解决方案。


一、 问题复现

项目依赖中同时存在 spring-boot-starter-web 和 spring-boot-starter-webflux。按照 Sa-Token 的官方文档,我们配置了标准的 Spring MVC 拦截器:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/auth/login");
}
}

业务接口使用了 Flux 实现流式输出:

@PostMapping(value = "/send", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<CommonResult<XXXRespVO>> send(@RequestBody @Validated XXXReqVO reqVO) {
return xxxService.xxxSend(reqVO);
}

报错堆栈关键信息如下:

cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化
at cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff.getModelBox(…)

at cn.dev33.satoken.interceptor.SaInterceptor.preHandle(SaInterceptor.java:102)

at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:599)


二、 深度排查:为什么会报错?

通过分析堆栈日志,我们发现了几个关键线索:AsyncContextImpl 和 AsyncRunnable。这说明问题发生在 Servlet 的异步处理阶段。

1. 执行流程分析

当 Controller 返回 Flux 对象时,Spring MVC 的处理流程并非“一气呵成”,而是分为了两个阶段:

  • 第一阶段(REQUEST 阶段):

    • Tomcat 主线程接收请求。
    • 进入 SaInterceptor,此时处于同步线程,ThreadLocal 正常,鉴权成功。
    • Controller 方法执行,返回 Flux 对象。
    • Spring MVC 判定返回值为响应式类型,调用 request.startAsync() 开启异步模式,主线程结束并释放。
  • 第二阶段(ASYNC 阶段):

    • 当 Flux 流开始发射数据时,Servlet 容器会触发一次 异步分发。
    • 关键点:Spring MVC 的拦截器机制在异步分发时会再次执行。
    • 此时,Tomcat 使用的是异步线程(或复用线程),该线程中没有 Sa-Token 的 ThreadLocal 上下文。
    • SaInterceptor 再次执行 preHandle,尝试获取上下文,结果为空,抛出异常。

2. 根本原因

Sa-Token 默认基于 ThreadLocal 存储上下文。在 Servlet 规范中,异步分发会导致线程切换或上下文重置,导致 ThreadLocal 丢失。而拦截器默认对 所有分发类型(包括 ASYNC)都进行拦截,从而引发了误报。


三、 解决方案

既然异步分发阶段只是数据的“搬运工”,且鉴权已经在第一次 REQUEST 阶段完成,那么我们只需要让拦截器在异步分发时“放行”即可。

修改 SaTokenConfigure 配置类:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 【核心修复】如果是异步分发(ASYNC),直接放行
// 异步分发阶段线程上下文已丢失,但鉴权已在 REQUEST 阶段完成
if (request.getDispatcherType() == DispatcherType.ASYNC) {
return true;
}

// 以下为正常鉴权逻辑,仅在 REQUEST 阶段执行
SaRouter.match("/**")
.notMatch("/auth/login", "/error", "/swagger-ui/**", "/v3/api-docs/**")
.check(r -> StpUtil.checkLogin());

return true;
}
}).addPathPatterns("/**");
}
}

代码解析: 通过判断 request.getDispatcherType(),我们识别出当前是异步分发,直接返回 true 放行。这样就避免了在 ASYNC 阶段调用 StpUtil 导致的上下文异常。


四、 安全性思考:这样做会有漏洞吗?

可能有同学会担心:“直接在 ASYNC 阶段放行,会不会导致未鉴权的请求绕过安全检查?”

答案是:绝对不会。

1. ASYNC 无法伪造

DispatcherType 是 Servlet 容器内部维护的状态标识,并非 HTTP 请求头或参数。

  • REQUEST:凡是外部通过网络发起的请求,Tomcat 必定将其标记为 REQUEST。
  • ASYNC:只有服务器内部代码调用了 request.startAsync() 并触发分发时才会产生。

2. 逻辑闭环

  • 黑客发起攻击:请求被标记为 REQUEST -> 进入拦截器 -> 执行鉴权逻辑 -> 鉴权失败 -> 返回 401。流程结束,不可能触发 ASYNC。
  • 正常用户请求:请求被标记为 REQUEST -> 鉴权成功 -> Controller 返回 Flux -> 触发 ASYNC -> 拦截器放行。

这就好比机场安检:

  • REQUEST 阶段(安检口):必须检查证件。
  • ASYNC 阶段(候机厅内转机):你已经在安全区内了,不需要再查一次证件。

  • 五、 相关原理知识

    可以阅读我写的下面这篇文章:

    • Spring Web MVC的异步请求解读

    六、 总结

    在 Spring MVC + WebFlux 混合架构中,Sa-Token 的 ThreadLocal 模式确实会遇到挑战。通过理解 Servlet 3.1 的异步分发机制,我们可以精准地通过 DispatcherType 判断来规避此类问题。

    最佳实践建议:

  • 入口处取值:在 Controller 方法第一行 Object loginId = StpUtil.getLoginId();。
  • 全链路传参:将 loginId 显式传给 Service 层,严禁在 Flux 流内部(异步线程)调用 StpUtil。
  • 拦截器适配:务必在拦截器中排除 DispatcherType.ASYNC,这是混合架构下的标准解法。
  • 希望这篇博客能帮助遇到同样问题的朋友少走弯路!

    赞(0)
    未经允许不得转载:171主机测评 » 解决Sa-Token在 Spring MVC + WebFlux 混合架构中流式接口报错SaTokenContext 上下文尚未初始化的问题
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址