增加用户端websocket连接端点

This commit is contained in:
2025-10-10 17:35:26 +08:00
parent b9d0deddd8
commit fc67643418
10 changed files with 151 additions and 22 deletions

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
/log/

View File

@@ -1,10 +1,11 @@
package com.onekeycall.videotablet.config; package com.onekeycall.videotablet.config;
import com.onekeycall.videotablet.handler.CustomWebSocketHandler; import com.onekeycall.videotablet.handler.WebSocketTabletHandler;
import com.onekeycall.videotablet.interceptor.AuthHandshakeInterceptor; import com.onekeycall.videotablet.handler.WebSocketUserHandler;
import com.onekeycall.videotablet.interceptor.HandshakeTabletInterceptor;
import com.onekeycall.videotablet.interceptor.HandshakeUserInterceptor;
import com.onekeycall.videotablet.utils.JwtUtil; import com.onekeycall.videotablet.utils.JwtUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
@@ -17,12 +18,17 @@ public class WebSocketConfig implements WebSocketConfigurer {
private final JwtUtil jwtUtil; private final JwtUtil jwtUtil;
private final CustomWebSocketHandler webSocketHandler; private final WebSocketTabletHandler webSocketTabletHandler;
private final WebSocketUserHandler webSocketUserHandler;
@Override @Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/tablet_ws") // 客户端连接端点 registry.addHandler(webSocketTabletHandler, "/ws/tablet_ws") // 客户端连接端点
.setAllowedOrigins("*") // 允许跨域 .setAllowedOrigins("*") // 允许跨域
.addInterceptors(new AuthHandshakeInterceptor(jwtUtil)); // 握手拦截器如JWT校验 .addInterceptors(new HandshakeTabletInterceptor(jwtUtil)); // 握手拦截器如JWT校验
registry.addHandler(webSocketUserHandler, "/ws/user_ws") // 客户端连接端点
.setAllowedOrigins("*") // 允许跨域
.addInterceptors(new HandshakeUserInterceptor(jwtUtil)); // 握手拦截器如JWT校验
} }
} }

View File

@@ -1,6 +1,6 @@
package com.onekeycall.videotablet.controller; package com.onekeycall.videotablet.controller;
import com.onekeycall.videotablet.handler.CustomWebSocketHandler; import com.onekeycall.videotablet.handler.WebSocketTabletHandler;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -11,9 +11,9 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/api/ws") @RequestMapping("/api/ws")
public class WebSocketController { public class WebSocketController {
private final CustomWebSocketHandler webSocketHandler; private final WebSocketTabletHandler webSocketHandler;
public WebSocketController(CustomWebSocketHandler webSocketHandler) { public WebSocketController(WebSocketTabletHandler webSocketHandler) {
this.webSocketHandler = webSocketHandler; this.webSocketHandler = webSocketHandler;
} }

View File

@@ -2,26 +2,22 @@ package com.onekeycall.videotablet.handler;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@Component @Component
public class CustomWebSocketHandler extends TextWebSocketHandler { public class WebSocketTabletHandler extends TextWebSocketHandler {
// 存储活跃会话线程安全 // 存储活跃会话线程安全
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override @Override
public void afterConnectionEstablished(WebSocketSession session) throws IOException { public void afterConnectionEstablished(WebSocketSession session) throws IOException {
// URI uri = session.getUri(); // URI uri = session.getUri();

View File

@@ -0,0 +1,78 @@
package com.onekeycall.videotablet.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class WebSocketUserHandler extends TextWebSocketHandler {
// 存储活跃会话(线程安全)
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws IOException {
// URI uri = session.getUri();
// if (uri == null) {
// return;
// }
// MultiValueMap<String, String> queryParams = UriComponentsBuilder.fromUri(uri).build().getQueryParams();
// String sn = queryParams.getFirst("sn");
// log.info("sn = " + sn);
// if (sn == null) {
// session.close(CloseStatus.BAD_DATA.withReason("Missing device SN"));
// return;
// }
String sessionId = session.getId();
String sn = (String) session.getAttributes().get("sn");
sessions.put(sn, session);
log.info("✅ 连接建立: ID={},sn={},当前连接数: {}", sessionId, sn, sessions.size());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
log.info("📩 收到消息: {}", payload);
// 示例:回复客户端
String reply = "服务器已接收: " + payload;
try {
session.sendMessage(new TextMessage(reply));
} catch (IOException e) {
log.error("回复失败: ID={}", session.getId(), e);
}
// 可选:广播给所有客户端
// broadcast("用户" + sessionId + "说: " + payload);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String sessionId = session.getId();
String sn = (String) session.getAttributes().get("sn");
sessions.remove(sn);
log.info("❌ 连接关闭: ID={},sn={},Code: {},原因: {},当前连接数: {}", sessionId, sn, status.getCode(), status.getReason(), sessions.size());
}
// 广播消息给所有客户端
public void broadcast(String message) {
sessions.values().stream()
.filter(WebSocketSession::isOpen)
.forEach(session -> {
try {
session.sendMessage(new TextMessage(message));
} catch (IOException e) {
log.error("广播失败: ID={}", session.getId(), e);
}
});
}
}

View File

@@ -12,11 +12,11 @@ import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map; import java.util.Map;
@Slf4j @Slf4j
public class AuthHandshakeInterceptor implements HandshakeInterceptor { public class HandshakeTabletInterceptor implements HandshakeInterceptor {
private final JwtUtil jwtUtil; private final JwtUtil jwtUtil;
public AuthHandshakeInterceptor(JwtUtil jwtUtil) { public HandshakeTabletInterceptor(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil; this.jwtUtil = jwtUtil;
} }

View File

@@ -0,0 +1,48 @@
package com.onekeycall.videotablet.interceptor;
import com.onekeycall.videotablet.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
@Slf4j
public class HandshakeUserInterceptor implements HandshakeInterceptor {
private final JwtUtil jwtUtil;
public HandshakeUserInterceptor(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
String authHeader = request.getHeaders().getFirst("Authorization");
String deviceId = request.getHeaders().getFirst("Device-ID");
String userId = request.getHeaders().getFirst("User-ID");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpRequest = servletRequest.getServletRequest();
String sn = httpRequest.getParameter("sn");
attributes.put("sn", sn);
log.info("Intercepted - sn: " + sn);
}
if (authHeader == null || !authHeader.startsWith("Bearer ") || userId == null) {
return false;
}
String token = authHeader.substring(7); // 去掉 "Bearer " 前缀
return jwtUtil.validateAccessToken(userId, token, deviceId); // 自定义校验逻辑
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -39,9 +39,9 @@ jwt.refresh-expire=2592000000
jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!'
# 指定日志文件名(项目根目录生成) # 指定日志文件名(项目根目录生成)
logging.file.name=app.log logging.file.name=log/onekeycall_video_tablet.log
# 或指定日志目录(目录下生成 spring.log # 或指定日志目录(目录下生成 spring.log
logging.file.path=/var/log/myapp logging.file.path=/var/log/onekeycall
# 设置日志级别 # 设置日志级别
logging.level.root=INFO logging.level.root=INFO

View File

@@ -39,9 +39,9 @@ jwt.refresh-expire=2592000000
jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!'
# 指定日志文件名(项目根目录生成) # 指定日志文件名(项目根目录生成)
logging.file.name=app.log logging.file.name=log/onekeycall_video_tablet.log
# 或指定日志目录(目录下生成 spring.log # 或指定日志目录(目录下生成 spring.log
logging.file.path=/var/log/myapp logging.file.path=/var/log/onekeycall
# 设置日志级别 # 设置日志级别
logging.level.root=INFO logging.level.root=INFO

View File

@@ -39,9 +39,9 @@ jwt.refresh-expire=2592000000
jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!'
# 指定日志文件名(项目根目录生成) # 指定日志文件名(项目根目录生成)
logging.file.name=app.log logging.file.name=log/onekeycall_video_tablet.log
# 或指定日志目录(目录下生成 spring.log # 或指定日志目录(目录下生成 spring.log
logging.file.path=/var/log/myapp logging.file.path=/var/log/onekeycall
# 设置日志级别 # 设置日志级别
logging.level.root=INFO logging.level.root=INFO