#967·Sa-Token

SaInterceptor 在 ASYNC 收尾派发上重复鉴权,token 在长流期间失效会中止已 committed 的流式响应

Author: bennameCreated Aug 18, 2026Updated Aug 18, 2026

1.46.0(7c986649)把上下文过滤器注册成 REQUEST+ASYNC 之后,#963 的上下文异常已解决。但同一场景还有另一半:

Spring MVC 的异步请求在响应写完后会经历一次 Servlet ASYNC 收尾派发,DispatcherServlet.doDispatch 在这次派发里再次执行 applyPreHandleSaInterceptor 于是对同一请求第二次执行注解鉴权。此时响应通常已经 committed:

  • SSE / StreamingResponseBody 长连接持续几分钟很常见(比如 AI 流式输出);
  • token 在流进行期间过期,或被注销/顶下线;
  • 收尾派发上 StpUtil.checkLoginNotLoginException
  • 响应已 committed,异常无法转成错误响应,容器只能中止连接:客户端丢整条流,Nginx 记 upstream prematurely closed connection

复现条件:@SaCheckLogin + SseEmitter/StreamingResponseBody,流的持续时间跨过 token 有效期;或者流进行中把该 token 注销。

这次重复鉴权拦不住任何东西——数据在首段鉴权通过后就已经开始下发了。建议 SaInterceptor.preHandle 跳过异步收尾派发:

java
if (request.getDispatcherType() == DispatcherType.ASYNC
        && WebAsyncUtils.getAsyncManager(request).hasConcurrentResult()) {
    return true;
}

判断条件不能放宽成 dispatcherType != REQUESTAsyncContext.dispatch("/other/path") 这类派发到另一个资源的场景,目标 handler 的鉴权注解必须执行,一律跳过会越权。

Spring Security 在 ASYNC 派发上也执行授权过滤器,但它校验的是首段捕获、经 request attribute 恢复的 SecurityContext——等效于鉴权快照,中途注销不会阻断同一请求的收尾渲染。

WebAsyncUtils 在 spring-web 里,这段判断放在 webmvc 相关模块比放在 servlet 通用模块合适。