拦截 WebSocket 流量
本教程演示如何在浏览器中拦截 WebSocket 流量并将其转发到 Java 代码进行日志记录或处理。
前提条件
要完成本教程,您需要:
- Git。
- Java 17 或更高版本。
- 有效的 JxBrowser 许可证,评估版或商业版均可。有关许可证的更多信息,请参阅许可证指南。
获取代码
请执行以下命令获取代码:
git clone https://github.com/TeamDev-IP/JxBrowser-Examples
cd JxBrowser-Examples/tutorials/intercepting-web-sockets
实现方案
当 Web 应用程序使用 WebSocket 进行实时通信时,您可能需要从 Java 端监控或记录此流量。由于 WebSocket 连接完全由浏览器中的 JavaScript 管理,因此您需要一种方式来拦截此流量并将其桥接到 Java。
实现方案很简单:
- 在 JavaScript 中覆盖原生
WebSocket构造函数,以包装所有 WebSocket 连接。 - 钩入
send方法和message事件,以捕获发出和接收的数据。 - 从 JavaScript 调用 Java 函数,将拦截到的数据转发过去。
此技术透明地工作——任何创建 WebSocket 连接的代码都将被自动拦截,无需修改。
JavaScript 拦截器
JavaScript 代码通过将原生 WebSocket 构造函数替换为一个钩入连接生命周期的包装器来拦截 WebSocket 流量。
创建名为 websocket-interceptor.js 的文件:
// 保存对原生 WebSocket 的引用。
const NativeWebSocket = window.WebSocket;
// 覆盖全局 WebSocket 构造函数。
window.WebSocket = function(url, protocols) {
const socket = new NativeWebSocket(url, protocols);
// 拦截接收到的消息。
socket.addEventListener('message', (event) => {
if (window.onWebSocketReceived) {
window.onWebSocketReceived(event.data);
}
});
// 拦截发出的消息。
const originalSend = socket.send;
socket.send = function(data) {
if (window.onWebSocketSent) {
window.onWebSocketSent(data);
}
originalSend.call(socket, data);
};
return socket;
};
// 保留原型链。
window.WebSocket.prototype = NativeWebSocket.prototype;
拦截器代码期望在 window 对象中提供两个函数:onWebSocketReceived 和 onWebSocketSent。这些将由 Java 端提供。
演示页面
为了测试拦截器,创建一个使用 WebSocket 的简单 HTML 页面。
创建 websocket-demo.html:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Interception Demo</title>
</head>
<body>
<h1>WebSocket Interception Demo</h1>
<button id="connect">Connect</button>
<button id="send" disabled>Send Message</button>
<button id="disconnect" disabled>Disconnect</button>
<div id="status">Not connected</div>
<script>
let socket = null;
document.getElementById('connect').addEventListener('click', () => {
socket = new WebSocket('wss://echo.websocket.org/');
socket.addEventListener('open', () => {
document.getElementById('status').textContent = 'Connected';
document.getElementById('connect').disabled = true;
document.getElementById('send').disabled = false;
document.getElementById('disconnect').disabled = false;
});
socket.addEventListener('close', () => {
document.getElementById('status').textContent = 'Disconnected';
document.getElementById('connect').disabled = false;
document.getElementById('send').disabled = true;
document.getElementById('disconnect').disabled = true;
});
});
document.getElementById('send').addEventListener('click', () => {
socket.send('Hello from browser at ' + new Date());
});
document.getElementById('disconnect').addEventListener('click', () => {
socket.close();
});
</script>
</body>
</html>
此页面连接到一个公共 WebSocket 回显服务器。当您点击"Send Message"时,它会发送一条带有时间戳的消息,服务器将其原样返回,从而触发传入消息处理程序。
请注意,此 HTML 中不包含拦截器脚本。Java 应用程序将在页面加载前注入它。
Java 应用程序
Java 应用程序加载 HTML 页面,注入 JavaScript 拦截器,并注册接收拦截到的 WebSocket 数据的回调函数。
以下是完整的应用程序:
import com.teamdev.jxbrowser.browser.Browser;
import com.teamdev.jxbrowser.browser.callback.InjectJsCallback;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.engine.RenderingMode;
import com.teamdev.jxbrowser.frame.Frame;
import com.teamdev.jxbrowser.js.JsFunctionCallback;
import com.teamdev.jxbrowser.js.JsObject;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class WebSocketInterceptorApp {
public static void main(String[] args) {
var engine = Engine.newInstance(RenderingMode.HARDWARE_ACCELERATED);
var browser = engine.newBrowser();
// 注入 JavaScript 并注册桥接函数。
browser.set(InjectJsCallback.class, params -> {
var frame = params.frame();
injectInterceptor(frame);
registerBridgeFunctions(frame);
return InjectJsCallback.Response.proceed();
});
SwingUtilities.invokeLater(() -> {
var view = BrowserView.newInstance(browser);
var frame = new JFrame("WebSocket Interceptor");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.add(view, BorderLayout.CENTER);
frame.setSize(800, 600);
frame.setVisible(true);
});
// 加载演示 HTML 页面。
var html = loadResource("websocket-demo.html");
browser.navigation().loadHtml(html);
}
private static void injectInterceptor(Frame frame) {
var script = loadResource("websocket-interceptor.js");
frame.executeJavaScript(script);
}
private static void registerBridgeFunctions(Frame frame) {
JsObject window = frame.executeJavaScript("window");
window.putProperty("onWebSocketReceived", (JsFunctionCallback) args -> {
var data = args.get(0).toString();
System.out.println("[RECEIVED] " + data);
return null;
});
window.putProperty("onWebSocketSent", (JsFunctionCallback) args -> {
var data = args.get(0).toString();
System.out.println("[SENT] " + data);
return null;
});
}
private static String loadResource(String resourceName) {
try (var stream = WebSocketInterceptorApp.class.getResourceAsStream("/" + resourceName)) {
if (stream == null) {
throw new RuntimeException("Resource not found: " + resourceName);
}
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Failed to load resource: " + resourceName, e);
}
}
}
让我们逐步了解关键部分:
注入拦截器
InjectJsCallback 在每次创建新帧时运行:
browser.set(InjectJsCallback.class, params -> {
var frame = params.frame();
injectInterceptor(frame);
registerBridgeFunctions(frame);
return InjectJsCallback.Response.proceed();
});
此回调使您在页面自身的 JavaScript 运行之前就能访问帧,这是注入拦截器的最佳时机。
加载并执行拦截器脚本
injectInterceptor 方法从资源中加载 JavaScript 文件并执行它:
private static void injectInterceptor(Frame frame) {
var script = loadResource("websocket-interceptor.js");
frame.executeJavaScript(script);
}
运行此方法后,原生 WebSocket 构造函数将被替换,所有 WebSocket 连接都将被拦截。
注册桥接函数
registerBridgeFunctions 方法创建两个 Java 回调并将其暴露给 JavaScript:
private static void registerBridgeFunctions(Frame frame) {
JsObject window = frame.executeJavaScript("window");
window.putProperty("onWebSocketReceived", (JsFunctionCallback) args -> {
var data = args[0].toString();
System.out.println("[RECEIVED] " + data);
return null;
});
window.putProperty("onWebSocketSent", (JsFunctionCallback) args -> {
var data = args[0].toString();
System.out.println("[SENT] " + data);
return null;
});
}
每当检测到 WebSocket 流量时,JavaScript 拦截器就会调用这些函数。在本示例中,它们只是将数据打印到控制台,但您也可以将其记录到文件、存储到数据库,或以任何其他方式进行处理。
运行应用程序
运行应用程序后,您将看到一个带有三个按钮的浏览器窗口。
点击"Connect"建立 WebSocket 连接,然后点击"Send Message"发送数据。在 Java 控制台中,您应该会看到如下输出:
[SENT] Hello from browser at 2026-01-20T13:45:23.123Z
[RECEIVED] Hello from browser at 2026-01-20T13:45:23.123Z
回显服务器会返回您发送的相同消息,因此您将看到发出和接收的流量。
总结
在本教程中,您学习了如何:
- 覆盖原生
WebSocket构造函数以拦截所有 WebSocket 连接。 - 钩入
send方法和message事件以捕获流量。 - 在 JavaScript 世界中注册 Java 函数,以便在 Java 端接收 WebSocket 数据。
- 将 WebSocket 数据从浏览器桥接到 Java 进行日志记录或处理。
此技术可以扩展为修改 WebSocket 消息、基于 URL 模式阻止连接,或为任何使用 WebSocket 的 Web 应用程序添加自定义日志记录和分析功能。