ARTICLE DETAIL

资讯详情

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

反广告拦截器原理拆解:从广告拦截机制到检测实现

反广告拦截器原理拆解:从广告拦截机制到检测实现 反广告拦截器这个词我最早是2018年在自己站点广告后台里真正接触到的。当时运营同事反馈广告位展示量掉得厉害点击率也异常排查了一圈才发现相当一部分流量来自装了广告拦截器的浏览器。服务器成本一分不少广告收入却打了折于是我们开始认真研究这个“曲线救国”的方向反广告拦截器Anti-Adblock。简单说它是一段运行在网站前端的代码用来判断当前浏览器里是否有广告拦截器在运行如果有就决定是弹警告、遮内容、引导放行还是把被拦截的广告位替换成自家推广。这篇文章我打算把它从里到外拆一遍从广告拦截器的底层原理到反广告拦截器的检测手段再到产品落地与常见坑位完整梳理一套你自己也能看明白、能参考落地的认识。1. 先搞懂对手广告拦截器到底在拦截什么反广告拦截器本质上是在和广告拦截器博弈所以第一步不是研究怎么“反”而是搞清楚广告拦截器究竟动了哪几个环节。只有知道对手在哪个环节拦截才能在对应的位置设计检测逻辑。1.1 请求层让广告脚本根本没机会加载大部分广告AdSense、联盟素材、第三方跟踪都来自外部域名比如 googleads.g.doubleclick.net、googlesyndication.com、amazon-adsystem.com 这些。广告拦截器最基础的能力就是在网络请求发出之前把它拦下来。以 Chrome 扩展为例Manifest V2 时代常用chrome.webRequest.onBeforeRequest监听匹配到广告域名就直接cancel掉这次请求。Manifest V3 之后改成了chrome.declarativeNetRequest用一组声明式规则来阻止请求规则本身长这样{ id: 1, priority: 1, action: { type: block }, condition: { urlFilter: ||doubleclick.net^, resourceTypes: [script, image] } }urlFilter里的||表示域名边界结尾的^表示分隔符这个写法在过滤规则里非常常见。只要请求 URL 命中规则浏览器直接拦截页面脚本根本拿不到广告内容自然没有任何展示。所以很多站长会发现广告位区域是空的而不是显示一个“加载失败”的图标。请求被拦截后站点拿不到广告响应前端 JS 只能读到超时或错误状态。1.2 样式层把广告容器“画”成隐形请求拦截之外还有一类更温柔的拦截方式元素隐藏。过滤规则集里大量存在类似这样的规则##.ad-banner ##.ad-container ###ads-toolbox这些规则通常来自 EasyList、uBlock Origin 的默认过滤清单。浏览器扩展会把这些选择器转换成页面上的一段 CSS强制给匹配到的元素加上display: none !important。广告脚本可能仍然执行了广告资源可能也加载了但用户看不到任何东西。为什么会有这种方案因为有些广告是页面内嵌代码直接输出的不经过外部域名请求阻断拦不到还有一些广告容器同时在服务端渲染了内容单纯隐藏能减少页面重排体验更平滑。但无论哪种方式广告位在浏览器里被“画”成隐形了反广告拦截器就可以顺着这个特征做文章。1.3 脚本层与过滤器生态拦截依据从哪来广告拦截器之所以“聪明”核心是有一份不断更新的过滤器清单。这份清单靠社区爬取、人工维护、自动测试来更新收录了广告联盟的域名、广告容器常见 id/class、追踪脚本特征等。有了这份清单拦截就变成模板匹配请求 URL 命中就断掉DOM 选择器命中就隐藏。但也正因为这样整个拦截体系存在一个天然弱点它依赖“特征”。一旦特征变化拦截效果就会下降。反广告拦截器恰恰抓住了这一点你可以创建出“长得极像广告”的元素和请求让拦截器自己暴露自己。2. 反广告拦截器的检测手段网站怎么发现广告被拦截广告拦截器是在“暗中”工作的网站需要把它逼出来。目前前端最实用的检测手段可以分为三大类诱饵请求、DOM 样式反向侦察、网络与性能佐证。实际生产环境中成熟的检测库往往会把它们组合起来用降低误报率。2.1 最经典的 Bait 诱饵方案拿“假广告”试水Bait 方案是所有检测手段里最经典、也最容易理解的一种。思路是这样的我在页面里动态创建一个图片或脚本标签把它的 src 指向一个“看起来很像广告资源”的地址。如果当前浏览器装了广告拦截器这个请求大概率会被过滤器直接拦截于是标签会立刻触发onerror回调。如果用户干干净净这个请求大概率会正常加载然后触发onload。早期像 FuckAdBlock 这类库就是这么干的代码核心长这样function testBait(timeout) { return new Promise(function (resolve) { var img new Image(); var timer setTimeout(function () { resolve(false); }, timeout || 800); img.onload function () { clearTimeout(timer); resolve(false); }; img.onerror function () { clearTimeout(timer); resolve(true); }; // 找一个大概率被过滤清单收录的广告域名 img.src https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?r Math.random(); }); }这里有个关键细节onerror触发的速度。广告拦截器是在请求发出阶段就 cancel所以onerror几乎毫秒级到达。而如果是网络断掉、DNS 解析失败导致的onerror通常需要更长等待。因此代码里设置了一个超时时间比如 800ms超过这个时间还没有触发onerror就认为没有被拦截。这个超时值太短可能误报太长则影响检测速度需要实际调。2.2 DOM 与样式反向侦察检查“隐形广告框”请求诱饵能检测到“请求拦截型”广告拦截器但很多拦截器更偏好元素隐藏模式。它们不拦截请求而是让广告容器隐形。这时候前端创建一些命中过滤规则特征的元素再用getComputedStyle读取计算样式就能发现异常。举个例子function testDom() { var candidates [ { tag: div, className: banner-ad }, { tag: div, className: ad-container }, { tag: div, id: ad-banner } ]; var detected false; for (var i 0; i candidates.length; i) { var el document.createElement(candidates[i].tag); el.className candidates[i].className || ; if (candidates[i].id) el.id candidates[i].id; el.style.cssText position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;; document.body.appendChild(el); var cs getComputedStyle(el); if (cs.display none || cs.visibility hidden || parseFloat(cs.opacity) 0) { detected true; } document.body.removeChild(el); } return detected; }这个方案的逻辑在于我自己创建的 DOM 元素本来没有任何遮遮掩掩的动机如果它的最终显示状态是display:none那一定有第三方规则作用在它身上。而第三方规则只能是广告过滤器。这里我踩过一个坑过滤器规则的针对性往往很强比如某些规则是###ad-wrapper但你的候选元素只有ad-containerclass就匹配不上。所以真实项目里不能只造一两个特征要尽量覆盖 EasyList 里最常见的广告容器 id/class。还有一些过滤规则挂在父级上子元素getComputedStyle可能读不到hidden这属于检测方案的固有盲区所以需要组合多种手段。2.3 网络与性能层佐证加载时延、请求状态与资源时间线前两种方案已经能覆盖大多数场景但在一些极端情况里会产生误判比如页面响应慢、广告域名被公司防火墙拦了、用户装了“反指纹”类隐私插件。于是更高阶的检测会用浏览器性能 API 来佐证。原理是记录广告域名的资源加载时间线正常加载会出现在performance.getEntriesByType(resource)里被拦截则会表现为请求不完整或干脆没有条目。也可以结合fetch()对广告地址发一个遵守 CORS 的探测请求看它返回的是ok还是网络错误配合AbortSignal.timeout做超时控制。这类检测更精确但代码更重通常只在比较敏感的页面用。反广告拦截库实际部署时常见的做法是“主检测同步执行辅检测异步兜底”先用 Bait 和 DOM 检测快速得出结果再在requestIdleCallback或setTimeout里做性能层验证修正异常状态。这样可以兼顾体验和准确率。2.4 成熟库的检测策略对比FuckAdBlock、BlockAdBlock 与自研自己做一套组合检测听上去不难但真正让它稳定运行其实很费心。社区里比较成熟的开源库有 FuckAdBlock 和它的后续维护版本 BlockAdBlock。两者的核心思路都还贴着上面的原理只是维护成本和细节处理不同。方案主要检测手段误报控制维护成本适用场景FuckAdBlockBait 图片 onerror依赖超时参数低原作者已很少更新小型站点、展示提示BlockAdBlockBait DOM 样式检测组合多条件组合较稳中有社区维护内容站、需要较准检测完全自研按需组合所有手段 业务侧上报可深度定制高需要持续跟过滤器变化对准确率要求极高的大站选型建议很简单如果只是“用户开了拦截器就提示一句”用 BlockAdBlock 足够如果需要把检测结果和订单、会员体系、广告位替换联动那就值得自研因为业务逻辑耦合程度一高通用库往往满足不了。3. 检测之后怎么办反广告拦截的处置与产品化检测本身不是目的检测出来之后做什么才是真正影响收入、体验和用户留存的部分。不同站点的策略差异很大但技术实现上基本可以归成几条路线。3.1 弹窗、遮罩与内容门禁最常见、也最让用户印象深刻的处置方式是“全屏警告”检测到拦截器后页面插入一个position: fixed的遮罩层提示用户关闭广告拦截器。技术上要注意几点遮罩层要有足够高的z-index否则抵不过广告过滤规则顺手隐藏掉。讽刺的是一些网站反广告拦截遮罩也会被过滤器清单收录如果你把遮罩元素的 class 写成adblock-warninguBlock 一类的扩展可能直接把整个遮罩隐藏等于做无用功。所以生产代码里遮罩元素命名越中性越好比如modal-tip、paywall-tip尽量少和广告特征沾边。另外要注意“检测、展示”的时序。通常是把检测结果写在 Promise 里检测完成后再挂载遮罩 DOM。延迟太久用户以为页面卡了太早又可能误伤正常访问一般建议页面主内容可见后 300~500ms 再弹。还有种做法是“内容门禁”遮罩只盖住正文下方顶部露出标题用户往下滑动时看到模糊或者截断的内容再提示放行。这种设计比全屏弹窗温和不少转化率反而更好代价是技术复杂度高一点需要给正文容器加高度截断和渐隐效果。3.2 放行凭证与状态管理用户被提示“请关闭广告拦截器”之后通常的操作是去扩展栏把当前网站加入白名单然后刷新。网站怎么知道用户已经放行最可靠的办法就是刷新后重新检测检测通过就正常加载广告。但用户不一定愿意刷新很多站点会提供一个“我已经停用重新加载”的按钮触发location.reload()。还有一种做法是放行凭证用户点了某个按钮、或者主动点击“继续阅读”之后前端写一个localStorage标记服务端存一个会话状态下次访问不再弹遮罩。这里我吃过亏只写localStorage标记有个明显问题很多用户是直接删除浏览器数据、或使用无痕模式标记丢失又会被反复打扰。反过来如果完全不写标记每次刷新都弹用户反感度会直线上升。比较好的策略是“检测结果 会话标记”双写以服务端会话为主前端标记只是加速判断。3.3 广告替换、白名单引导与合规边界不硬碰硬也不意味着放弃收入。一些站点检测到广告被拦截后会把原本广告位替换成其他内容自家的促销活动、订阅引导、公众号二维码、内容推荐模块。这些内容不会被广告过滤器拦因为拦截器只针对第三方广告联盟特征。技术实现上广告容器初始化时留一个 fallback 区域检测到拦截后再渲染替代模块。从产品价值观上讲我不建议在检测到拦截后偷偷运行隐藏挖矿脚本也不建议欺骗性诱导用户点击“关闭拦截器”却把页面导去广告页。这类做法一旦被用户识破损失的是长期信任甚至会被安全软件标记有点得不偿失。合规上也要注意弹窗的克制性、隐私声明的透明度特别是涉及读取页面状态、用户交互数据时最好提前做用户告知。另外现在内容平台普遍流行“Acceptable Ads”标准广告拦截器会放行一些符合规范的、克制且不干扰的广告。如果你的广告位设计合理可以考虑主动申请这类白名单从根源减少被拦截的概率这比纯粹的攻防对抗更健康。4. 攻防升级为什么反广告拦截器不能一劳永逸很多第一次接触反广告拦截器的朋友会问我部署一次检测库是不是以后都能拦截了答案是否定的。广告拦截器和反广告拦截器之间是一场持续的双向军备竞赛任何一方固定下来另一方很快就能找到破解点。4.1 检测脚本的脆弱点被加入过滤列表就失效反广告拦截器本身也是“某段 JS 代码”它也要加载、执行。过滤器维护者完全可以把你这个 lib 的域名、路径、甚至 JS 里的关键变量名收录进过滤清单直接从源头入手让你的检测逻辑根本不加载。比如某知名检测库的脚本地址曾经在 GitHub 上公开且被大量站点直接引用过滤清单里只要加一行规则几乎所有引用该 CDN 的站点都会失效。这也是为什么成熟的检测库会把核心算法拆成多个模块、随机分配文件名而不是乖乖待在固定的静态路径上。4.2 随机化与混淆特征对抗的两条路线为了逃避被过滤反广告拦截器这边的技术路线有两类一是资源与命名随机化二是逻辑混淆。资源与命名随机化说起来很直白Bait 元素不固定用ad-container这个 class而是每次访问从一组特征里随机挑Bait 请求的 URL 不写死域名而是通过一个随机子域参数拼接最大化匹配过滤规则又增加维护成本。逻辑混淆则是把检测代码做混淆压缩变量名缩短、函数调用扁平化让过滤器无法用特征字符串匹配。广告拦截器这边也在跟着升级。现在很多过滤器已经不只看函数名和变量名而是会用“行为特征”比如动态创建隐藏图片、短时间内发起对多处广告域名的请求即使域名是随机生成的这种动作也被看作可疑行为。两边都在拿“模式识别”做文章谁先预测到对方的模式谁就占据上风。4.3 体验、隐私与搜索流量的平衡技术对抗是有成本的。检测脚本越复杂页面加载损耗越高弹窗越强推用户跳出率越高遮罩越像插页广告搜索引擎对页面体验的评估越差可能直接影响自然搜索流量。我个人的判断是反广告拦截器最适合的应用场景是那些“广告收入是唯一收入来源且用户粘性很高”的内容站。对于一个刚起步的站点与其花大力气对抗拦截器不如先把内容质量和用户信任做起来再考虑广告变现。技术方案永远要服务于产品阶段不能陷入“为了对抗而对抗”的状态。5. 实操记录从零实现一个最小反广告拦截 Demo说了这么多原理最好还是动手写点东西。这个章节我记录一个自己折腾过的最小 Demo麻雀虽小五脏俱全读完之后你可以对照着改代码完整体验完整的检测链路。5.1 Demo 目标与运行环境我当时的 Demo 目标很简单访问页面时检测是否加载了广告拦截器如果是弹出遮罩提示放行如果不是页面正常展示。运行环境直接用一个普通 HTML 页面加原生 JavaScript不依赖任何框架浏览器用 Chrome。为了把问题说透我故意没有用任何现成库检测逻辑全部手写这样每一步都有机会验证。代码的核心分两段Bait 网络检测、DOM 样式检测两个结果只要有一个命中就判定为拦截器存在。5.2 核心代码与运行流程完整代码我整理成下面这样可以直接在本地服务器里跑起来看效果!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleAnti-Adblock Demo/title style #modal-tip { display: none; position: fixed; inset: 0; background: rgba(240, 240, 240, 0.96); z-index: 9999; text-align: center; padding-top: 18vh; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; } /style /head body div idmodal-tip h2看起来你正在使用广告拦截器/h2 p请将本站加入白名单后刷新页面以继续阅读完整内容。/p button onclicklocation.reload()我已停用重新加载/button /div main h1这是一篇正常的内容/h1 p如果你能看到全文说明页面没有被广告拦截器遮罩。/p /main script (function () { function testBait(timeout) { return new Promise(function (resolve) { var img new Image(); var timer setTimeout(function () { resolve(false); }, timeout || 800); img.onload function () { clearTimeout(timer); resolve(false); }; img.onerror function () { clearTimeout(timer); resolve(true); }; // 常见被过滤清单收录的广告脚本加随机参数防止缓存 img.src https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?r Math.random(); }); } function testDom() { var candidates [ { tag: div, cls: banner-ad }, { tag: div, cls: ad-container }, { tag: div, id: ad-banner } ]; var detected false; for (var i 0; i candidates.length; i) { var el document.createElement(candidates[i].tag); if (candidates[i].cls) el.className candidates[i].cls; if (candidates[i].id) el.id candidates[i].id; el.style.cssText position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;; document.body.appendChild(el); var cs getComputedStyle(el); if (cs.display none || cs.visibility hidden || cs.opacity 0) { detected true; } document.body.removeChild(el); } return detected; } function showModal() { document.getElementById(modal-tip).style.display block; } testBait(800).then(function (baitResult) { var domResult testDom(); if (baitResult || domResult) { showModal(); } else { console.log(No adblocker detected); } }); })(); /script /body /html运行流程很清楚页面加载完脚本立刻发起 Bait 请求和 DOM 特征检测两个 Promise 结果汇总后只要有一个判定命中就显示遮罩否则保持正常内容可见。testDom()里的候选元素覆盖了常见的banner-ad、ad-container和ad-banner这几个特征在过滤清单里出现频率很高。5.3 实测结果与踩坑细节这个 Demo 我在三种环境里实测过干净 Chrome、装 uBlock Origin 的 Chrome、装 AdGuard 的 Chrome。结果基本准确但有一个现象很有意思uBlock 的默认模式会同时拦截请求和做元素隐藏Bait 检测一秒内就触发而 AdGuard 如果只启用元素隐藏规则Bait 请求不会被拦反而是 DOM 检测在起作用。这正好说明组合检测的必要性。踩过的坑主要有三个。第一Bait 请求的 URL 如果指向一个有缓存策略的资源二次访问可能直接命中浏览器缓存onerror不触发检测失效。所以一定要加随机参数让每次检测都发出新的请求。第二testDom()里创建的候选元素如果使用#ad-banner这种 id在同一页面运行多次而没有清理干净下一个getElementById会拿到旧元素。所以我每创建完一个元素就立刻removeChild保证检测过程本身不影响页面。第三本地静态文件环境下跑 Demofile://协议下某些扩展规则不生效导致明明开着拦截器也没检测出来。需要起一个本地 HTTP 服务比如python3 -m http.server 8080用 localhost 访问才符合真实场景。6. 常见问题与排查技巧实录把这个 Demo 放到真实站点之前最好先看看别人在这些问题上踩过什么坑。我见过不少团队照着开源库一贴就上线结果被用户反馈淹没。这里整理几个高频问题算是经验速查。6.1 误报排查用户明明没开拦截器最让反广告拦截器头疼的就是误报。用户没开任何广告拦截器页面却提示“请关闭广告拦截器”这种体验几乎是一次性的用户可能从此不再回来。常见原因主要有公司网络或路由器级广告过滤比如 AdGuard Home、Pi-hole它们在网络层直接黑掉广告域名浏览器里完全看不出插件痕迹但 Bait 请求同样会失败。浏览器安全扩展太激进比如某些反指纹、隐私加固插件把所有第三方脚本都拦截你的 Bait 请求也被“无差别攻击”。广告域名本身挂了或地区网络抽风onerror被正常网络错误触发。排查方法很简单先在干净无痕窗口打开页面看提示是否消失再关闭“隐私保护插件”对比测试。生产环境建议把 Bait 域名改成多个候选、配合超时和 DOM 检测综合判断而且要设计“连续 N 次检测异常才提示”的防抖逻辑减少偶发网络波动导致的误报。6.2 部署后日活与广告收入变化怎么看有朋友部署反广告拦截器后广告收入没涨日活反而掉了问我是不是检测错了。这个问题很多时候不是技术问题而是策略问题。遮罩弹窗本身就会让一部分用户直接离开尤其是那些“开了拦截器但愿意看广告”的中间地带用户。上线前一定要先设好指标页面可见率、广告展示率、跳出率、回访率分开看。如果广告展示率上去了但回访率明显下滑说明你的提示策略太激进。可以先做灰度发布只对广告位暴露时间长、页面浏览深度高的人群开启观察数据再说。技术上把检测结果埋点上报比单纯弹窗更有长期价值。6.3 移动端、嵌入式与 DNS 过滤场景的盲区移动端浏览器上扩展类广告拦截器的安装率没有桌面端高但系统级广告过滤、内置浏览器拦截反而更普遍。这些场景里页面内 JS 不一定能感知到拦截发生因为 Bait 请求可能在系统网络层就被掐断了onerror却又不像扩展拦截那么稳定。即使前端检测判定为“没有拦截器”实际广告素材可能依然无法展示。所以移动端更适合看“广告容器是否真的有内容渲染出来”而不是只看拦截检测。对于重度使用 DNS 过滤的用户反广告拦截器基本无解只能靠后端统计展示率间接判断前端不要强求。从我实测的经验来看反广告拦截器永远只能作为站点收入体系里的一环而不是救命稻草。把检测结果和用户分层结合起来优先做“提示、引导、替换”这类温和策略技术才会真正为产品加分。真要长期做就得保持每几个月更新一次检测特征的意识和过滤器清单“赛跑”的节奏体力消耗不小得做好心理准备。
返回列表