新闻详情

新闻详情

首页 / 资讯中心 / 详情

CEF拦截WSS连接实战:握手控制与WebSocket帧注入方案

发布时间:2026/9/8 1:25:18来源:尧图网络
CEF拦截WSS连接实战:握手控制与WebSocket帧注入方案
简介面向.NET开发者的CEFSharp网络调试方案基于WinForms工程实现拦截网站所有WSS/WebSocket Secure通信适用于需要深度分析实时接口、安全审计或自定义数据过滤的桌面应用场景适合有一定CEF/Chromium基础的中高级开发者。zip压缩包共21个文件、约76KB主体为13个C#源码文件包含浏览器封装、请求/响应拦截、协议处理等核心逻辑另有解决方案文件、项目配置、界面资源及设置项结构清晰便于直接编译和二次开发。已有200人学习下载。项目演示了通过Cef.RegisterSchemeHandlerFactory注册ws与wss协议处理器继承ISchemeHandler完成握手代理、数据帧截取、日志记录与转发控制的全过程同时覆盖请求处理器、响应过滤器等配套组件。阅读代码可掌握解析URL、建立TCP连接、处理WebSocket握手以及收发帧的自定义逻辑并了解错误处理与连接断开机制对需要深层控制网络通信的开发者具有直接参考价值。 调试内嵌浏览器的时候最头疼的不是页面渲染而是那些悄悄跑起来的WebSocket连接。尤其是现在的Web应用消息推送、实时聊天、行情数据几乎全走WSSWebSocket Secure它和HTTPS一样加密传输抓包工具能看到握手但很难看到帧内容更别提从业务层去修改数据了。CEFChromium Embedded Framework作为桌面端最主流的嵌入式浏览器方案想要“拦截一切网站的WSS”光靠常规的网络代理思路根本行不通。这篇文章就记录我实际做过的方案和完整代码适合桌面客户端开发、自动化测试工具作者、爬虫工程师以及所有需要在CEF里掌控WebSocket流量的朋友参考。我先把结论放前面CEF里拦截WSS一定要拆成两层来做。第一层在Browser进程的请求回调里拦“握手”对连接本身做控制比如改头、阻断、加鉴权参数第二层在Renderer进程注入JavaScriptHook掉页面里的WebSocket对象才能拿到真正的帧数据。这两层缺一不可只做任何一层都只能算“半个拦截”。1. 为什么非要拆两层先搞懂CEF网络栈的脾气1.1 WSS不是普通HTTP请求资源层看不到数据帧很多人第一次上手会直接去CefRequestHandler的OnBeforeResourceLoad里拦URL觉得只要判断scheme是ws还是wss就能像拦HTTP一样处理。结果发现握手请求确实能拦到但后续发出去的业务数据在这个回调里完全看不到。原因在于WebSocket协议的特殊性。它借用了HTTP的握手阶段发起一个带Upgrade: websocket头的请求然后连接就会被“升级”成独立的双向通道这之后再走的帧数据就不再是普通HTTP语义了。CEF的资源请求回调本质上还是HTTP生命周期里的钩子负责处理请求、响应、重定向这些阶段。一旦连接升级完成帧数据就在另一个通道里流动资源回调自然就抓不到。我打个比方握手就像去银行办业务你在柜台填单子HTTP请求、柜员给你办完101响应之后你直接走VIP通道跟行长聊天了柜台的监控系统资源回调就拍不到后续内容了。想把这部分内容也记录下来就得在VIP通道里再装一个摄像头也就是Renderer进程里的JS注入。1.2 四个候选方案我为什么最终选了组合拳我在动手之前把社区里常见的做法都过了一遍各有各的坑方案能干的事痛点只拦CefRequestHandler能拦握手能改请求头能Cancel连接拿不到帧数据业务层完全失控注册自定义SchemeHandler处理ws/wss理论可行实测CEF对标准scheme不生效自定义handler主要服务于自定义协议开DevTools远程调试端口再抓WebSocket帧帧数据能拿到要额外起一个调试端口自己抓自己很别扭而且部署到客户机器上基本不现实请求层拦握手 Renderer层JS注入握手可控、帧数据可控、可修改实现成本略高但效果最完整前三个方案都只覆盖了连接的一个侧面。我要的是“拦截一切网站的WSS”也就是无论页面里的WebSocket是谁创建的、创建在哪个Frame里我都要能控制连接、能看到内容、能动态修改。满足这个条件的只有第四种组合路线。1.3 一个容易忽略的前提多进程模型CEF本身是Chromium的多进程架构Browser进程负责窗口管理和网络请求等主流程Renderer进程负责页面JS和DOM渲染。这意味着你在CefRequestHandler里写的代码跑在Browser进程你在页面里注入的JS跑在Renderer进程两边的内存不相通。想互通得靠CEF提供的进程间消息机制比如CefProcessMessage。这一点直接决定了架构设计。很多人写一半发现我在渲染进程的JS里把数据抓到了怎么传给Browser进程传不过去就卡住了。所以完整方案里必须把“哪里抓”和“往哪送”想清楚再动笔写代码。2. 落地架构Browser进程管连接Renderer进程管内容2.1 Browser进程在握手阶段插一脚整体思路是自定义一个CefRequestHandler的子类重写GetResourceRequestHandler让它对每个子请求都返回一个自定义的CefResourceRequestHandler。然后在OnBeforeResourceLoad里判断如果URL的scheme是ws或wss并且请求头里有Upgrade: websocket就说明这是一个WebSocket握手请求。在这个阶段你可以做很多事情修改请求头比如替换Origin、改写Cookie、追加自定义鉴权字段也可以直接返回false之后调用callback-Cancel()彻底阻断这个连接甚至可以把请求截下来自己发一个HTTP请求去验证一下Token再决定放行还是拦截。这种“先校验再放行”的玩法在做企业级客户端时特别实用。需要注意的是WebSocket握手请求的Method是GET但它和普通GET最大的区别就是那个Upgrade头。判断的时候一定要两个条件一起看不能只凭scheme。我的开发环境里遇到过同事只判断scheme结果把正常的HTTPS请求也误伤的情况排查了很久。2.2 Renderer进程给WebSocket上“监控”连接可以拦了帧数据还没着落。数据层的拦截核心思路是在CefRenderProcessHandler的OnContextCreated回调里向每个页面Frame的JS上下文注入Hook脚本把页面里的window.WebSocket“换”成一个增强版。增强版要做四件事第一保留原始WebSocket构造函数第二在新构造函数里记录每个实例的URL、协议、状态第三重写send方法把调用方传出去的数据先上报再转发给原生WebSocket实例第四包裹onmessage回调把服务端推回来的数据也先上报再传给业务逻辑。这套Hook思路古已有之很多Web调试工具就是这么干的。把它塞进CEF的Renderer进程等于给每一个页面的WebSocket都装上了双向探针。更妙的是在这个层面你不仅能看数据还能改数据比如把消息体里的敏感字段脱敏之后再交给上层业务这在数据合规的场景下很有价值。2.3 为什么不用中间人代理方案可能有人会问既然要抓WSS内容为什么不直接在本地起一个代理服务器做标准的中人代理这个方案当然成熟Charles、Fiddler就是这么干的但它有几个痛点在CEF场景下特别明显。一是代理模式下客户端要信任代理的根证书CEF内嵌浏览器虽然可以配置忽略证书错误但强行改掉证书校验逻辑在某些站点上会触发TLS指纹检测反而更容易暴露。二是代理服务器是独立进程跟CEF的Browser进程、Renderer进程之间的通信链路全部要自己搭复杂度一点不比JS注入低。三是遇到用客户端证书或者自定义协议栈的站点代理模式基本歇菜。相比之下JS注入是直接“长”在页面环境里的不存在SSL剥离和指纹问题只要页面能正常走WSS我们就能跟着正常走。3. 完整代码与关键步骤3.1 初始化CEF时的准备工作代码之前先把环境准备好。我用的CEF版本是较新的Chromium内核分支分发包里通常包含libcef_dll_wrapper的工程文件这里不赘述环境搭建只说几个和拦截相关的细节。第一个是初始化设置里不要开--disable-web-security因为有的教程会让你关掉同源策略来方便调试但关闭之后站点做跨域WSS校验时行为会跟真实环境不一致反而让你排查问题时误判。第二个建议是如果有条件在非发布版本里开一下远程调试端口方便用Chrome DevTools直接观察页面里的WebSocket详情定位Hook脚本到底有没有生效CefSettings settings; settings.remote_debugging_port 9333; // 仅本地调试用发布版记得关掉 CefInitialize(settings, app, nullptr);开这个端口只是为了开发期调试最终交付时一定要去掉否则用户机器上等于留了个后门安全性没法交代。3.2 Browser进程核心代码拦截握手的ResourceRequestHandler下面这段是最终实际跑通的代码骨架我做了精简。重点是OnBeforeResourceLoad里的判断逻辑以及在必要的时候修改请求头或取消请求#include include/cef_request_handler.h #include include/cef_resource_request_handler.h namespace { bool IsWebSocketRequest(CefRefPtrCefRequest request) { const std::string url request-GetURL(); if (url.rfind(ws://, 0) ! 0 url.rfind(wss://, 0) ! 0) { return false; } CefRequest::HeaderMap headers; request-GetHeaderMap(headers); for (const auto pair : headers) { std::string name pair.first; std::transform(name.begin(), name.end(), name.begin(), ::tolower); if (name upgrade pair.second.find(websocket) ! std::string::npos) { return true; } } return false; } class DemoResourceHandler : public CefResourceRequestHandler { public: bool OnBeforeResourceLoad( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, CefRefPtrCefCallback callback) override { if (IsWebSocketRequest(request)) { // 场景1直接放行但记录日志 LOG(INFO) [WSS] URL request-GetURL(); // 场景2修改请求头比如追加自定义认证信息 CefRequest::HeaderMap headers; request-GetHeaderMap(headers); headers.insert(std::make_pair(X-Cef-Wss-Debug, 1)); request-SetHeaderMap(headers); // 场景3需要断掉连接时直接cancel // callback-Cancel(); // return true; } return false; // 返回false表示继续处理请求 } private: IMPLEMENT_REFCOUNTING(DemoResourceHandler); }; class DemoRequestHandler : public CefRequestHandler { public: CefRefPtrCefResourceRequestHandler GetResourceRequestHandler( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, bool is_navigation, bool is_download, const CefString request_initiator, bool disable_default_handling) override { return new DemoResourceHandler(); } private: IMPLEMENT_REFCOUNTING(DemoRequestHandler); }; } // namespace写完这个类之后在主应用的CefApp子类里重写OnRegisterCustomSchemes或者直接在创建CefSettings之前把DemoRequestHandler挂到CefBrowserSettings或者CefRequestContext上。实际工程里我一般通过CefRequestContext的SetHandler方式挂载这样能针对不同会话做不同策略CefRefPtrCefRequestContext ctx CefRequestContext::GetGlobalContext(); ctx-SetHandler(new DemoRequestHandler());一个容易踩的坑是GetResourceRequestHandler会非常频繁地被调用每一个子资源请求都会走这里一次所以里面千万别做耗时操作。我之前在拦截WSS握手时顺手做了个DNS查询结果整个页面的资源加载全变慢了。这种高频回调里只做内存判断和请求头读写其他事情全部丢到工作线程。3.3 Renderer进程核心代码Hook WebSocket的注入脚本Browser进程写完下一步就是渲染进程注入。这里需要在CefApp里同时实现CefRenderProcessHandler然后在OnContextCreated里执行一段精心准备的JavaScript。先看C侧的注册下面这是最简版本#include include/cef_app.h #include include/cef_v8.h class DemoApp : public CefApp, public CefRenderProcessHandler { public: // 在浏览器进程里不需要走RenderProcessHandler这里用ProcessType区分 CefRefPtrCefRenderProcessHandler GetRenderProcessHandler() override { return this; } void OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) override { // 在每个Frame的JS上下文创建完成后注入Hook脚本 std::string hookScript BuildHookScript(); CefRefPtrCefV8Value retVal; CefRefPtrCefV8Exception exception; if (!context-Eval(hookScript, CefString(), 0, retVal, exception)) { LOG(ERROR) Inject WSS hook failed: (exception ? exception-GetMessage().ToString() : unknown); } } private: std::string BuildHookScript() { // 核心JS放在单独的脚本文件里这里简化为字符串 return ...; // 见下文JS代码 } IMPLEMENT_REFCOUNTING(DemoApp); };真正的Hook脚本才是重头戏。脚本既要捕获新建的WebSocket连接又要能跨过同源Frame的限制还要把事件上报给Native侧。我用的是CefV8Handler注册一个全局函数让JS能把消息send给C侧C再通过CefProcessMessage把帧数据转发给你的业务层(function() { if (window.__wssHookInstalled__) return; window.__wssHookInstalled__ true; var Native function(cmd, data) { // 这个函数由CefV8Handler在C里实现 window.__cefWssBridge(cmd, JSON.stringify(data)); }; var OriginalWS window.WebSocket; if (!OriginalWS) return; window.WebSocket function(url, protocols) { var self this; var ws (typeof protocols string) ? new OriginalWS(url, protocols) : new OriginalWS(url); var reported false; function reportOpen() { if (reported) return; reported true; Native(open, { url: url, readyState: ws.readyState }); } ws.addEventListener(open, reportOpen, false); var nativeSend ws.send.bind(ws); ws.send function(data) { var payload; if (data instanceof Blob) { // Blob没法直接看内容标记一下类型 payload { type: blob, size: data.size }; } else if (data instanceof ArrayBuffer) { payload { type: arraybuffer, byteLength: data.byteLength }; } else { payload { type: string, data: String(data) }; } Native(send, { url: url, payload: payload }); return nativeSend(data); }; var onMessage ws.addEventListener; ws.addEventListener function(type, listener, useCapture) { if (type message) { onMessage.call(ws, type, function(event) { var data event.data; var desc; if (typeof data string) { desc { type: string, data: data }; } else if (data instanceof Blob) { desc { type: blob, size: data.size }; } else if (data instanceof ArrayBuffer) { desc { type: arraybuffer, byteLength: data.byteLength }; } else { desc { type: unknown }; } Native(message, { url: url, payload: desc }); listener.call(this, event); }, useCapture); } else { onMessage.call(ws, type, listener, useCapture); } }; return ws; }; window.WebSocket.prototype OriginalWS.prototype; Object.defineProperty(window.WebSocket, CONNECTING, { value: 0 }); Object.defineProperty(window.WebSocket, OPEN, { value: 1 }); Object.defineProperty(window.WebSocket, CLOSING, { value: 2 }); Object.defineProperty(window.WebSocket, CLOSED, { value: 3 }); // 上报所有新连接创建事件 var nativeDescribe function(url) { Native(create, { url: url }); }; var origOpen window.WebSocket.prototype; // 如果需要拿到connection实例可以在open之后通过get准备好映射表 })();这段脚本有个细节要说明我把send和message里的数据都做了类型归一化。因为WebSocket可以发字符串、Blob、ArrayBuffer如果不区分类型业务层拿到乱七八糟的对象会非常难处理。字符串类数据直接给原文二进制数据只暴露元信息类型和大小有需要再通过C侧另外走文件通道去读完整内容避免大对象在进程间通信时撑爆消息队列。3.4 数据从Renderer进程传回Browser进程进程间通信JS把数据传给Native侧之后还要做一次进程间搬运。我通过CefV8Handler在C里实现了__cefWssBridge收到JS消息后立刻组装成一个CefProcessMessagePostMessage到Browser进程的某个CefBrowser实例上。直接在浏览器进程里接收并打印其实已经很直观了我把代码简化如下class DemoMessageHandler { public: static void Install(CefRefPtrCefBrowser browser) { browser-GetMainFrame()-AddMessageListener(wss_debug, [](CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefProcessId source_process, CefRefPtrCefProcessMessage message) { CefRefPtrCefListValue args message-GetArgumentList(); std::string event args-GetString(0); std::string data args-GetString(1); LOG(INFO) [WSS Event] event data; // 这里你可以做实时入库、改动数据策略等 }); } };这里有个经验之谈WebSocket帧非常密集尤其是行情类、聊天类应用每秒几十条消息很正常。如果每条都立刻PostMessage到Browser进程可能在主线程形成阻塞。我在实际工程里是先放一个无锁环形缓冲后台线程批量刷日志或者按会话维度聚合之后再输出效果会好很多。4. 常见问题与排查技巧实录4.1 OnBeforeResourceLoad里看不到WSS连接排查步骤无非三板斧第一步确认你的CefRequestHandler确实挂到了RequestContext上别只挂在某个Browser上第二步确认IsWebSocketRequest函数判断条件完整命中的是wss://而不是https://和一些二进制的WebSocket子协议第三步加日志打印所有请求的URL和Method看看WebSocket握手请求到底走没走这个回调。我遇到过有些CEF版本会把WebSocket请求直接交给网络服务资源回调压根不触发这时候需要检查你初始化CEF时是否设置过自定义网络代理如果有代理层会先把它劫走。4.2 修改握手头之后连接直接失败最常见的原因是修改了Host头或者Origin头。浏览器对Origin的校验很严格任意改动都可能被服务端拒绝。我的建议是握手阶段尽量做“追加型修改”比如加自定义头、追加Cookie项不要动已经被浏览器计算好的Host、Origin、Sec-WebSocket-Key这些标准头除非你确切知道服务端的校验逻辑。4.3 帧数据收不到只能抓到握手日志这种情况基本是JS Hook脚本没生效或者生效了但数据没送出来。先在OnContextCreated里打日志确认每个Frame都执行了注入脚本然后打开DevTools远程调试端口在Console里手动执行window.WebSocket undefined看是不是被覆盖了最后确认CefV8Handler的绑定是否工作JS调用Native函数有没有抛异常。4.4 Worker和Service Worker里的WebSocket抓不到这是Hook方案一个比较明显的边界Worker线程有自己的全局作用域不走主线程的OnContextCreated。我之前在CEF社区也问过这个问题最终的折中方案是改绑CefV8Context的scope在CefRenderProcessHandler里用OnWorkerContextCreated接口对Worker上下文也注入同样的Hook脚本。新版本的CEF已经提供了相关回调尽量用官方API而不是自己在web worker外层包一层JSProxy。4.5 数据量大时主线程卡顿性能问题往往是最后爆发的。Stream数据每秒几百帧时JS侧Native调用不能做得太重。我后来做了四个优化第一JS侧把数据拼接成JSON数组攒够几十条再统一send一次第二C侧负责收数据的函数只做投递不做任何磁盘或网络IO第三大数据负载的二进制内容不直接送进程间通信而是写到一个临时映射文件里只传文件路径和偏移第四在Browser进程侧开独立线程消费队列不阻塞UI。现象可能原因排查手段回调里搜不到wss请求handler未挂载到RequestContext打印所有请求走一遍上下文链改了请求头后握手失败标准头被非法修改只追加自定义头不动标准头只有握手日志没有帧数据JS注入失败或V8Handler异常DevTools看页面WebSocket是否被覆盖Worker里抓不到Worker作用域未注入使用OnWorkerContextCreated做同样注入高频率下页面掉帧线程阻塞批量上报 异步IO 队列消费最后的落地建议整个方案我前后改了三个版本最初只在Browser进程拦握手交付后被业务方吐槽“光拦握手有什么用数据还是看不见”后来加了JS注入但第一次跑线上就被高并发WebSocket流量打挂了才意识到性能问题要前置。我的体会是CEF的这套进程机制虽然麻烦但它把“网络控制权”和“页面执行权”清楚分开了搞清楚这两条线各自的边界再动手写代码比任何现成代码都重要。如果你也想在自己项目里跑通这套逻辑我建议先不要一步到位按这个顺序来第一步只打印所有WSS握手的URL和请求头确认走通回调第二步用最简单的注入脚本覆盖window.WebSocket在Console里打印所有send消息第三步再把两套逻辑串起来按我上面的方案做进程间通信。每一步都能看到明确输出排查起来不难。另外这套方案同时也具备广告拦截的能力通过在握手阶段拦截特定的广告域名的WSS连接配合资源层的HTTP请求过滤能实现比普通规则更彻底的广告屏蔽效果。至于拦截后的数据如何加工、如何利用那就是下一个项目的事了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

全志平台GT9xx触摸屏驱动适配与调试全攻略 2026/9/8 3:01:32

全志平台GT9xx触摸屏驱动适配与调试全攻略

简介:面向嵌入式Linux开发者及触控驱动维护者,全志平台GT9XX触摸屏驱动程序资源包聚焦全志R16平台与input子系统,覆盖驱动加载、设备树匹配、触摸数据上报、电源管理等多个开发环节,重点解决触摸芯片驱动移植与调试难题。资源共25…

阅读更多 →
《创新者的窘境》核心拆解:为什么大公司会被颠覆? 2026/9/8 3:01:32

《创新者的窘境》核心拆解:为什么大公司会被颠覆?

先把丑话说在前面:这本书我翻过不下五遍,每次以为自己读懂了,过一阵子再翻一页,还是会冒出冷汗。1997年出版,二十多年过去,书里的案例从硬盘换成了手机、汽车、芯片,但剧本几乎没变过——大公司…

阅读更多 →
Android属性服务PropertyService源码解析:从setprop到Binder全链路 2026/9/8 3:01:32

Android属性服务PropertyService源码解析:从setprop到Binder全链路

1. PropertyService 是什么,为什么值得读源码先交代一个背景:PropertyService(属性服务)是 Android 系统里最“不起眼”却最核心的系统服务之一,运行在 system_server 进程中,通过 Binder 对外提供系统属性…

阅读更多 →
SD卡选购与验证:UHS-I、U3、V30与4K视频写入需求全解析 2026/9/8 3:01:32

SD卡选购与验证:UHS-I、U3、V30与4K视频写入需求全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
HackBGRT 1.5.1完全指南:用UEFI方式替换Windows开机Logo 2026/9/8 3:01:32

HackBGRT 1.5.1完全指南:用UEFI方式替换Windows开机Logo

简介:这是一份用于个性化修改Windows 10开机LOGO的工具包,面向希望自定义启动画面的系统爱好者、开发者以及日常用户。HackBGRT 1.5.1可替换默认的BGRT启动标志,让开机过程呈现个人风格。资源共20个文件,结构清晰,包含…

阅读更多 →
嵌入式驱动岗秋招面试十连问:字符设备、总线、中断与并发全覆盖 2026/9/8 2:58:32

嵌入式驱动岗秋招面试十连问:字符设备、总线、中断与并发全覆盖

秋招进入密集面试期,嵌入式软件岗里“驱动开发”方向因为门槛高、坑位稳定,一直是不少人的主战场。面试官问的问题往往不追求背概念,而是看你能不能把内核机制和真实硬件关联起来。这篇文章整理了近期嵌入式驱动岗面试中最常出现的十连问&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞