update 优化 将框架内的 sse ws 统一走认证处理器 不再自行排除编码处理
update 适配 snail-ai 的 spring-ai 版本过低问题 先降级spring-ai版本到m4等后续适配
This commit is contained in:
+28
@@ -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();
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package org.dromara.common.push.condition;
|
||||
|
||||
import org.dromara.common.push.annotation.ConditionalOnMessageTransport;
|
||||
import org.dromara.common.push.enums.MessageTransportEnum;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
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(@NonNull ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
package org.dromara.common.push.config;
|
||||
|
||||
import org.dromara.common.push.annotation.ConditionalOnMessageTransport;
|
||||
import org.dromara.common.push.listener.MessageTopicListener;
|
||||
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.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 {
|
||||
|
||||
/**
|
||||
|
||||
+17
-4
@@ -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仅供参考 禁止使用 请在业务逻辑中使用工具发送而不是用接口发送
|
||||
// /**
|
||||
// * 向特定用户发送消息
|
||||
|
||||
+6
-41
@@ -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<String, Object> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user