首页 / 资讯中心 / 文章详情

WebSocket数据采集实战:JS Hook与长连接监听技术解析

WebSocket数据采集实战:JS Hook与长连接监听技术解析 ★ FEATURED ARTICLE
1. 项目缘起与整体思路拆解1.1 这个案例到底在解决什么问题先把场景说清楚。有一类站点页面上的数据不是靠传统的一次性HTTP请求拉回来的而是页面加载后由前端脚本主动建立一条长连接服务端把数据一段一段推过来前端再渲染到界面上。你打开开发者工具看Network面板会发现常规的XHR列表里干干净净数据却实实在在地显示在页面上——这就是典型的WebSocket推送型数据源。这个案例要解决的核心问题就一句话如何稳定、可维护地把WebSocket通道里的数据取出来并且在关键参数被前端脚本动态生成、无法直接复现的情况下用JS hook的手段把生成逻辑“钓”出来。为什么这件事值得单独拿出来讲因为大部分采集类项目卡住的地方往往不是“不会发请求”而是“不知道该发什么”。WebSocket的握手阶段通常带着签名、时间戳、随机串、甚至加密后的鉴权字段这些值由前端JS在运行时算出来你静态读代码可能面对的是混淆过的几万行脚本。硬啃代码效率极低而hook的思路是不去逆向算法而是让浏览器自己把算好的结果交给你。适合谁来参考有一定前端调试基础、会用浏览器开发者工具、了解Promise和事件机制、做过常规接口采集的从业者。完全零基础也能看懂思路但实操部分需要你动手跟着调。1.2 为什么选“hook 长连接监听”而不是硬逆算法方案选型这块我踩过不少坑值得展开说。面对一个带签名的WebSocket连接常见有三条路方案做法优点缺点适用场景硬逆算法静态分析混淆代码还原签名函数一次搞定可脱离浏览器成本极高混淆一更新就失效算法简单且稳定中间人抓包抓握手包重放请求快速验证可行性签名有时效重放常失败临时验证Hook注入在关键函数上打桩运行时截获参数不依赖算法细节抗混淆需要浏览器环境需处理时序长期维护的采集项目我最终选hook路线理由很实在混淆可以千变万化但运行时调用的原生API是固定的。不管前端怎么加密最终要发出去的数据总得经过WebSocket构造函数、send方法、或者某个序列化函数。这些是“必经之路”在必经之路上设卡比在迷宫尽头找出口靠谱得多。这里有个关键认知hook不是“破解”而是“观测”。你是在浏览器自己的运行环境里合法地记录它本来就要做的事情。这个定位很重要它决定了你的代码应该尽量轻量、无侵入只做记录和转发不改变原有逻辑否则容易把页面搞崩。1.3 整体架构三层结构我把整个方案拆成三层逻辑上互相独立方便单独调试第一层连接层接管或旁路WebSocket拿到原始消息流。核心是重写window.WebSocket在构造函数和onmessage上做包装。第二层参数层hook住握手前计算签名/鉴权的关键函数把入参和出参dump出来。这一层解决“连接怎么建立”的问题。第三层解析层对拿到的消息做协议解析可能是JSON、可能是自定义二进制帧转成结构化数据落盘。三层之间用统一的消息总线串起来任何一层出问题都能单独定位。这个分层不是拍脑袋是因为实际调试时你会发现连接建立失败和消息解析失败是两类完全不同的问题混在一起排查会疯掉。2. 核心细节解析与实操要点2.1 WebSocket构造函数劫持的正确姿势最直接的入口是window.WebSocket。很多人上来就写window.WebSocket function(){...}结果页面直接白屏——因为原生WebSocket是个类有静态常量CONNECTING、OPEN等和原型方法你用一个普通函数替换页面里但凡用到WebSocket.OPEN的地方全炸。正确做法是保留原型链用Proxy或者继承的方式包装。我倾向用Proxy代码短且不破坏原有行为(function () { const NativeWS window.WebSocket; const hookLog []; window.WebSocket new Proxy(NativeWS, { construct(target, args) { const [url, protocols] args; // 记录握手地址和子协议这是排查连接问题的第一手资料 hookLog.push({ type: connect, url, protocols, ts: Date.now() }); const instance new target(...args); // 包装send记录上行数据 const nativeSend instance.send.bind(instance); instance.send function (data) { hookLog.push({ type: send, data, ts: Date.now() }); return nativeSend(data); }; // 包装onmessage记录下行数据 instance.addEventListener(message, function (evt) { hookLog.push({ type: recv, data: evt.data, ts: Date.now() }); }); window.__wsHookLog hookLog; return instance; } }); })();这段代码有几个细节必须注意。第一construct陷阱里必须new target(...args)不能直接调用否则拿不到实例。第二send要用bind保存原生引用否则递归调用自己。第三监听用addEventListener而不是覆盖onmessage因为页面自己可能也用了onmessage你覆盖掉就把人家逻辑顶了。注意注入时机极其关键。这段脚本必须在页面主脚本执行之前跑否则页面已经缓存了原生WebSocket引用你的Proxy就形同虚设。用浏览器扩展的document_start注入或者在调试时用“请求拦截”功能在文档加载前插入都是可行手段。2.2 签名函数的定位从调用栈反推连接层搞定后你会发现握手URL里带着一长串看不懂的参数比如?signxxxxtsxxxx。这时候参数层要上场了。定位签名函数最有效的方法不是搜关键字混淆后变量名全是a、b、c而是在WebSocket构造被调用的瞬间打断点看调用栈。具体操作在Proxy的construct里加一句debugger;页面暂停后看右侧Call Stack一层层往上翻找到那个“看起来像在拼URL”的函数。通常它附近就有签名计算。找到后对这个函数做hook// 假设定位到签名函数挂在某个对象上先用通用方式扫描 function hookByName(obj, name) { const origin obj[name]; if (typeof origin ! function) return; obj[name] function (...args) { const result origin.apply(this, args); console.log([hook], name, args, args, result, result); return result; }; }如果函数是模块内私有、挂不到全局对象上就用“函数替换 原型污染”的组合拳找到它所在的原型或闭包暴露点。实在找不到暴露点还有一个笨但有效的办法——在String.prototype或JSON.stringify上做文章因为签名结果最终往往要拼进字符串或序列化在这些原生方法上打桩能捞到大量中间值。2.3 消息协议的识别与解析拿到evt.data后第一件事是判断类型。WebSocket的data可能是字符串也可能是Blob或ArrayBuffer。字符串直接看如果是JSON就JSON.parse如果是二进制先转ArrayBuffer再看头部字节。我遇到过的几种常见形态纯JSON文本最省心直接解析。JSON 压缩字符串开头是乱码需要先解压常见的是某种流式压缩。自定义二进制帧前几个字节是魔数和长度后面是payload需要按协议文档或逆向结构体来拆。分片消息一条逻辑消息被拆成多个帧推送需要按序号拼接。判断方法很土但管用把前几条消息的原始数据打印出来看规律。文本类看开头字符二进制类看十六进制头部。这一步不要急着写解析器先人工看二十条样本规律自然浮现。实操心得解析层一定要做“原始数据留存”。我习惯把每条原始消息连同时间戳写进一个环形缓冲区解析失败时能回放。很多协议问题是偶发的没有原始数据根本没法复现。3. 实操过程与核心环节实现3.1 环境搭建与注入链路整个实操分四步走我按真实调试顺序写。第一步准备注入环境。我不用任何需要额外安装的重型工具就用浏览器自带的开发者工具加一个轻量脚本注入手段。最朴素的方式是在控制台手动粘贴hook脚本缺点是刷新就没了适合验证阶段。稳定阶段用浏览器扩展的“内容脚本”机制配置在document_start执行保证先于页面脚本。第二步验证hook是否生效。注入后刷新页面在控制台执行window.__wsHookLog如果能看到connect记录说明连接层通了。如果为空八成是注入时机晚了或者页面用了Worker里的WebSocketWorker环境是独立的主线程hook管不到需要在Worker脚本里单独注入这是个大坑后面细说。第三步dump签名参数。在construct的debugger处暂停翻调用栈定位签名函数打上hook重新触发连接观察控制台输出的args和result。把多次调用的结果对比找出哪些字段是随机的、哪些是时间相关的、哪些是固定的。这一步的产出是一张“参数表”标明每个字段的来源和变化规律。第四步消息解析与落盘。在recv记录的基础上写解析函数把结构化结果通过一个本地接口或直接console导出。调试阶段我一般先导出成JSON文件确认字段完整后再接后续流程。3.2 参数规律分析与复现验证拿到参数表后要验证一件事这些参数能不能在浏览器外复现分两种情况。如果签名算法简单比如就是时间戳加固定盐做哈希那hook只是帮你确认了算法最终可以脱离浏览器。如果算法复杂涉及浏览器指纹、环境检测那就老老实实保留浏览器环境hook方案本身就是最终方案。我倾向于后者因为维护成本低。浏览器环境里跑页面自己会算好一切你只管取。这也是为什么我强调hook要“无侵入”——你得让页面觉得一切正常它才会持续给你产出正确的参数。验证方法连续触发多次连接对比每次的参数确认你的hook没有改变原有行为比如有没有因为你的包装导致send失败、有没有因为日志太多拖慢页面。实测下来Proxy方案对性能影响可以忽略但如果你在recv里做了重计算就要注意别阻塞主线程。3.3 一个完整的消息处理闭环把上面串起来一个可用的闭环是这样的// 消息总线三层之间解耦 const bus { handlers: [], on(fn) { this.handlers.push(fn); }, emit(msg) { this.handlers.forEach(fn fn(msg)); } }; // 解析层注册 bus.on(function (msg) { if (msg.type ! recv) return; let payload; try { payload typeof msg.data string ? JSON.parse(msg.data) : msg.data; } catch (e) { console.warn(parse fail, msg.data); return; } // 这里按实际协议取字段 console.log(结构化数据, payload); });这个闭环的关键在于异常隔离解析失败不能影响连接层连接层出错不能影响页面本身。每一层都包在try/catch里日志分级输出。我见过太多人把hook写得太“贪”一个解析异常把整个页面脚本链带崩结果连原始数据都拿不到了。4. 常见问题与排查技巧实录4.1 高频问题速查表现象可能原因排查方向解决手段__wsHookLog为空注入时机晚于页面脚本看注入配置的执行时机改到文档加载前注入页面白屏/报错Proxy破坏了原生行为检查是否覆盖了静态属性用Proxy而非直接赋值有connect无recv消息走了Worker或SharedWorker看Network里连接归属在Worker脚本内单独注入签名参数每次不同但重放失败参数含一次性nonce或环境绑定对比多次参数差异保留浏览器环境不脱离消息解析乱码二进制帧或压缩打印十六进制头部按协议拆帧或解压hook后页面变卡日志或解析阻塞主线程看Performance面板日志异步化、限流4.2 几个只有踩过才知道的坑坑一Worker里的WebSocket。这是最隐蔽的。页面主线程干干净净Network里却有连接八成是Worker里建的。Worker有独立全局环境主线程的window.WebSockethook完全管不到。解决办法是在Worker脚本加载前注入或者用importScripts拦截。判断方法在Network面板看连接的Initiator如果指向某个worker脚本就实锤了。坑二hook被页面自己的防调试逻辑干扰。有些页面会检测debugger语句的执行频率或者检测console是否被重写。这时候你的hook日志可能触发它的防御。应对方式是降低日志频率、避免在关键路径上打debugger、必要时对console做一层透传包装让页面以为一切正常。坑三消息顺序与分片。WebSocket保证消息顺序但不保证“一条逻辑消息等于一个帧”。有些服务端会把大消息拆成多个帧推前端自己拼。你如果按帧解析就会拿到半截数据。判断方法看消息长度是否规律、是否有明显的序号字段。有的话按序号拼没有的话看协议文档。坑四连接重建时的状态丢失。长连接会断线重连重连时页面可能重新走一遍签名流程。如果你的hook只在首次注入时生效重连后就抓不到了。所以hook要设计成“幂等”的重复注入不产生副作用或者监听连接生命周期事件在重建时重新挂载。提示调试阶段建议把__wsHookLog做成有上限的环形数组比如只保留最近500条。无上限的日志在长时间运行下会吃光内存页面直接卡死这个坑我踩过不止一次。4.3 稳定性与可维护性的经验hook方案最大的敌人不是技术难度是页面更新。前端一改版函数名、调用链、甚至WebSocket的使用方式都可能变。所以我的做法是hook点尽量选“稳定层”——原生API优先于业务函数构造函数优先于具体方法。原生API的签名几年不变业务函数一周三改。另外所有hook逻辑要能一键关闭。我习惯加一个全局开关window.__hookEnabled出问题时先关掉确认是不是hook引起的再逐步打开定位。这个习惯救过我很多次尤其是在页面行为和预期不符的时候先排除自己的干扰永远是第一步。最后说个心态上的事。这类项目最忌讳“一把梭”上来就想全自动。正确的节奏是先能观测拿到原始数据再能理解看懂参数和协议最后才谈自动化。观测阶段多花的时间会在后面省下成倍的返工。我见过太多人跳过观测直接写采集逻辑结果参数一变全盘重来。这个案例后续还能往几个方向扩展一是把hook脚本做成可配置的规则引擎不同站点配不同规则二是把消息解析层抽象成插件协议变了只换插件三是加一层数据校验对拿到的结构化数据做完整性检查异常时自动回放原始数据。这些扩展都不难难的是先把基础的三层结构搭稳。
阅读完成 · 觉得有帮助?
咨询建站