拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

C# CefSharp 多账号登录 Cookie 隔离与浏览器指纹修改实战

C# CefSharp 多账号登录 Cookie 隔离与浏览器指纹修改实战 简介这份资源提供了一套基于 C# 与 CEFSharp 的多账号登录与浏览器指纹隔离方案适合有 C# 基础、正在做 Web 自动化、爬虫或账号批量管理的开发者。核心思路是为每个账号创建独立浏览器实例通过 IRequestContext 隔离 Cookie再结合 User-Agent 修改、JavaScript 注入和 FingerprintJS 等办法隐藏真实指纹避免账号间串号与反爬识别。压缩包为 RAR 格式共 873 个文件大小约 376.59MB包含 cs 工程源码、h/cpp 原生相关代码、dll/exe 运行组件、pak 资源文件以及 xml 配置、pdb 调试符号、项目解决方案等多种类型目录结构对应多账号初始化、Cookie 隔离、指纹伪装和自动化操作等模块。工程中还包括 sln/csproj 组织文件、Chromium 运行所需的 snapshot 资源及调试所需 pdb便于还原项目入口和排查问题。除源码外还整理了 CEFSharp 的 API 调用示例、项目组织方式和扩展思路便于二次开发。已有 4442 人学习下载对需要快速落地多账号浏览器应用的开发者有较强参考价值。1. C# cefsharp 多账号同时登陆先把 Cookie 串号这个坑填上做过多账号登录自动化的 C# 工程师大概率都遇到过“A 账号刚登录完B 账号打开页面竟然也是登录状态”的灵异现场。原因并不玄学CefSharp 默认把同一个进程里所有 Browser 实例的 Cookie、LocalStorage、IndexedDB 全部丢进同一个全局 RequestContext。只要你不做 cookie 隔离浏览器内核和服务端看到的两个账号就是同一个人。这个标题实际要拆成两件事一是用独立的 RequestContext 独立缓存目录把账号会话彻底隔开二是在页面脚本执行之前注入指纹修改脚本让 User-Agent、Canvas、WebGL 这些可检测特征在每个账号之间互不相同从而降低账号被服务端批量关联的概率。适合用 WinForms 或 WPF 做多账号管理工具、后台自动化脚本的 C# 开发者。下面按“架构 → Cookie 隔离 → 指纹 → 封装 → 排错 → 验证”的顺序讲完整套落地路径。2. 多账号 Cookie 隔离每个账号一个独立 RequestContext2.1 为什么不能开多个 Browser 就完事全局缓存与 Cookie 串号的根源CefSharp 的多浏览器实例并不是“各管各的”。在同一个进程里 new 多个ChromiumWebBrowser如果不显式传IRequestContext它们默认全部挂在Cef.GetGlobalRequestContext()上。这个全局上下文维护着整份 Cookie、HTTP 缓存、LocalStorage、IndexedDB、Service Worker 注册表任何一个页面写入的数据其他 Browser 实例都能读到。更隐蔽的一点是CEF 的多进程模型里渲染进程是按需创建的但网络请求、Cookie 读写、存储管理都集中在浏览器进程的网络层。两个 Browser 控件即使各自有独立的渲染进程网络层走的仍然是同一个上下文。所以“每个控件开独立子进程”并不等于“每个控件有独立会话”这一点和很多人直觉相反也是后来一系列串号 bug 的根源。正确的隔离单位是CefRequestContext不是 Browser 控件。每个账号对应一个独立 RequestContext再把这个上下文传给对应 Browser 的构造函数Cookie、缓存、存储才真正分开。2.2 账号目录结构与 CefRequestContext 的创建参数我一般把账号存储做成固定的目录约定方便排查和备份accounts/ acc_001/ cache/ # CEF 请求上下文缓存含 Cookies 文件 cookies.json # 手动导出的 Cookie 快照 acc_002/ cache/ cookies.json下面这段是创建隔离上下文的核心代码using CefSharp; using CefSharp.WinForms; private IRequestContext CreateAccountContext(string accountId) { var cacheRoot Path.Combine(AppContext.BaseDirectory, accounts, accountId, cache); Directory.CreateDirectory(cacheRoot); var contextSettings new CefRequestContextSettings { CachePath cacheRoot, PersistSessionCookies true, // 会话 Cookie 写盘重启后不丢 PersistUserPreferences true // localStorage 等偏好数据落盘 }; return new CefRequestContext(contextSettings); }逻辑说明每个账号一个CefRequestContextSettings其中CachePath指向账号专属目录。CefRequestContext一旦创建就与该目录绑定后续所有 HTTP 缓存、Cookie、存储都落在这个目录下互不干扰。参数说明PersistSessionCookies决定 session 级 Cookie 是否持久化到磁盘默认是 false意味着重启进程后 session Cookie 全部蒸发PersistUserPreferences控制 localStorage 这类偏好存储多账号场景建议两个都打开。注意CefRequestContextSettings.CachePath与CefSettings.CachePath是两个层级前者只作用于当前上下文后者是全局兜底下文避坑章节会专门说二者的关系。2.3 PersistSessionCookies 与 FlushStore会话不丢的最后一个环节很多人在这一步会犯一个错把PersistSessionCookies true设了程序正常退出前什么都不做结果下一次启动登录态还是丢了。原因是 CEF 的 Cookie 写入是异步批量的进程被强制结束时最后一段时间产生的 Cookie 变更可能还在内存里没来得及落盘。要可靠地保存会话需要在退出前主动刷新 Cookie 存储public static Taskbool FlushStoreAsync(ICookieManager cookieManager) { var tcs new TaskCompletionSourcebool(); cookieManager.FlushStore(new FlushCallback(tcs)); return tcs.Task; } public class FlushCallback : ICompletionCallback { private readonly TaskCompletionSourcebool _tcs; public FlushCallback(TaskCompletionSourcebool tcs) _tcs tcs; public void OnComplete(bool success) _tcs.TrySetResult(success); public void Dispose() { } }逻辑说明FlushStore是异步方法回调里的true/false表示写盘是否成功。上面的封装把回调转成Taskbool方便在退出逻辑里await。实际项目里我一般把这段放在账号列表“保存全部并退出”的按钮事件里等所有账号的 FlushStore 都返回后才真正调用Cef.Shutdown()。参数说明FlushStore只负责把当前内存中的 Cookie 变更写进 CachePath它不会擅自决定“哪些 Cookie 属于谁”。真正决定 Cookie 归属的是 RequestContext所以调用前先确认你拿到的ICookieManager是从账号自己的上下文取出来的。2.4 从 Cookie 列表备份到 CefCookie 序列化除了靠 CachePath 持久化多账号工具通常还希望把 Cookie 单独导出一份方便迁移账号或换机器。CefSharp 里遍历 Cookie 的入口是ICookieManager.VisitAllCookies回调是逐个 Cookie 触发的需要自己收集public class CookieDumpVisitor : ICookieVisitor { private readonly ListCefCookie _cookies new(); private readonly TaskCompletionSourceListCefCookie _tcs new(); public TaskListCefCookie Task _tcs.Task; public bool Visit(CefCookie cookie, int count, int total, ref bool delete) { _cookies.Add(cookie); if (count total - 1) { _tcs.TrySetResult(_cookies); } return true; } } var cookieManager requestContext.GetCookieManager(null); var visitor new CookieDumpVisitor(); cookieManager.VisitAllCookies(visitor); var cookies await visitor.Task;逻辑说明Visit回调每进来一个 Cookie 就追加进列表count是当前序号total是总数。ref bool delete设为 false 表示只读取不删除返回 true 表示继续遍历。收集完成后转成 JSON 存到账号目录下就完成了一次可恢复的 Cookie 快照。参数说明CefCookie里有Name、Value、Domain、Path、Secure、HttpOnly、Expires、Creation等字段。序列化时至少要保留前四项和最关键的 Expires否则回填时容易遇到过期判断问题。HttpOnly 标记也要保留因为很多登录态 Cookie 都是 HttpOnly回填时被浏览器丢弃是你第一个要排查的点。备份和恢复看似绕路但它解决了一个实际问题同一套指纹和账号要换机器继续跑时重新登录一遍可能被风控盯上直接回填 Cookie 快照是更稳的路。3. 修改浏览器指纹CefSharp 能改的与必须用 JS 补的3.1 User-Agent 两层结构请求头改了navigator.userAgent 没变初做指纹修改的人经常栽在 User-Agent 上用IRequestHandler把每个请求头里的 UA 改成了目标值抓包看也确实变了但页面里的navigator.userAgent还是 CEF 默认的 Chrome 版本。因为 UA 在 Chromium 里存在两层HTTP 请求头是一层渲染进程里的navigator.userAgent是另一层。JS 读取的是后者单纯改请求头只骗得过服务端日志骗不过页面里的 JS 检测。CefSharp 里CefSettings.UserAgent是进程级的一旦Cef.Initialize之后就不能逐个实例改。而多账号场景每个账号的 UA 不一样所以常规做法是进程级设置一个公共的默认 UA保证所有实例的navigator.userAgent至少有合理基线每个账号实例通过请求处理器改写 HTTP 头的 UA同时用注入脚本覆写navigator.userAgent、navigator.platform、navigator.language让 JS 读取到与请求头一致的值。请求头这一层用下面这段public class AccountResourceRequestHandler : CefSharp.Handler.ResourceRequestHandler { private readonly string _userAgent; private readonly string _acceptLanguage; public AccountResourceRequestHandler(string userAgent, string acceptLanguage) { _userAgent userAgent; _acceptLanguage acceptLanguage; } protected override bool OnBeforeResourceLoad( IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { request.SetHeaderByName(User-Agent, _userAgent, true); request.SetHeaderByName(Accept-Language, _acceptLanguage, true); return false; } }逻辑说明OnBeforeResourceLoad在资源请求发出前触发第三参数true表示覆盖同名旧值。返回 false 表示放行请求。这里把 UA 和 Accept-Language 都改了因为指纹检测通常同时看这两个头。参数说明Accept-Language建议按账号设定为固定的几种组合比如zh-CN,zh;q0.9,en;q0.8不要所有账号都用同一个值否则 UA 变了语言却都是同一序列还是会被聚簇关联。navigator层的覆写在 3.2 节的注入脚本里一并处理。3.2 Canvas 指纹在 document 创建阶段注入噪点脚本Canvas 指纹是服务端检测多账号最高频的手段之一。原理是让页面绘制一段文本和图形把canvas.toDataURL()的结果做哈希不同 GPU、不同渲染驱动会得到不同值同一台机器则长期稳定。多账号场景要把这个稳定值改成“每个账号各自稳定”。最有效的注入时机是 document 创建阶段也就是页面自带脚本还没运行时。CefSharp 在 Browser 实例初始化后立刻注册注入脚本即可private static string BuildCanvasNoiseScript(string accountId) { return $ (function() {{ if (window.__fp_account_noise) return; window.__fp_account_noise {accountId}; var originalToDataURL HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL function(type, quality) {{ var ctx this.getContext(2d); if (ctx this.width 4 this.height 4) {{ ctx.save(); ctx.globalAlpha 0.001; ctx.fillStyle #000000; ctx.fillRect(2, 2, 1, 1); ctx.restore(); }} return originalToDataURL.call(this, type, quality); }}; }})(); ; }逻辑说明脚本为每个账号固定一个__fp_account_noise标记值防止同一页面重复注入。真正的改动发生在toDataURL被调用前往 Canvas 的 (2,2) 坐标画一个 alpha 只有 0.001 的黑点。这个点视觉上不可见但会改变最终生成的 base64 字符串从而改变哈希值。参数说明之所以选 (2,2) 而不是 (0,0)是因为很多检测脚本会在 Canvas 上先写白底或清空角落上的点容易被覆盖取中间偏左的位置命中率高一些。globalAlpha 0.001是经过权衡的太大肉眼可见影响体验太小在某些浏览器渲染下可能被压缩掉Windows 上 0.001 到 0.005 是实测比较稳的区间。这个脚本要和账号绑定后固定下来不要每次启动换个随机值——指纹稳定比指纹奇怪更重要。3.3 WebGL 与自动化特征覆写参数 隐藏自动化标记WebGL 指纹靠的是WEBGL_debug_renderer_info扩展暴露的UNMASKED_VENDOR_WEBGL和UNMASKED_RENDERER_WEBGL两者分别返回 GPU 厂商和具体渲染器字符串。同一个显卡的返回值是固定的不同账号如果共享这一特征风控很容易做聚类。常见的做法是给每个账号维护一组预置的厂商和渲染器字符串而不是简单拼个后缀。拼接后缀会让特征变得“刻意”预置真实值更像正常用户(function() { var ACCOUNT_NOISE acc_001; var VENDOR_TABLE { acc_001: Google Inc. (AMD), acc_002: Google Inc. (Intel) }; var RENDERER_TABLE { acc_001: ANGLE (AMD, AMD Radeon RX 6600 Direct3D11 vs_5_0 ps_5_0, D3D11), acc_002: ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11) }; function patchWebGL(proto) { var origGetParameter proto.getParameter; proto.getParameter function(pname) { if (pname 37445) { return VENDOR_TABLE[ACCOUNT_NOISE] || origGetParameter.call(this, pname); } if (pname 37446) { return RENDERER_TABLE[ACCOUNT_NOISE] || origGetParameter.call(this, pname); } return origGetParameter.call(this, pname); }; } patchWebGL(WebGLRenderingContext.prototype); if (WebGL2RenderingContext) { patchWebGL(WebGL2RenderingContext.prototype); } Object.defineProperty(navigator, webdriver, { get: function() { return undefined; } }); })();逻辑说明37445和37446分别是UNMASKED_VENDOR_WEBGL和UNMASKED_RENDERER_WEBGL的常量值。脚本同时 patch WebGL1 和 WebGL2 的原型避免检测脚本升级后用 WebGL2 对比 WebGL1 发现不一致。最后用Object.defineProperty把navigator.webdriver抹成 undefined降低自动化检测嫌疑。参数说明预置表的 GPU 型号最好与账号所在机器的真实硬件差异不要太大否则渲染效果会露馅。比如机器是 AMD 显卡但声明成 NVIDIAWebGL 实际绘制出来的抗锯齿、纹理精度等细节会被更精细的脚本识别出矛盾。稳妥的做法是让“不同账号”使用“同品牌不同型号”的字符串而不是跨品牌乱跳。3.4 指纹的边界哪些是 CefSharp 做不到的把丑话说在前面CefSharp 改不了所有指纹。浏览器指纹是一个综合体CefSharp 这种基于 CEF 封装、复用系统 Chromium 的方案在以下几项上天然受限时区Date.prototype.getTimezoneOffset可以通过 JS 覆写但Intl.DateTimeFormat().resolvedOptions().timeZone读的是 C 层数据JS 改不动会导致前后不一致字体列表系统安装字体通过 Canvas 测量和 CSS 枚举暴露CEF 不提供按实例隔离字体表的开关硬件并发数navigator.hardwareConcurrency可以 JS 覆写但和 WebGL 渲染器、Canvas 绘制性能之间的联动难以伪装的完全自洽真实 IP 归属指纹只能影响浏览器侧数据账号出口网络的 IP 地域和设备指纹对不上照样会被关联。这些边界意味着 CefSharp 方案适合“降低被关联概率”而不是“伪装成完全不同的另一台电脑”。做项目评审时要和需求方对齐预期否则后期验收会卡在“为什么时区没变”这种问题上。4. 落地封装 AccountBrowserManager从账号模型到多开实例4.1 AccountProfile 与指纹参数生成把账号配置抽象成一个 Profile指纹参数全部由它派生避免散落在各段代码里public class AccountProfile { public string AccountId { get; set; } public string UserAgent { get; set; } public string AcceptLanguage { get; set; } public string CanvasNoiseSeed { get; set; } public int TimezoneOffsetMinutes { get; set; } public string StartUrl { get; set; } public string CacheDir { get; set; } public string CanvasNoiseScript BuildCanvasNoiseScript(CanvasNoiseSeed); public string WebGlScript BuildWebGlScript(AccountId); }逻辑说明CanvasNoiseSeed是账号维度的常量生成指纹脚本时用同一个种子保证每次启动得到的指纹一致。AcceptLanguage不能只写语言建议带上 q 值格式更接近真实浏览器。CacheDir就是第 2 章里那个目录约定Profile 和数据文件放一起备份账号时连目录一起拷走就行。4.2 创建隔离浏览器实例的最小代码封装管理器把 Profile 到 Browser 的装配过程固定下来public class AccountBrowserManager { private readonly Dictionarystring, AccountBrowserItem _items new(); public AccountBrowserItem Launch(AccountProfile profile) { if (_items.ContainsKey(profile.AccountId)) return _items[profile.AccountId]; var contextSettings new CefRequestContextSettings { CachePath profile.CacheDir, PersistSessionCookies true, PersistUserPreferences true }; var requestContext new CefRequestContext(contextSettings); var browser new ChromiumWebBrowser(profile.StartUrl, requestContext); browser.RequestHandler new AccountRequestHandler(profile); // 在浏览器初始化完成后立即注册指纹脚本保证页面自带脚本执行前注入 browser.IsBrowserInitializedChanged (sender, args) { if (args.IsBrowserInitialized) { var js profile.CanvasNoiseScript profile.WebGlScript; browser.AddScriptToExecuteOnDocumentCreatedAsync(js); } }; var item new AccountBrowserItem(profile, browser, requestContext); _items.Add(profile.AccountId, item); return item; } }逻辑说明ChromiumWebBrowser构造函数第二个参数传requestContext这是 Cookie 隔离的关键一步。AddScriptToExecuteOnDocumentCreatedAsync是 CefSharp 提供的 document 创建阶段注册接口比在FrameLoadEnd里执行脚本早得多能覆盖第一个页面。参数说明IsBrowserInitializedChanged是为了确保注册脚本时底层浏览器已经就绪。这里有一个时序细节如果StartUrl是非空地址注册动作在一开始的导航里可能慢半拍。更稳的做法是先把StartUrl置空或设成about:blank等初始化完成、指纹脚本注册好之后再LoadUrl到目标地址。代价是首屏加载慢零点几秒换来的是指纹稳定值得。4.3 登录态回填与账号切换Cookie 快照回填用于换机器或恢复会话这一步要和 CookieManager 配合public static async Task RestoreCookiesAsync( IRequestContext requestContext, IEnumerableCefCookie cookies) { var cookieManager requestContext.GetCookieManager(null); foreach (var cookie in cookies) { var url (cookie.Secure ? https:// : http://) cookie.Domain.TrimStart(.); cookieManager.SetCookie(url, cookie); } await FlushStoreAsync(cookieManager); }逻辑说明SetCookie的第一个参数是 Cookie 归属的 URL必须带 scheme且域名要和 Cookie 的 Domain 匹配。TrimStart(.)用于处理.example.com这种带头点的域名不然拼出的 URL 不合法。回填完成后再 FlushStore确保写盘。参数说明CefCookie.Expires如果是默认值不是有效的未来时间浏览器会把 Cookie 当成 session Cookie 处理。所以序列化时一定要把Expires完整保存回填后也要确认这个字段没有被序列化过程丢掉否则恢复的登录态活不过一次进程重启。4.4 资源开销控制多实例不是无上限开CefSharp 每个 Browser 会拉起独立渲染进程打开 10 个账号任务管理器里就能看到 10 个CefSharp.BrowserSubprocess进程外加 GPU 进程、网络进程各一份。内存占用通常在单实例 200MB 到 500MB 之间视页面复杂度而定。实测中我一般按三个原则控制同时打开的实例数控制在 6 到 8 个以内超过的账号用“先起 Browser → 完成登录 → 立刻 FlushStore → 关闭实例”的批处理流程而不是全部常驻用CefSettings.CefCommandLineArgs限制渲染进程数量比如renderer-process-limit2减少进程数代价是多个 Browser 共享渲染进程隔离性下降权衡后我通常只在低配机器上开页面确定不需要后马上Browser.Dispose()释放 Renderer 进程和 GPU 资源而不是把 Browser 控件继续挂在窗体上。如果业务要求几十个账号同时在线CefSharp 这种重量级方案就不合适了应该考虑无头浏览器集群或服务端浏览器容器方案。5. 多账号 指纹隔离排查五个翻车现场与具体解法5.1 Cookie 串号B 账号带着 A 的登录态打开了页面现象A 账号登录成功后打开 B 账号的 Browser发现 B 页面直接是 A 的登录状态。抓包看到 B 的请求里带着 A 的 Cookie。原因创建ChromiumWebBrowser时没有传入独立 RequestContext两个 Browser 全部挂在全局上下文下。Cookie 存储是上下文的资源不是 Browser 控件的资源。解决每个账号显式new CefRequestContext(settings)并传入 Browser 构造函数缓存目录各自独立。这行代码省不得。还有一个隐蔽变种CefSettings.CachePath全局设了cache/default而账号上下文 CachePath 设了cache/acc_001此时出现串号要检查是否某些请求走了全局上下文而非账号上下文。5.2 请求头 UA 改了navigator.userAgent 还是默认值现象请求头抓包 UA 已正确页面 JS 检测navigator.userAgent还是Chrome/xxx默认值造成服务端后端记录与前端埋点数据不一致。原因UA 在 Chromium 内部有两套取值来源。请求头由网络层控制navigator.userAgent由渲染进程在创建时从进程级设置读取。解决双层配合。进程级CefSettings.UserAgent设置公共基线实例级请求处理器改请求头注入脚本覆写navigator.userAgent、platform、language。注入脚本里用Object.defineProperty(navigator, userAgent, { get: () UA })注意要用 defineProperty 而不是直接赋值直接赋值在大多数 Chrome 版本上无效。5.3 指纹注入脚本比页面自身的脚本慢了一拍现象指纹检测页使用同一个账号反复刷新Canvas hash 时变时不变并且页面早期执行的检测代码总能拿到真实 canvas 值。原因注入时机不对。在FrameLoadEnd里用ExecuteScriptAsync注入此时页面文档已解析完毕页面自带的检测脚本早已执行过toDataURL也只能管到注入完成之后的调用。解决改用AddScriptToExecuteOnDocumentCreatedAsync并且注册动作放在 Browser 初始化事件里第一时间触发。如果严格到首屏都不能露真就从about:blank启动注册完成后再导航。另一种兜底是维护一个已知的检测页面列表这类页面强制先导航about:blank再二次跳转。5.4 两个实例共用一个缓存目录第二个直接白屏现象第二个账号的 Browser 打开后白屏日志里出现 storage 相关的锁冲突或数据库损坏错误。原因Chromium 的 SQLite 存储Cookies、Local Storage不支持两个上下文同时写同一个目录。账号隔离时只改了 RequestContext没有改 CachePath两个上下文指向同一目录。解决每个账号的 CachePath 必须唯一创建目录时用账号 ID 而不是时间戳这样重启后还能复用同样的指纹数据。如果一个目录曾经被多实例同时打开过里面的 SQLite 文件可能已经损坏直接把账号缓存目录删掉重新生成即可。5.5 FlushStore 不等待退出后登录态全部蒸发现象界面上能看到登录成功也调了 Cookie 相关接口重启后全部要求重新登录。原因Cookie 写盘是异步的程序退出时FlushStore回调还没返回进程就被终止内存中的 Cookie 变更全部丢弃。很多人以为“设置了 PersistSessionCookies true 就万事大吉”这个属性只管写入策略不管退出时序。解决退出流程用任务等待。把第 2 章的FlushStoreAsync作为在每个实例退出前的必经步骤等所有账号的刷新任务完成后再执行Cef.Shutdown()。如果还丢检查是不是在FormClosing里用了同步阻塞等待导致 UI 线程卡死async void事件里正确 await 才是正解。6. 验证指纹与 Cookie 隔离是否生效一份可执行的检测脚本落地最后一步是验证不要光看感觉。我一般会开一个本地检测页把当前环境的关键指纹用 JS 汇总再通过EvaluateScriptAsync拿回 C# 侧比对var verifyJs (function() { var canvas document.createElement(canvas); canvas.width 200; canvas.height 60; var ctx canvas.getContext(2d); ctx.textBaseline top; ctx.font 14px Arial; ctx.fillText(fp-check- Date.now(), 2, 2); var canvasFp canvas.toDataURL().substring(0, 200); var gl document.createElement(canvas).getContext(webgl); var ext gl.getExtension(WEBGL_debug_renderer_info); return JSON.stringify({ ua: navigator.userAgent, platform: navigator.platform, language: navigator.language, canvasFp: canvasFp, webglVendor: ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : null, webglRenderer: ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : null, timezoneOffset: new Date().getTimezoneOffset() }); })(); var response await browser.EvaluateScriptAsync(verifyJs); var json response.Result as string; var fingerprint JsonConvert.DeserializeObjectFingerprintSnapshot(json);逻辑说明这段脚本把 UA、平台、语言、Canvas 哈希、WebGL 厂商和渲染器打包成 JSON再回到 C# 侧解析。做验证时跑两个账号对比不同账号的canvasFp和webglRenderer应该明显不同同一个账号连续刷新几次canvasFp应保持稳定或只在末尾少量字符变化。参数说明canvasFp只截取前 200 字符做快速判断正式校验建议截全串后做 SHA256。WebGL 的UNMASKED_*常量值恰好对应ext.UNMASKED_VENDOR_WEBGL和ext.UNMASKED_RENDERER_WEBGL不要写死 37445老版本浏览器不一定注册同一个值。这轮验证赶早不赶晚。我见过有人在项目里搭好了整套多账号框架上生产前才想起拿指纹检测网站跑结果发现所有账号的 WebGL 渲染器都带了同一个账号后缀一查是 3.3 节的注入脚本里表名写错了。吃一堑长一智后来每次加新账号或改指纹参数第一件事就是把这套脚本跑一遍把每个账号的指纹快照存档。以后再遇到“为什么 A 账号被关联”的反馈翻快照对比比翻代码快得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表