☰
WinForm中CefSharp拦截请求响应与注入JS实战
2026/10/8 15:48:52 网站建设 项目流程

简介:这是一份面向.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 和 Filter

2.1 先搞清楚 CefSharp 的版本分水岭

CefSharp 的 API 在 63 版本前后有一次大改,网上大量老教程还在用IRequestHandler的旧签名,直接抄会编译不过。判断方法很简单:看CefSharp.Core.dll的版本号,或者看packages.config里CefSharp.Common的版本。49 及以前用CefSharp.IRequestHandler的同步接口,63 以后GetResourceRequestHandler成为主入口,IResourceRequestHandler负责具体的请求拦截。这份资源里的写法按 63+ 的接口来,如果你还在 49,需要把GetResourceRequestHandler换成OnBeforeResourceLoad那套。

选型上,WinForm 里嵌浏览器目前主流就三条路:WebBrowser(IE,已淘汰)、WebView2(Edge 内核,微软官方,但拦截请求响应要靠 DevTools 协议,写起来绕)、CefSharp(Chromium 内核,拦截接口最直接)。要抓 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; // 可能为 null(GET 请求) 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 里的表单和 JSON

OnBeforeResourceLoad里拿到的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,解码后是a=1&b=2格式,用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有超时限制,默认几秒,长任务要拆开。如果页面有 CSP(Content-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 请求本来就没有 body,PostData为 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 dll(libcef.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),确认绑定活着再往下写业务,这个习惯帮我省了至少三次通宵排查。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询