From e0124221dae9e51ce83fae1b84320c099fc0095f Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Thu, 25 Jun 2026 10:30:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=20=E4=BF=AE=E5=A4=8D=20SSE=20?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E8=BF=9E=E6=8E=A5=20ASYNC=20=E6=B4=BE?= =?UTF-8?q?=E5=8F=91=E6=97=B6=20SaToken=20=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E6=9C=AA=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSE 端点(/notify/user/sse/connect)连接完成/超时/异常时, Servlet 容器做 ASYNC 重派发, SaToken 上下文过滤器(OncePerRequestFilter, 仅 REQUEST 派发)不重新执行, 再次进入 SaInterceptor 抛 SaTokenContextException; 异常处理器对已锁定 text/event-stream 的响应写 JSON 又二次抛 HttpMessageNotWritableException. - SaTokenConfigure: 包装 SaInterceptor, DispatcherType.ASYNC 时直接放行 - SaExceptionHandler: SaTokenException 命中 text/event-stream 时不写 body, 正常分支显式 JSON --- .../auth/configuration/SaTokenConfigure.java | 18 ++++++++++++++++-- .../auth/handler/SaExceptionHandler.java | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/configuration/SaTokenConfigure.java b/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/configuration/SaTokenConfigure.java index dc54b4856..9253cc54b 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/configuration/SaTokenConfigure.java +++ b/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/configuration/SaTokenConfigure.java @@ -4,11 +4,15 @@ import cn.daxpay.open.platform.common.config.properties.PlatformStarterPropertie import cn.daxpay.open.platform.capability.auth.handler.SaRouteHandler; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaRouter; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -58,7 +62,17 @@ public class SaTokenConfigure implements WebMvcConfigurer { .check(saRouteHandler.check(handler)) ); // 注册路由拦截器,自定义验证规则 - registry.addInterceptor(saInterceptor) - .addPathPatterns("/**"); + // 包装一层: SSE 等异步连接(SseEmitter)在完成/超时/异常时, Servlet 容器会做 ASYNC 重派发, + // 此时 SaToken 上下文过滤器(OncePerRequestFilter, 仅 REQUEST 派发)不会重新执行, ThreadLocal 上下文为空, + // 若再次进入 SaInterceptor 会抛 SaTokenContextException. 鉴权已在初始 REQUEST 派发完成, ASYNC 派发直接放行. + registry.addInterceptor(new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (request.getDispatcherType() == DispatcherType.ASYNC) { + return true; + } + return saInterceptor.preHandle(request, response, handler); + } + }).addPathPatterns("/**"); } } diff --git a/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/handler/SaExceptionHandler.java b/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/handler/SaExceptionHandler.java index 18313ec0e..50dc39607 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/handler/SaExceptionHandler.java +++ b/daxpay-platform/daxpay-platform-capability/capability-auth/src/main/java/cn/daxpay/open/platform/capability/auth/handler/SaExceptionHandler.java @@ -6,11 +6,13 @@ import cn.daxpay.open.platform.core.rest.result.Result; import cn.daxpay.open.platform.capability.auth.exception.NotLoginException; import cn.daxpay.open.platform.capability.auth.exception.RouterCheckException; import cn.dev33.satoken.exception.SaTokenException; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -39,10 +41,21 @@ public class SaExceptionHandler { } /// sa鉴权业务异常 + /// + /// 注意: SSE 等异步端点产生的事件流响应 Content-Type 已锁定为 text/event-stream, + /// 无 JSON 消息转换器, 此时返回 Result 会抛 HttpMessageNotWritableException, 故仅返回无 body 的状态码. @ExceptionHandler(SaTokenException.class) - public Result handleBusinessException(SaTokenException ex) { + public ResponseEntity handleBusinessException(SaTokenException ex, HttpServletResponse response) { log.info(ex.getMessage(), ex); - return Res.response(CommonCode.FAIL_CODE, ex.getMessage()); + String contentType = response.getContentType(); + // SSE 事件流响应: 无 JSON 转换器, 不写 body, 避免二次异常 + if (contentType != null && contentType.contains(MediaType.TEXT_EVENT_STREAM_VALUE)) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + Result result = Res.response(CommonCode.FAIL_CODE, ex.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(result); } }