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

文章目录

    • 背景
    • [一、 问题复现](#一、 问题复现)
    • [二、 深度排查:为什么会报错?](#二、 深度排查:为什么会报错?)
      • [1. 执行流程分析](#1. 执行流程分析)
      • [2. 根本原因](#2. 根本原因)
    • [三、 解决方案](#三、 解决方案)
    • [四、 安全性思考:这样做会有漏洞吗?](#四、 安全性思考:这样做会有漏洞吗?)
      • [1. ASYNC 无法伪造](#1. ASYNC 无法伪造)
      • [2. 逻辑闭环](#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 拦截器:

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

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

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

报错堆栈关键信息如下:

text 复制代码
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 配置类:

java 复制代码
@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 -> 拦截器放行。

这就好比机场安检:

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

五、 相关原理知识

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


六、 总结

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

最佳实践建议:

  1. 入口处取值 :在 Controller 方法第一行 Object loginId = StpUtil.getLoginId();。
  2. 全链路传参 :将 loginId 显式传给 Service 层,严禁在 Flux 流内部(异步线程)调用 StpUtil。
  3. 拦截器适配 :务必在拦截器中排除 DispatcherType.ASYNC,这是混合架构下的标准解法。

希望这篇博客能帮助遇到同样问题的朋友少走弯路!

相关推荐
forest_0911 分钟前
Agent 系列 21|部署、发布与运维:把 Agent 变成一个能上线、能放量、能撤回的服务
人工智能·机器学习·架构·prompt·agi
用户127653018141 小时前
规则引擎优先、模型兜底:我把三个流程做成接口后的取舍
架构
海宇服务3 小时前
零信任架构实战:基于海宇风控经营异常预警构建自动化央企物资采购供应商资信排查网关
运维·人工智能·架构·自动化
OxYGC3 小时前
Spring 面经系列 · 第 6 篇:Spring MVC 请求全流程、父子容器与零配置原理
java·spring·mvc
加宝的起点雷达3 小时前
多个 AI 编程代理之间,上下文怎么共享:三类做法和三个坑
人工智能·架构
小尹哥-程序员4 小时前
第9集:Spring AI Advisor链:像Spring MVC拦截器一样优雅
人工智能·spring·mvc
9527!到!5 小时前
Springboot @value读取配置参数踩坑&推荐解决方案
java·spring boot·spring
weixin_750330236 小时前
GEO获客与电商短视频:AI销售架构的选型分析
人工智能·架构·电商带货短视频·geo搜索排名
leo在掘金6 小时前
纯Rust重写Photoshop,一个月7.2万Star
后端·架构
HelloWorld0016 小时前
百万 QPS 流量压测不垮!基于 Spring Cloud Gateway + Sentinel 打造分布式动态限流与灰度分流中枢(源码+避坑实战)
架构