From 066a07875c6145f93c516c4bd0599ba9b6aff422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Thu, 28 May 2026 15:15:14 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=20=E5=B0=86?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E5=86=85=E7=9A=84=20sse=20ws=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=B5=B0=E8=AE=A4=E8=AF=81=E5=A4=84=E7=90=86=E5=99=A8?= =?UTF-8?q?=20=E4=B8=8D=E5=86=8D=E8=87=AA=E8=A1=8C=E6=8E=92=E9=99=A4?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConditionalOnMessageTransport.java | 28 +++++++++++ .../condition/MessageTransportCondition.java | 38 +++++++++++++++ .../push/config/MessageSseConfiguration.java | 4 +- .../config/MessageWebSocketConfiguration.java | 4 +- .../common/push/controller/SseController.java | 21 +++++++-- .../interceptor/PlusWebSocketInterceptor.java | 47 +++---------------- .../config/SecurityConfiguration.java | 25 ++++++++++ .../dromara/gateway/filter/AuthFilter.java | 26 +++++++++- script/config/nacos/ruoyi-gateway.yml | 2 +- 9 files changed, 144 insertions(+), 51 deletions(-) create mode 100644 ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/annotation/ConditionalOnMessageTransport.java create mode 100644 ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/condition/MessageTransportCondition.java diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/annotation/ConditionalOnMessageTransport.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/annotation/ConditionalOnMessageTransport.java new file mode 100644 index 000000000..3a2bed5f1 --- /dev/null +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/annotation/ConditionalOnMessageTransport.java @@ -0,0 +1,28 @@ +package org.dromara.common.push.annotation; + +import org.dromara.common.push.condition.MessageTransportCondition; +import org.springframework.context.annotation.Conditional; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 按消息推送传输方式启用组件。 + * + * @author Lion Li + */ +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Conditional(MessageTransportCondition.class) +public @interface ConditionalOnMessageTransport { + + /** + * 传输方式:sse / websocket。 + */ + String value(); + +} diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/condition/MessageTransportCondition.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/condition/MessageTransportCondition.java new file mode 100644 index 000000000..4fe26aa05 --- /dev/null +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/condition/MessageTransportCondition.java @@ -0,0 +1,38 @@ +package org.dromara.common.push.condition; + +import org.dromara.common.push.annotation.ConditionalOnMessageTransport; +import org.dromara.common.push.enums.MessageTransportEnum; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import java.util.Map; + +/** + * 消息推送传输方式条件判断。 + * + * @author Lion Li + */ +public class MessageTransportCondition implements Condition { + + /** + * 判断当前消息推送配置是否匹配注解声明的传输方式。 + * + * @param context 条件上下文 + * @param metadata 注解元数据 + * @return 是否匹配 + */ + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + Map attributes = metadata.getAnnotationAttributes(ConditionalOnMessageTransport.class.getName()); + if (attributes == null) { + return true; + } + + Boolean enabled = context.getEnvironment().getProperty("message.enabled", Boolean.class, true); + String transport = context.getEnvironment().getProperty("message.transport", MessageTransportEnum.SSE.getCode()); + String expected = (String) attributes.get("value"); + return enabled && expected.equalsIgnoreCase(transport); + } + +} diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java index c67ec5d96..2a9b21f64 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageSseConfiguration.java @@ -1,11 +1,11 @@ package org.dromara.common.push.config; +import org.dromara.common.push.annotation.ConditionalOnMessageTransport; import org.dromara.common.push.controller.SseController; import org.dromara.common.push.core.SseEmitterSessionManager; import org.dromara.common.push.listener.MessageTopicListener; import org.dromara.common.push.properties.MessageProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Bean; import java.util.concurrent.ScheduledExecutorService; @@ -16,7 +16,7 @@ import java.util.concurrent.ScheduledExecutorService; * @author Lion Li */ @AutoConfiguration(after = MessageAutoConfiguration.class) -@ConditionalOnExpression("'${message.enabled:true}'.equalsIgnoreCase('true') && '${message.transport:sse}'.equalsIgnoreCase('sse')") +@ConditionalOnMessageTransport("sse") public class MessageSseConfiguration { /** diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java index 8c67f2114..6a899577c 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/config/MessageWebSocketConfiguration.java @@ -1,12 +1,12 @@ package org.dromara.common.push.config; +import org.dromara.common.push.annotation.ConditionalOnMessageTransport; import org.dromara.common.push.core.WebSocketSessionManager; import org.dromara.common.push.handler.PlusWebSocketHandler; import org.dromara.common.push.interceptor.PlusWebSocketInterceptor; import org.dromara.common.push.listener.MessageTopicListener; import org.dromara.common.push.properties.MessageProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Bean; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.annotation.EnableWebSocket; @@ -22,7 +22,7 @@ import java.util.concurrent.ScheduledExecutorService; */ @EnableWebSocket @AutoConfiguration(after = MessageAutoConfiguration.class) -@ConditionalOnExpression("'${message.enabled:true}'.equalsIgnoreCase('true') && '${message.transport:sse}'.equalsIgnoreCase('websocket')") +@ConditionalOnMessageTransport("websocket") public class MessageWebSocketConfiguration { /** diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/controller/SseController.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/controller/SseController.java index d89433345..f23c17fae 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/controller/SseController.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/controller/SseController.java @@ -2,8 +2,10 @@ package org.dromara.common.push.controller; import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.stp.StpUtil; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; +import org.dromara.common.push.annotation.ConditionalOnMessageTransport; import org.dromara.common.push.core.SseEmitterSessionManager; import org.dromara.common.satoken.utils.LoginHelper; import org.springframework.beans.factory.DisposableBean; @@ -18,6 +20,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; * @author Lion Li */ @RestController +@ConditionalOnMessageTransport("sse") @RequiredArgsConstructor public class SseController implements DisposableBean { @@ -29,10 +32,8 @@ public class SseController implements DisposableBean { * @return SSE 发射器 */ @GetMapping(value = "${message.path:/resource/message}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter connect() { - if (!StpUtil.isLogin()) { - return null; - } + public SseEmitter connect(HttpServletResponse response) { + prepareSseResponse(response); String tokenValue = StpUtil.getTokenValue(); Long userId = LoginHelper.getUserId(); return sessionManager.connect(userId, tokenValue); @@ -52,6 +53,18 @@ public class SseController implements DisposableBean { return R.ok(); } + /** + * 设置 SSE 响应头,覆盖统一鉴权成功路径中的默认 JSON 响应类型。 + * + * @param response 当前响应 + */ + private void prepareSseResponse(HttpServletResponse response) { + response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE); + response.setCharacterEncoding("UTF-8"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("X-Accel-Buffering", "no"); + } + // 以下为demo仅供参考 禁止使用 请在业务逻辑中使用工具发送而不是用接口发送 // /** // * 向特定用户发送消息 diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java index 4bc1461e5..b6eb8b03b 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/interceptor/PlusWebSocketInterceptor.java @@ -1,9 +1,6 @@ package org.dromara.common.push.interceptor; -import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.stp.StpUtil; -import lombok.extern.slf4j.Slf4j; -import org.dromara.common.core.utils.StringUtils; import org.dromara.common.push.constant.MessageConstants; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.system.api.model.LoginUser; @@ -11,7 +8,6 @@ import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; -import org.springframework.web.util.UriComponentsBuilder; import java.util.Map; @@ -20,12 +16,10 @@ import java.util.Map; * * @author Lion Li */ -@Slf4j public class PlusWebSocketInterceptor implements HandshakeInterceptor { /** - * 握手前拦截(核心认证逻辑) - * 校验登录状态、Token、客户端ID,认证通过才允许建立 WebSocket 连接 + * 握手前提取统一鉴权后的用户信息。 * * @param attributes 用于传递到 WebSocketSession 的属性集合 * @return 是否允许握手(true=允许,false=拒绝) @@ -33,40 +27,11 @@ public class PlusWebSocketInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { - try { - // 1. 获取当前登录用户与 Token - LoginUser loginUser = LoginHelper.getLoginUser(); - String tokenValue = StpUtil.getTokenValue(); - - // 2. 未登录直接拒绝握手 - if (loginUser == null || StringUtils.isBlank(tokenValue)) { - return false; - } - - // 3. 校验客户端ID(防止多端冒用) - String headerCid = request.getHeaders().getFirst(LoginHelper.CLIENT_KEY); - String paramCid = UriComponentsBuilder.fromUri(request.getURI()) - .build() - .getQueryParams() - .getFirst(LoginHelper.CLIENT_KEY); - Object clientExtra = StpUtil.getExtra(LoginHelper.CLIENT_KEY); - - // 客户端ID必须与请求头/参数中的一致,否则拒绝连接 - if (clientExtra == null || !StringUtils.equalsAny(clientExtra.toString(), headerCid, paramCid)) { - throw NotLoginException.newInstance(StpUtil.getLoginType(), - "-100", "客户端ID与Token不匹配", - StpUtil.getTokenValue()); - } - - // 4. 认证通过,将用户信息存入会话属性,供后续 WebSocketHandler 使用 - attributes.put(MessageConstants.LOGIN_USER_KEY, loginUser); - attributes.put(MessageConstants.LOGIN_TOKEN_KEY, tokenValue); - return true; - } catch (NotLoginException e) { - // 认证失败,记录日志并拒绝连接 - log.error("WebSocket 认证失败'{}',无法访问系统资源", e.getMessage()); - return false; - } + LoginUser loginUser = LoginHelper.getLoginUser(); + String tokenValue = StpUtil.getTokenValue(); + attributes.put(MessageConstants.LOGIN_USER_KEY, loginUser); + attributes.put(MessageConstants.LOGIN_TOKEN_KEY, tokenValue); + return true; } /** diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java index 92c9e0012..0115b73f9 100644 --- a/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java +++ b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfiguration.java @@ -2,20 +2,26 @@ package org.dromara.common.security.config; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.filter.SaTokenContextFilterForJakartaServlet; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.util.SaResult; import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.DispatcherType; import jakarta.servlet.http.HttpServletResponse; import org.dromara.common.core.constant.HttpStatus; import org.dromara.common.core.utils.ServletUtils; import org.dromara.common.core.utils.SpringUtils; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.EnumSet; + /** * 权限安全配置 * @@ -24,6 +30,25 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @AutoConfiguration public class SecurityConfiguration implements WebMvcConfigurer { + /** + * 重新注册 Sa-Token 上下文过滤器,使其覆盖 Servlet 异步分发。 + * + * @param filter Sa-Token 官方上下文过滤器 + * @return 过滤器注册配置 + */ + @Bean + public FilterRegistrationBean saTokenContextFilterRegistration( + SaTokenContextFilterForJakartaServlet filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(filter); + registration.setName("saTokenContextFilterForServlet"); + registration.addUrlPatterns("/*"); + registration.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR)); + registration.setAsyncSupported(true); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } + /** * 注册sa-token的拦截器 */ diff --git a/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java b/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java index 9699d9158..8d5574638 100644 --- a/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java +++ b/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java @@ -3,12 +3,14 @@ package org.dromara.gateway.filter; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.filter.SaTokenContextFilterForJakartaServlet; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.DispatcherType; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.dromara.common.core.constant.HttpStatus; @@ -18,11 +20,14 @@ import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.gateway.config.properties.IgnoreWhiteProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.EnumSet; import java.util.List; /** @@ -41,6 +46,25 @@ public class AuthFilter implements WebMvcConfigurer { this.ignoreWhite = ignoreWhite; } + /** + * 重新注册 Sa-Token 上下文过滤器,使其覆盖 Servlet 异步分发。 + * + * @param filter Sa-Token 官方上下文过滤器 + * @return 过滤器注册配置 + */ + @Bean + public FilterRegistrationBean saTokenContextFilterRegistration( + SaTokenContextFilterForJakartaServlet filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(filter); + registration.setName("saTokenContextFilterForServlet"); + registration.addUrlPatterns("/*"); + registration.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR)); + registration.setAsyncSupported(true); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handler -> SaRouter.match("/**") @@ -62,7 +86,7 @@ public class AuthFilter implements WebMvcConfigurer { validateClientAccessRules(request); }))) .addPathPatterns("/**") - .excludePathPatterns("/favicon.ico", "/actuator", "/actuator/**", "/resource/sse" , "/error"); + .excludePathPatterns("/favicon.ico", "/actuator", "/actuator/**", "/error"); } /** diff --git a/script/config/nacos/ruoyi-gateway.yml b/script/config/nacos/ruoyi-gateway.yml index 3a9bc5f30..50bf6a860 100644 --- a/script/config/nacos/ruoyi-gateway.yml +++ b/script/config/nacos/ruoyi-gateway.yml @@ -10,7 +10,7 @@ security: - /auth/register - /auth/tenant/list - /resource/sms/code - - /resource/sse/close + - /resource/message/close - /*/v3/api-docs - /*/error - /csrf