Commit 19aa1f99 authored by ligaowei's avatar ligaowei

refactor(websocket): 优化DomSyncHandler及JWT握手拦截器实现

- 引入JwtHandshakeInterceptor组件用于WebSocket连接的JWT认证
- 使DomSyncWebSocketConfig支持通过依赖注入使用JwtHandshakeInterceptor
- 移除旧版内联JWT拦截器实现,改用单独组件类
- 优化DomSyncHandler,针对iframe模式简化DOM同步逻辑
- 移除DomSyncData类,合并相关字段简化数据传输结构
- 实现HTML内容转义以避免JSON序列化错误
- 取消传输样式和脚本,仅同步完整DOM和增量DOM变化
- 增加获取完整DOM时的重试机制以提升稳定性
- 修改大消息广播方案,添加消息大小检查与分片发送逻辑
- 删除不必要的压缩功能,改用纯文本消息分片传输
- 删除部分WebSocket、SSE及控制台消息监听,减少复杂度
- 改进Playwright初始化和页面导航监听逻辑,避免阻塞
- 精简WebSocket命令处理,去除对容器元素的特定限制
- 增加详细日志输出,方便定位和跟踪用户操作及错误
- 优化异常处理,确保错误信息及时反馈给客户端
- 调整客户端连接管理,实现异常时安全移除无效会话
parent b2158e16
......@@ -4,6 +4,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
......@@ -11,6 +13,7 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UriComponentsBuilder;
import pangea.hiagent.utils.JwtUtil;
import pangea.hiagent.websocket.DomSyncHandler;
import java.util.Map;
......@@ -22,6 +25,12 @@ import java.util.Map;
@EnableWebSocket
public class DomSyncWebSocketConfig implements WebSocketConfigurer {
private final JwtHandshakeInterceptor jwtHandshakeInterceptor;
public DomSyncWebSocketConfig(JwtHandshakeInterceptor jwtHandshakeInterceptor) {
this.jwtHandshakeInterceptor = jwtHandshakeInterceptor;
}
// 注入DomSyncHandler,交由Spring管理生命周期
@Bean
public DomSyncHandler domSyncHandler() {
......@@ -32,62 +41,86 @@ public class DomSyncWebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(domSyncHandler(), "/ws/dom-sync")
// 添加握手拦截器用于JWT验证
.addInterceptors(new JwtHandshakeInterceptor())
.addInterceptors(jwtHandshakeInterceptor)
// 生产环境:替换为具体域名,禁止使用*
.setAllowedOrigins("*");
}
}
/**
* JWT握手拦截器,用于WebSocket连接时的认证
*/
@Component
class JwtHandshakeInterceptor implements HandshakeInterceptor {
private final JwtUtil jwtUtil;
/**
* JWT握手拦截器,用于WebSocket连接时的认证
*/
public static class JwtHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 首先尝试从请求头中获取JWT Token
String token = request.getHeaders().getFirst("Authorization");
// 如果请求头中没有,则尝试从查询参数中获取
if (token == null) {
String query = request.getURI().getQuery();
if (query != null) {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().query(query);
token = builder.build().getQueryParams().getFirst("token");
}
}
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // 移除"Bearer "前缀
}
if (token != null && !token.isEmpty()) {
try {
// 简单检查token是否包含典型的JWT部分
String[] parts = token.split("\\.");
if (parts.length == 3) {
// 基本格式正确,接受连接
public JwtHandshakeInterceptor(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
String token = extractTokenFromRequest(request);
if (StringUtils.hasText(token)) {
try {
// 验证token是否有效
boolean isValid = jwtUtil.validateToken(token);
if (isValid) {
// 获取真实的用户ID
String userId = jwtUtil.getUserIdFromToken(token);
if (userId != null) {
attributes.put("token", token);
// 使用token的一部分作为用户标识(实际应用中应该解析JWT获取用户ID)
attributes.put("userId", "user_" + token.substring(0, Math.min(8, token.length())));
System.out.println("WebSocket连接认证成功,Token: " + token);
attributes.put("userId", userId);
System.out.println("WebSocket连接认证成功,用户ID: " + userId);
return true;
} else {
System.err.println("无法从token中提取用户ID");
}
} catch (Exception e) {
System.err.println("JWT验证过程中发生错误: " + e.getMessage());
e.printStackTrace();
} else {
System.err.println("JWT验证失败,token可能已过期或无效");
}
} catch (Exception e) {
System.err.println("JWT验证过程中发生错误: " + e.getMessage());
e.printStackTrace();
}
// 如果没有有效的token,拒绝连接
System.err.println("WebSocket连接缺少有效的认证token");
response.setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// 握手后处理,这里不需要特殊处理
// 如果没有有效的token,拒绝连接
System.err.println("WebSocket连接缺少有效的认证token");
response.setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// 握手后处理,这里不需要特殊处理
}
/**
* 从请求头或参数中提取Token
* 复用JwtAuthenticationFilter中的逻辑
*/
private String extractTokenFromRequest(ServerHttpRequest request) {
// 首先尝试从请求头中提取Token
String authHeader = request.getHeaders().getFirst("Authorization");
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
return token;
}
// 如果请求头中没有Token,则尝试从URL参数中提取
String query = request.getURI().getQuery();
if (query != null) {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().query(query);
String token = builder.build().getQueryParams().getFirst("token");
if (StringUtils.hasText(token)) {
return token;
}
}
return null;
}
}
\ No newline at end of file
package pangea.hiagent.dto;
package pangea.hiagent.websocket;
import com.alibaba.fastjson2.annotation.JSONField;
......@@ -6,11 +6,11 @@ import com.alibaba.fastjson2.annotation.JSONField;
* DOM同步的数据传输对象
*/
public class DomSyncData {
// 消息类型:init(初始化完整DOM)、update(增量DOM更新)、style(样式)、script(脚本)
// 消息类型:init(初始化完整DOM)、update(增量DOM更新)、style(样式)、script(脚本)、fragment(分片消息)
@JSONField(name = "type")
private String type;
// DOM内容(完整/增量)
// DOM内容(完整/增量/分片
@JSONField(name = "dom")
private String dom;
......
This diff is collapsed.
......@@ -6,7 +6,7 @@
<div class="page-content">
<div class="left-panel">
<DomSyncViewer />
<DomSyncViewer ref="domSyncViewerRef" />
</div>
<div class="right-panel">
......@@ -56,11 +56,20 @@
import { ref, onMounted } from 'vue'
import DomSyncViewer from '@/components/DomSyncViewer.vue'
// 定义DomSyncViewer组件实例的类型
interface DomSyncViewerInstance {
urlInput: Ref<string>
navigateToUrl: () => void
}
// 响应式数据
const targetUrl = ref('https://www.baidu.com')
const connectionStatus = ref('disconnected')
const currentPage = ref('未连接')
// DomSyncViewer组件引用
const domSyncViewerRef = ref<DomSyncViewerInstance | null>(null)
// 导航到指定URL
const navigateToUrl = () => {
// 验证URL格式
......@@ -74,8 +83,13 @@ const navigateToUrl = () => {
return;
}
// 这里可以通过某种方式通知DomSyncViewer组件导航到指定URL
console.log('导航到:', targetUrl.value)
// 通过ref调用DomSyncViewer组件的navigateToUrl方法来导航到指定URL
if (domSyncViewerRef.value) {
// 直接传递URL给DomSyncViewer组件的navigateToUrl方法
domSyncViewerRef.value.navigateToUrl(targetUrl.value)
} else {
console.error('DomSyncViewer组件引用未找到')
}
}
// 组件挂载时的处理
......
......@@ -21,6 +21,11 @@ export default defineConfig({
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api')
},
'/ws': {
target: 'http://localhost:8080',
ws: true, // 启用WebSocket代理
changeOrigin: true
}
}
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment