ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WinForm中CefSharp拦截请求响应与注入JS实战

WinForm中CefSharp拦截请求响应与注入JS实战 简介这是一份面向.NET桌面开发者的CefSharp实战示例工程基于VS2019与.NET Framework 4.6构建聚焦WinForm窗体中内嵌Chromium浏览器的进阶用法。内容覆盖获取页面加载后的资源、截取Request请求参数、拦截Response响应数据以及向目标页面注入jQuery文件与自定义JS代码等典型场景适合需要做数据采集、页面自动化或混合式桌面应用的中级开发者参考。压缩包共718个文件约371.21MB以273个cs源码、43个cpp与112个h头文件为核心配合174个pak资源包、33个dll及若干pdb、nupkg、xml、config等依赖与配置文件完整保留了CefSharp运行所需的目录结构。目前已有6674人学习下载。工程可直接编译运行便于对照调试请求拦截与脚本注入逻辑理解CefSharp生命周期与事件回调快速迁移到自己的WinForm项目中。1. 从一次抓不到包的 WinForm 内嵌浏览器说起做 WinForm 桌面端的朋友大概率遇到过这种场景程序里嵌了个浏览器控件页面加载完想拿它渲染后的 DOM、想抓它发出去的请求参数、想改它返回的响应体结果发现 WebBrowser 控件IE 内核要么白屏要么拿不到数据换成 CefSharp 之后又不知道从哪下手。这份资源就是围绕这个痛点展开的在 WinForm 窗体程序里用 CefSharp 获取加载后的资源、截取 request 参数、拦截 response 数据以及注入 jquery 文件和自定义 js 代码。它适合两类人——一类是被 IE 内核折磨过、准备迁移到 Chromium 内核的桌面开发者另一类是已经用上 CefSharp但只会 Navigate 和 ExecuteScript遇到请求响应层就卡住的熟手。下面按「能干什么 → 怎么配 → 怎么抓 → 怎么注入 → 坑在哪」的顺序拆开讲每一步都能直接抄。2. CefSharp 的请求响应管线为什么必须走 RequestHandler 和 Filter2.1 先搞清楚 CefSharp 的版本分水岭CefSharp 的 API 在 63 版本前后有一次大改网上大量老教程还在用IRequestHandler的旧签名直接抄会编译不过。判断方法很简单看CefSharp.Core.dll的版本号或者看packages.config里CefSharp.Common的版本。49 及以前用CefSharp.IRequestHandler的同步接口63 以后GetResourceRequestHandler成为主入口IResourceRequestHandler负责具体的请求拦截。这份资源里的写法按 63 的接口来如果你还在 49需要把GetResourceRequestHandler换成OnBeforeResourceLoad那套。选型上WinForm 里嵌浏览器目前主流就三条路WebBrowserIE已淘汰、WebView2Edge 内核微软官方但拦截请求响应要靠 DevTools 协议写起来绕、CefSharpChromium 内核拦截接口最直接。要抓 request/response 这种底层数据CefSharp 的IResourceRequestHandler和IResponseFilter是最省事的这也是这份资源选它的核心理由。2.2 请求拦截的四个关键接口CefSharp 把请求生命周期拆成了几个可重写的点理解它们的分工比背 API 重要接口触发时机能拿到什么典型用途IRequestHandler.OnBeforeBrowse导航前URL、是否用户手势拦截跳转、白名单IResourceRequestHandler.OnBeforeResourceLoad资源请求发出前IRequest、PostData改 header、抓参数IResourceRequestHandler.GetResourceResponseFilter响应头到达后IResponse挂载响应过滤器IResponseFilter.Filter响应体分块到达字节流改 body、抓数据OnBeforeResourceLoad是抓 request 参数的主战场GetResourceResponseFilter返回的过滤器是抓 response 数据的唯一入口。很多人只重写了OnBeforeResourceLoad就以为能拿到返回内容结果发现IResponse里只有 header 没有 body这就是没挂 filter 的典型翻车。2.3 最小可运行的拦截骨架先搭一个能跑起来的IRequestHandler把请求和响应两条线都接上public class CustomRequestHandler : CefSharp.Handler.RequestHandler { protected override IResourceRequestHandler GetResourceRequestHandler( IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) { // 每个请求都会走到这里返回自定义的 ResourceRequestHandler return new CustomResourceRequestHandler(); } } public class CustomResourceRequestHandler : CefSharp.Handler.ResourceRequestHandler { protected override CefReturnValue OnBeforeResourceLoad( IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { // 抓取 request 参数URL、Method、PostData、Headers var url request.Url; var method request.Method; var headers request.Headers; // NameValueCollection var postData request.PostData; // 可能为 nullGET 请求 if (postData ! null) { foreach (var element in postData.Elements) { // 表单字段、上传文件都在这里 var bytes element.GetBytes(); } } return CefReturnValue.Continue; } protected override IResponseFilter GetResourceResponseFilter( IBrowser browser, IFrame frame, IRequest request, IResponse response) { // 只有这里返回非 null才能拿到响应体 return new CustomResponseFilter(); } }逻辑说明GetResourceRequestHandler是总入口每个资源请求包括图片、XHR、文档都会进来一次返回的 handler 决定这个请求怎么处理。OnBeforeResourceLoad里request.PostData对 GET 是 null对 POST 才有值Elements里每个元素可能是字节数组也可能是文件要按ElementType判断。GetResourceResponseFilter返回 null 就等于放弃响应体这是最容易漏的一步。参数说明CefReturnValue.Continue表示放行Cancel表示拦截ContinueAsync用于异步场景。request.Headers是NameValueCollection改它就能改发出去的 header但注意部分 header 受 Chromium 保护改不动。3. 抓 request 参数与 response 数据从 PostData 到 Filter 的完整链路3.1 解析 PostData 里的表单和 JSONOnBeforeResourceLoad里拿到的IPostData结构比想象中复杂尤其是multipart/form-data上传。下面这段把常见几种情况都覆盖了private string ParsePostData(IPostData postData) { if (postData null) return string.Empty; var sb new StringBuilder(); foreach (var element in postData.Elements) { switch (element.Type) { case PostDataElementType.Bytes: // 普通表单字段或 JSON body var bytes element.GetBytes(); if (bytes ! null) sb.Append(Encoding.UTF8.GetString(bytes)); break; case PostDataElementType.File: // 文件上传只能拿到文件名 sb.Append($[File]{element.File}); break; } } return sb.ToString(); }逻辑说明PostDataElementType.Bytes覆盖了application/x-www-form-urlencoded和application/json两种最常见的 body直接按 UTF8 解码即可。PostDataElementType.File只能拿到文件路径拿不到文件内容这是 CefSharp 的设计限制别指望在这里读文件流。参数说明如果 body 是 JSON解码后建议用JsonConvert.DeserializeObject再处理别用字符串截取中文和转义字符会坑你。如果 body 是x-www-form-urlencoded解码后是a1b2格式用HttpUtility.ParseQueryString拆更稳。3.2 用 IResponseFilter 抓响应体响应体是分块到达的IResponseFilter.Filter会被调用多次必须自己维护缓冲区等filter.Status FilterStatus.Done才算收完public class CustomResponseFilter : IResponseFilter { private readonly MemoryStream _stream new MemoryStream(); public FilterStatus Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) { dataInRead 0; dataOutWritten 0; if (dataIn null) { // 数据流结束把攒下来的内容吐出去 var buffered _stream.ToArray(); dataOut.Write(buffered, 0, buffered.Length); dataOutWritten buffered.Length; return FilterStatus.Done; } // 边读边存同时原样透传给浏览器 var buffer new byte[dataIn.Length]; dataInRead dataIn.Read(buffer, 0, buffer.Length); _stream.Write(buffer, 0, (int)dataInRead); dataOut.Write(buffer, 0, (int)dataInRead); dataOutWritten dataInRead; return FilterStatus.NeedMoreData; } public bool InitFilter() true; public void Dispose() _stream.Dispose(); }逻辑说明Filter的返回值决定后续行为NeedMoreData表示还要继续收Done表示结束。dataIn为 null 是结束信号这时候要把缓冲区里的完整内容写进dataOut否则浏览器拿不到数据会白屏。注意dataOut的写入量必须和dataOutWritten一致写多了会抛异常。参数说明_stream是累积缓冲大响应比如几 MB 的 JSON会占内存生产环境建议加个上限超过就丢弃或落盘。InitFilter返回 true 表示过滤器启用返回 false 直接跳过。3.3 把抓到的数据回传到 WinForm 主线程CefSharp 的回调跑在非 UI 线程直接更新控件会抛跨线程异常。常见做法是用Control.Invoke或BeginInvoke把数据丢回主线程private void OnDataCaptured(string url, string body) { if (this.InvokeRequired) { this.BeginInvoke(new Action(() OnDataCaptured(url, body))); return; } // 这里已经在 UI 线程可以安全更新 ListView / TextBox listView1.Items.Add(new ListViewItem(new[] { url, body })); }逻辑说明InvokeRequired判断当前是否在 UI 线程不是就BeginInvoke递归回来。用BeginInvoke而不是Invoke是为了避免死锁——如果 CefSharp 回调线程正持有某个锁Invoke同步等待 UI 线程会卡住。参数说明BeginInvoke是异步投递不阻塞回调线程适合高频数据。如果数据量极大建议先入ConcurrentQueue再用 Timer 批量刷 UI别每个请求都 Invoke 一次。4. 注入 jquery 与自定义 js时机、方式与隔离4.1 注入的三种时机与选择CefSharp 注入 js 有三个入口用错时机脚本会拿不到 DOM方式触发时机适用场景FrameLoadStart文档开始加载注入 polyfill、hook 原生方法FrameLoadEnd文档加载完DOM 就绪操作 DOM、绑定事件ExecuteScriptAsync手动调用按需执行、响应用户操作要注入 jquery 并保证页面脚本能用最稳的是在FrameLoadStart里注入 jquery在FrameLoadEnd里注入业务脚本。因为 jquery 必须在页面自己的脚本执行前就位晚了页面里$就是 undefined。4.2 注入 jquery 文件的两种写法第一种是把 jquery 源码读成字符串直接注入private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) { // 只在主框架注入避免 iframe 重复注入 if (e.Frame.IsMain) { var jqueryCode File.ReadAllText(Scripts\jquery-3.6.0.min.js); e.Frame.ExecuteJavaScriptAsync(jqueryCode); } }第二种是用EvaluateScriptAsync配合RegisterJsObject做 C# 与 js 互调。第一种简单直接适合大多数场景第二种适合需要 js 回调 C# 方法的复杂交互。逻辑说明e.Frame.IsMain判断主框架很关键不加的话每个 iframe 都会注入一遍 jquery页面里会出现多个$冲突。ExecuteJavaScriptAsync是异步的不保证执行顺序如果业务脚本依赖 jquery要放在FrameLoadEnd里。参数说明jquery 文件路径建议用绝对路径或AppDomain.CurrentDomain.BaseDirectory拼接相对路径在打包成安装程序后经常找不到文件这是 WinForm 打包后的经典坑。4.3 注入业务脚本并读取返回值EvaluateScriptAsync能拿到 js 的返回值适合做数据提取private async void ExtractData() { var script (function() { var items []; $(.list-item).each(function() { items.push($(this).text().trim()); }); return JSON.stringify(items); })();; var result await browser.EvaluateScriptAsync(script); if (result.Success result.Result ! null) { var json result.Result.ToString(); // 反序列化后更新 UI } }逻辑说明脚本用 IIFE 包起来避免污染全局返回值必须是可序列化的类型复杂对象用JSON.stringify转字符串最稳。result.Success为 false 时result.Result是异常信息别直接 ToString 当数据用。参数说明EvaluateScriptAsync有超时限制默认几秒长任务要拆开。如果页面有 CSPContent-Security-Policy注入的脚本可能被拦需要在OnBeforeResourceLoad里改响应头去掉 CSP或者用RegisterJsObject绕过。5. 避坑与排查那些让我加班到凌晨的 CefSharp 问题5.1 现象页面白屏日志里没有任何请求原因CefSettings没初始化或者Cef.Initialize在Application.Run之后才调用。CefSharp 要求初始化必须在创建任何浏览器控件之前完成。解决在Program.cs的Main里第一行就初始化[STAThread] static void Main() { var settings new CefSettings(); settings.CachePath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, cache); Cef.Initialize(settings); Application.Run(new MainForm()); }CachePath不设的话默认写到系统临时目录打包后可能没权限这也是白屏的常见原因。5.2 现象PostData 永远是 null原因请求是 GET或者请求走了OnBeforeBrowse而不是OnBeforeResourceLoad。GET 请求本来就没有 bodyPostData为 null 是正常的。解决先确认request.Method是不是 POST再看是不是 XHR 请求。如果是 XHR 但拿不到检查GetResourceRequestHandler有没有被正确重写——很多人重写了OnBeforeResourceLoad但没重写GetResourceRequestHandler导致自定义 handler 根本没被调用。5.3 现象响应体抓到一半就断了原因Filter方法里dataOutWritten和实际写入量不一致或者dataIn为 null 时没把缓冲区吐出去。解决严格按 3.2 的模板写dataIn为 null 时必须把_stream里的内容写进dataOut并返回Done。另外大响应要注意dataOut的容量写不下会抛异常可以分多次返回NeedMoreData。5.4 现象注入的 jquery 报$ is not defined原因注入时机太晚页面脚本已经执行完了或者注入到了 iframe 但业务脚本在主框架。解决jquery 放FrameLoadStart业务脚本放FrameLoadEnd并且都用e.Frame.IsMain限定主框架。如果页面用了defer或async加载脚本FrameLoadEnd也可能早于页面脚本这时候要在业务脚本里加轮询等$出现。5.5 现象打包成安装程序后 CefSharp 报找不到 dll原因CefSharp 依赖一堆 native dlllibcef.dll、icudtl.dat等默认不会自动复制到输出目录。解决在项目里把CefSharp相关包的CopyLocal设为 true或者用CefSharp.Common提供的CefSharp.Build目标。打包工具如 Inno Setup要把x86和x64两个目录都带上CefSharp 按进程位数加载对应的 native dll少一个就崩。6. 进阶用 RegisterJsObject 打通 C# 与 js 的双向调用前面讲的注入都是 C# 单向往页面塞脚本但真实项目里经常需要页面主动调 C#——比如页面里点个按钮要触发 WinForm 弹窗或者写数据库。CefSharp 的RegisterJsObject就是干这个的它把 C# 对象暴露成 js 里的全局对象页面里直接window.obj.method()就能调到 C# 方法。先定义一个绑定类注意方法必须是 public参数和返回值要能被序列化public class JsBridge { public string GetUserName() { return Environment.UserName; } public void SaveData(string key, string value) { // 这里可以写数据库、写文件、更新 UI // 注意这个方法在 CefSharp 的线程上执行更新 UI 要 Invoke } }然后在浏览器控件初始化后注册browser.JavascriptObjectRepository.Register( bridge, // js 里的对象名页面用 window.bridge 访问 new JsBridge(), isAsync: false, // false 表示同步调用true 返回 Promise options: BindingOptions.DefaultBinder);逻辑说明isAsync: false时 js 调用是同步的C# 方法直接返回结果isAsync: true时返回 Promise适合耗时操作。BindingOptions.DefaultBinder是默认序列化器复杂对象建议自己实现IEnumerable或转成 JSON 字符串传。参数说明注册名bridge不能和页面已有全局变量冲突建议加前缀。方法参数超过 4 个或类型复杂时序列化容易出问题稳妥做法是只传一个 JSON 字符串C# 侧再反序列化。验证是否注册成功可以在页面控制台敲window.bridge能打印出对象就说明通了。如果报undefined检查JavascriptObjectRepository是不是在Cef.Initialize之后、浏览器创建之前注册的顺序错了不生效。一个我踩过的坑RegisterJsObject注册的对象在页面刷新后会丢失绑定需要在FrameLoadEnd里重新注册或者用RegisterAsyncJsObject配合JavascriptObjectRepository.ObjectBoundInJavascript事件做动态绑定。从那以后我每次做 CefSharp 项目都会先在FrameLoadEnd里打一行console.log(typeof window.bridge)确认绑定活着再往下写业务这个习惯帮我省了至少三次通宵排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表