目录

拦截 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。

实现方案很简单:

  1. 在 JavaScript 中覆盖原生 WebSocket 构造函数,以包装所有 WebSocket 连接。
  2. 钩入 send 方法和 message 事件,以捕获发出和接收的数据。
  3. 从 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 对象中提供两个函数:onWebSocketReceivedonWebSocketSent。这些将由 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 应用程序添加自定义日志记录和分析功能。