diff --git a/.gitignore b/.gitignore index 667aaef..920696c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ build/ ### VS Code ### .vscode/ +/log/ diff --git a/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java b/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java index f386366..93c9445 100644 --- a/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java +++ b/src/main/java/com/onekeycall/videotablet/config/WebSocketConfig.java @@ -1,10 +1,11 @@ package com.onekeycall.videotablet.config; -import com.onekeycall.videotablet.handler.CustomWebSocketHandler; -import com.onekeycall.videotablet.interceptor.AuthHandshakeInterceptor; +import com.onekeycall.videotablet.handler.WebSocketTabletHandler; +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 lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; @@ -17,12 +18,17 @@ public class WebSocketConfig implements WebSocketConfigurer { private final JwtUtil jwtUtil; - private final CustomWebSocketHandler webSocketHandler; + private final WebSocketTabletHandler webSocketTabletHandler; + private final WebSocketUserHandler webSocketUserHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(webSocketHandler, "/ws/tablet_ws") // 客户端连接端点 + registry.addHandler(webSocketTabletHandler, "/ws/tablet_ws") // 客户端连接端点 .setAllowedOrigins("*") // 允许跨域 - .addInterceptors(new AuthHandshakeInterceptor(jwtUtil)); // 握手拦截器(如JWT校验) + .addInterceptors(new HandshakeTabletInterceptor(jwtUtil)); // 握手拦截器(如JWT校验) + + registry.addHandler(webSocketUserHandler, "/ws/user_ws") // 客户端连接端点 + .setAllowedOrigins("*") // 允许跨域 + .addInterceptors(new HandshakeUserInterceptor(jwtUtil)); // 握手拦截器(如JWT校验) } } diff --git a/src/main/java/com/onekeycall/videotablet/controller/WebSocketController.java b/src/main/java/com/onekeycall/videotablet/controller/WebSocketController.java index 912c168..3bd4e61 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/WebSocketController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/WebSocketController.java @@ -1,6 +1,6 @@ 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.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -11,9 +11,9 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/api/ws") public class WebSocketController { - private final CustomWebSocketHandler webSocketHandler; + private final WebSocketTabletHandler webSocketHandler; - public WebSocketController(CustomWebSocketHandler webSocketHandler) { + public WebSocketController(WebSocketTabletHandler webSocketHandler) { this.webSocketHandler = webSocketHandler; } diff --git a/src/main/java/com/onekeycall/videotablet/handler/CustomWebSocketHandler.java b/src/main/java/com/onekeycall/videotablet/handler/WebSocketTabletHandler.java similarity index 93% rename from src/main/java/com/onekeycall/videotablet/handler/CustomWebSocketHandler.java rename to src/main/java/com/onekeycall/videotablet/handler/WebSocketTabletHandler.java index 49a12de..d1c2c25 100644 --- a/src/main/java/com/onekeycall/videotablet/handler/CustomWebSocketHandler.java +++ b/src/main/java/com/onekeycall/videotablet/handler/WebSocketTabletHandler.java @@ -2,26 +2,22 @@ package com.onekeycall.videotablet.handler; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.springframework.util.MultiValueMap; 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 org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; -import java.net.URI; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Component -public class CustomWebSocketHandler extends TextWebSocketHandler { +public class WebSocketTabletHandler extends TextWebSocketHandler { // 存储活跃会话(线程安全) private final Map sessions = new ConcurrentHashMap<>(); - @Override public void afterConnectionEstablished(WebSocketSession session) throws IOException { // URI uri = session.getUri(); diff --git a/src/main/java/com/onekeycall/videotablet/handler/WebSocketUserHandler.java b/src/main/java/com/onekeycall/videotablet/handler/WebSocketUserHandler.java new file mode 100644 index 0000000..d88b720 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/handler/WebSocketUserHandler.java @@ -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 sessions = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws IOException { +// URI uri = session.getUri(); +// if (uri == null) { +// return; +// } +// MultiValueMap 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); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/onekeycall/videotablet/interceptor/AuthHandshakeInterceptor.java b/src/main/java/com/onekeycall/videotablet/interceptor/HandshakeTabletInterceptor.java similarity index 93% rename from src/main/java/com/onekeycall/videotablet/interceptor/AuthHandshakeInterceptor.java rename to src/main/java/com/onekeycall/videotablet/interceptor/HandshakeTabletInterceptor.java index c366859..557da0e 100644 --- a/src/main/java/com/onekeycall/videotablet/interceptor/AuthHandshakeInterceptor.java +++ b/src/main/java/com/onekeycall/videotablet/interceptor/HandshakeTabletInterceptor.java @@ -12,11 +12,11 @@ import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.Map; @Slf4j -public class AuthHandshakeInterceptor implements HandshakeInterceptor { +public class HandshakeTabletInterceptor implements HandshakeInterceptor { private final JwtUtil jwtUtil; - public AuthHandshakeInterceptor(JwtUtil jwtUtil) { + public HandshakeTabletInterceptor(JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } diff --git a/src/main/java/com/onekeycall/videotablet/interceptor/HandshakeUserInterceptor.java b/src/main/java/com/onekeycall/videotablet/interceptor/HandshakeUserInterceptor.java new file mode 100644 index 0000000..7581ea4 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/interceptor/HandshakeUserInterceptor.java @@ -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 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) { + + } +} diff --git a/src/main/resources/application-debug.properties b/src/main/resources/application-debug.properties index c8f7365..2600656 100644 --- a/src/main/resources/application-debug.properties +++ b/src/main/resources/application-debug.properties @@ -39,9 +39,9 @@ jwt.refresh-expire=2592000000 jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' # 指定日志文件名(项目根目录生成) -logging.file.name=app.log +logging.file.name=log/onekeycall_video_tablet.log # 或指定日志目录(目录下生成 spring.log) -logging.file.path=/var/log/myapp +logging.file.path=/var/log/onekeycall # 设置日志级别 logging.level.root=INFO diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 0cfa50f..1637d26 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -39,9 +39,9 @@ jwt.refresh-expire=2592000000 jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' # 指定日志文件名(项目根目录生成) -logging.file.name=app.log +logging.file.name=log/onekeycall_video_tablet.log # 或指定日志目录(目录下生成 spring.log) -logging.file.path=/var/log/myapp +logging.file.path=/var/log/onekeycall # 设置日志级别 logging.level.root=INFO diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index ad4ee0c..e04615e 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -39,9 +39,9 @@ jwt.refresh-expire=2592000000 jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' # 指定日志文件名(项目根目录生成) -logging.file.name=app.log +logging.file.name=log/onekeycall_video_tablet.log # 或指定日志目录(目录下生成 spring.log) -logging.file.path=/var/log/myapp +logging.file.path=/var/log/onekeycall # 设置日志级别 logging.level.root=INFO