解决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-webspring-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)

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

通过分析堆栈日志,我们发现了几个关键线索:AsyncContextImplAsyncRunnable。这说明问题发生在 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,这是混合架构下的标准解法。

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

相关推荐
陈天伟教授4 分钟前
智能体架构:大语言模型驱动的自主系统深度解析与演进研究(一)
人工智能·语言模型·架构
掘根2 小时前
【微服务即时通讯项目】系统联调
微服务·云原生·架构
tianbaolc2 小时前
Claude Code 源码剖析 模块一 · 第六节:autoDream 自动记忆整合
人工智能·ai·架构·claude code
九皇叔叔3 小时前
003-SpringSecurity-Demo 统一响应类
java·javascript·spring·springsecurity
小二·3 小时前
零信任架构深度实践:从身份到数据的全链路零信任实施指南
架构
@不误正业3 小时前
AI Agent多轮对话管理:3大架构源码级实现与性能对比(附鸿蒙实战)
人工智能·架构·harmonyos
q5431470875 小时前
Partition架构
架构
小程故事多_805 小时前
Anthropic 内部架构曝光,Claude Code 如何用 Harness 驾驭强智能
人工智能·架构·aigc·harness