初始化 V1
This commit is contained in:
73
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
Normal file
73
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-websocket</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>WebSocket 框架,支持多节点的广播</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
|
||||
因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
|
||||
如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
|
||||
-->
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 消息队列相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.amqp</groupId>
|
||||
<artifactId>spring-rabbit</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<!-- 为什么要依赖 tenant 组件?
|
||||
因为广播某个类型的用户时候,需要根据租户过滤下,避免广播到别的租户!
|
||||
-->
|
||||
<groupId>cn.iocoder.cloud</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.framework.websocket.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* WebSocket 配置项
|
||||
*
|
||||
* @author xingyu4j
|
||||
*/
|
||||
@ConfigurationProperties("yudao.websocket")
|
||||
@Data
|
||||
@Validated
|
||||
public class WebSocketProperties {
|
||||
|
||||
/**
|
||||
* WebSocket 的连接路径
|
||||
*/
|
||||
@NotEmpty(message = "WebSocket 的连接路径不能为空")
|
||||
private String path = "/ws";
|
||||
|
||||
/**
|
||||
* 消息发送器的类型
|
||||
*
|
||||
* 可选值:local、redis、rocketmq、kafka、rabbitmq
|
||||
*/
|
||||
@NotNull(message = "WebSocket 的消息发送者不能为空")
|
||||
private String senderType = "local";
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.TypeUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
|
||||
import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
|
||||
import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* JSON 格式 {@link WebSocketHandler} 实现类
|
||||
*
|
||||
* 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
|
||||
|
||||
/**
|
||||
* type 与 WebSocketMessageListener 的映射
|
||||
*/
|
||||
private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
|
||||
listenersList.forEach((Consumer<WebSocketMessageListener>)
|
||||
listener -> listeners.put(listener.getType(), listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
// 1.1 空消息,跳过
|
||||
if (message.getPayloadLength() == 0) {
|
||||
return;
|
||||
}
|
||||
// 1.2 ping 心跳消息,直接返回 pong 消息。
|
||||
if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
|
||||
session.sendMessage(new TextMessage("pong"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 解析消息
|
||||
try {
|
||||
JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
|
||||
if (jsonMessage == null) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isEmpty(jsonMessage.getType())) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
|
||||
return;
|
||||
}
|
||||
// 2.2 获得对应的 WebSocketMessageListener
|
||||
WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
|
||||
if (messageListener == null) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
|
||||
return;
|
||||
}
|
||||
// 2.3 处理消息
|
||||
Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
|
||||
Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
|
||||
Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
|
||||
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
|
||||
} catch (Throwable ex) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core.listener;
|
||||
|
||||
import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
/**
|
||||
* WebSocket 消息监听器接口
|
||||
*
|
||||
* 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
|
||||
*
|
||||
* @param <T> 泛型,消息类型
|
||||
*/
|
||||
public interface WebSocketMessageListener<T> {
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*
|
||||
* @param session Session
|
||||
* @param message 消息
|
||||
*/
|
||||
void onMessage(WebSocketSession session, T message);
|
||||
|
||||
/**
|
||||
* 获得消息类型
|
||||
*
|
||||
* @see JsonWebSocketMessage#getType()
|
||||
* @return 消息类型
|
||||
*/
|
||||
String getType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core.message;
|
||||
|
||||
import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* JSON 格式的 WebSocket 消息帧
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class JsonWebSocketMessage implements Serializable {
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*
|
||||
* 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* 消息内容
|
||||
*
|
||||
* 要求 JSON 对象
|
||||
*/
|
||||
private String content;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core.security;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 登录用户的 {@link HandshakeInterceptor} 实现类
|
||||
*
|
||||
* 流程如下:
|
||||
* 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
|
||||
* 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Exception exception) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core.security;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
|
||||
import cn.iocoder.yudao.framework.websocket.config.WebSocketProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket 的权限自定义
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
|
||||
|
||||
private final WebSocketProperties webSocketProperties;
|
||||
|
||||
@Override
|
||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers(webSocketProperties.getPath()).permitAll();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.framework.websocket.core.sender;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
|
||||
/**
|
||||
* WebSocket 消息的发送器接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface WebSocketMessageSender {
|
||||
|
||||
/**
|
||||
* 发送消息给指定用户
|
||||
*
|
||||
* @param userType 用户类型
|
||||
* @param userId 用户编号
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容,JSON 格式
|
||||
*/
|
||||
void send(Integer userType, Long userId, String messageType, String messageContent);
|
||||
|
||||
/**
|
||||
* 发送消息给指定用户类型
|
||||
*
|
||||
* @param userType 用户类型
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容,JSON 格式
|
||||
*/
|
||||
void send(Integer userType, String messageType, String messageContent);
|
||||
|
||||
/**
|
||||
* 发送消息给指定 Session
|
||||
*
|
||||
* @param sessionId Session 编号
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容,JSON 格式
|
||||
*/
|
||||
void send(String sessionId, String messageType, String messageContent);
|
||||
|
||||
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
|
||||
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
|
||||
}
|
||||
|
||||
default void sendObject(Integer userType, String messageType, Object messageContent) {
|
||||
send(userType, messageType, JsonUtils.toJsonString(messageContent));
|
||||
}
|
||||
|
||||
default void sendObject(String sessionId, String messageType, Object messageContent) {
|
||||
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user