
本文已同步发布到微信公众号「人言兑」
👈 扫描二维码关注,第一时间获取更新!
我开源了一个谷歌广告拦截检测识别工具,代码仓库放在文章末尾,需要的自取。
独立开发者的小网站靠 AdSense 吃饭,本就没什么盈利,再加上浏览器的广告拦截插件,让原本就举步维艰的生活变得雪上加霜。
网站广告被拦了等于白干,写这篇是因为最近在给网站加广告屏蔽检测,踩了一堆坑,记录一下。
本文主要介绍广告拦截的技术原理,以及怎么检测我们网站上的AdSense广告是否被屏蔽了。
开始之前,请使用广告拦截插件的读者先读一下站长给用户的一封信: https://axiaoxin.com/letter/
广告拦截工具大致分三类,原理不太一样。
uBlock Origin、AdGuard、AdBlock Plus 这些,本质上是浏览器插件,靠过滤规则列表干活。
规则长这样:
||googleads.g.doubleclick.net^
||pagead2.googlesyndication.com^
##.adsbygoogle
##.ad-banner
| 开头的是网络请求拦截,匹配 URL 就直接阻断。
## 开头的是元素隐藏,页面渲染后把匹配的元素 display: none 掉。
Chrome 扩展用的是 webRequest API,可以在请求发出去之前掐掉它,或者重定向到本地替身脚本。Firefox 以前也支持 webRequest 的阻塞模式,后来搞 Manifest V3 限制了一波,但 uBlock Origin 这些还是能找到办法。
我使用的广告拦截插件 AdGuard 就是这么干的——它把 adsbygoogle.js 的请求被重定向到 chrome-extension://.../redirects/googlesyndication-adsbygoogle.js,一个空函数替身。
Pi-hole、NextDNS、AdGuard Home 这类,在路由器或者系统层面把广告域名解析到 0.0.0.0 或者 127.0.0.1。
你的电脑问 DNS:pagead2.googlesyndication.com 在哪?DNS层的广告拦截插件回:「127.0.0.1」。然后浏览器连本机,啥也拿不到,加载广告的请求就挂了。
这种方式看不到浏览器里的任何动静,扩展检测不到它,因为它根本不在浏览器里。
Brave 浏览器的 Shields、Safari 的「隐藏 IP 地址」、Firefox 的 Enhanced Tracking Protection,都是自带的。
Brave 比较特殊,它基于 Chromium,但内置了 Brave Shields,默认拦截第三方广告和追踪器。用户可能不知道自己开了广告拦截,因为这不是装的插件,是浏览器自带的。
知道了广告拦截怎么工作,检测就有方向了。总结了几种检测AdSense广告被拦截的核心思路。
在页面里塞一个看起来像广告的元素,然后检查它还在不在。
<!-- 诱饵 -->
<div class="adsbox" id="bait-adsbox">ad</div>
<script>
(function () {
var bait = document.getElementById("bait-adsbox");
if (!bait) {
console.log("元素被移除了,大概率有拦截器");
return;
}
var style = window.getComputedStyle(bait);
if (style.display === "none" || style.visibility === "hidden") {
console.log("元素被隐藏了");
}
})();
</script>
坑:诱饵放 position: absolute; left: -9999px 的话,offsetHeight 本来就是 0,会误判。得放在正常文档流里,或者用其他方式隐藏。
这是我从 Admiral 的代码里学来的,最靠谱的一招。
广告拦截会阻断 adsbygoogle.js 的加载。现代浏览器有 Performance API,可以通过 performance.getEntriesByType('resource') 能拿到所有加载的资源信息,包括 transferSize——实际传输的字节数。
正常从 Google CDN 加载 adsbygoogle.js,transferSize 好几万字节。如果被 AdGuard 重定向到本地扩展脚本,transferSize 就是 0,因为没走网络。
function checkResourceBlocked() {
var entries = performance.getEntriesByType("resource");
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
// 匹配广告相关资源
if (entry.name.indexOf("adsbygoogle") === -1) continue;
// transferSize === 0 意味着没走网络,可能被重定向到本地
if (entry.transferSize === 0 && entry.deliveryType === "") {
return {
blocked: true,
reason: "zero-transfer-size",
url: entry.name,
};
}
// 被重定向到 chrome-extension://
if (entry.name.indexOf("chrome-extension://") !== -1) {
return {
blocked: true,
reason: "redirected-to-extension",
url: entry.name,
};
}
}
return { blocked: false };
}
transferSize 是核心检测手段。正常从 Google CDN 加载的 adsbygoogle.js 有几十 KB,transferSize 不可能为 0。但 AdGuard 把它重定向到本地的替身脚本时,这个值就是 0。
坑点:deliveryType 得一起判断。如果是 “cache”,表示从缓存加载,transferSize 也是 0,但不是被拦截。
adsbygoogle.js 正常加载后会创建 window.adsbygoogle。如果用户装了拦截器,这个对象可能不存在,或者 push 方法被替换成空函数。
// 正常情况
typeof window.adsbygoogle; // "object"
typeof window.adsbygoogle.push; // "function"
// 被拦截后
typeof window.adsbygoogle; // "undefined"
但这里有个坑:adsbygoogle.js 是 async 加载的,检测脚本执行时它可能还没加载完,会误判。得等 window.load 事件后再查。
而且 AdGuard 比较鸡贼,它的替身脚本也会创建 window.adsbygoogle,但只是个空壳。可以进一步检测 push 方法的行为:
// 记录 push 前的网络请求数
var before = performance.getEntriesByType("resource").length;
// 调用 push
window.adsbygoogle.push({});
// 等一会儿,看有没有新请求
setTimeout(function () {
var after = performance.getEntriesByType("resource").length;
if (after === before) {
console.log("push 后没有新请求,可能是替身脚本");
}
}, 500);
DNS 拦截不修改浏览器里的任何东西,但有个特征:广告域名请求会极快失败。
检测方式就是探测广告域名可达性,发一个请求到广告域名,看能不能连上。
function checkDNSBlocking() {
var img = new Image();
var start = Date.now();
img.onerror = function () {
var duration = Date.now() - start;
// 正常网络错误(比如 404)通常要几百毫秒
// DNS 拦截返回 NXDOMAIN 或 127.0.0.1,通常 < 50ms
if (duration < 50) {
console.log("可能是 DNS 层拦截");
}
};
img.src = "https://pagead2.googlesyndication.com/favicon.ico?_=" + Date.now();
}
坑点:网络慢也可能导致失败,得结合时间判断。50ms 内失败基本确定是本地拦截。
Brave 会暴露 navigator.brave:
if (navigator.brave && typeof navigator.brave.isBrave === "function") {
console.log("Brave 浏览器");
}
但用户可能只是用 Brave 但没开 Shields,所以这只是识别浏览器,不能直接判定拦截。
单一检测都容易误报,得组合起来打分。
| 检测项 | 权重 |
|---|---|
| 资源重定向 (transferSize=0) | 40 |
| 诱饵元素被隐藏 | 25 |
| adsbygoogle 对象异常 | 20 |
| DNS 探测快速失败 | 15 |
| Safari 内容拦截器 | 15 |
| 企业防火墙特征 | 10 |
总分超过 50 就判定为「有拦截」。还可以加组合加成,比如「资源重定向 + 元素隐藏」额外加 15 分。
检测到广告拦截插件,强制弹窗提示用户关闭广告屏蔽插件,让页面无法在被操作。
用户 F12 打开控制台,document.getElementById('overlay').remove() 就绕过了。得加点防护:
// MutationObserver 监控遮罩被移除
var observer = new MutationObserver(function() {
if (!document.getElementById('adblock-overlay')) {
// 重建遮罩
showOverlay();
}
});
observer.observe(document.body, { childList: true, subtree: true });
// 同时隐藏页面内容
document.body.classList.add('adblock-locked');
CSS 配合:
body.adblock-locked > *:not(#adblock-overlay) {
display: none !important;
}
把上面这些拼起来,写个简单的检测库。
// adblock-shield.js
(function () {
"use strict";
var detected = false;
var checkCount = 0;
var MAX_CHECKS = 3;
// 显示遮挡层
function showOverlay() {
if (detected) return;
detected = true;
// 创建遮罩
var div = document.createElement("div");
div.id = "adblock-overlay";
div.innerHTML =
'<div style="position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;">' +
'<div style="background:#fff;padding:40px;border-radius:12px;text-align:center;max-width:400px;">' +
"<h2>检测到广告拦截器</h2>" +
"<p>请关闭广告拦截器后刷新页面</p>" +
'<button onclick="location.reload()">刷新</button>' +
"</div></div>";
document.body.appendChild(div);
// 隐藏页面内容
document.body.style.overflow = "hidden";
var main =
document.getElementById("main") || document.querySelector("main");
if (main) main.style.display = "none";
}
// 检测 1:资源重定向
function checkResourceRedirect() {
if (!performance.getEntriesByType) return { blocked: false };
var entries = performance.getEntriesByType("resource");
for (var i = 0; i < entries.length; i++) {
var e = entries[i];
if (e.name.indexOf("adsbygoogle") === -1) continue;
if (e.transferSize === 0 && e.deliveryType === "") {
return { blocked: true, reason: "zero-transfer" };
}
if (e.name.indexOf("chrome-extension://") !== -1) {
return { blocked: true, reason: "extension-redirect" };
}
}
return { blocked: false };
}
// 检测 2:诱饵元素
function checkBaitElement() {
var el = document.createElement("div");
el.className = "adsbygoogle";
el.style.cssText =
"display:block;width:10px;height:10px;position:absolute;left:-9999px;";
document.body.appendChild(el);
var style = window.getComputedStyle(el);
var blocked =
style.display === "none" ||
style.visibility === "hidden" ||
el.offsetHeight === 0;
document.body.removeChild(el);
return { blocked: blocked };
}
// 检测 3:脚本对象
function checkScriptObject() {
var hasScript =
document.querySelector('script[src*="adsbygoogle"]') !== null;
if (!hasScript) return { blocked: false };
if (!window.adsbygoogle) {
return { blocked: true, reason: "object-missing" };
}
return { blocked: false };
}
// 检测 4:DNS 层
function checkDNS() {
return new Promise(function (resolve) {
var img = new Image();
var start = Date.now();
img.onerror = function () {
resolve({ blocked: Date.now() - start < 50 });
};
img.onload = function () {
resolve({ blocked: false });
};
img.src =
"https://pagead2.googlesyndication.com/favicon.ico?_=" + Date.now();
setTimeout(function () {
resolve({ blocked: false });
}, 2000);
});
}
// 综合检测
async function detect() {
checkCount++;
var r1 = checkResourceRedirect();
var r2 = checkBaitElement();
var r3 = checkScriptObject();
var r4 = await checkDNS();
var score = 0;
if (r1.blocked) score += 40;
if (r2.blocked) score += 25;
if (r3.blocked) score += 20;
if (r4.blocked) score += 15;
console.log("Check #" + checkCount, "Score:", score);
if (score >= 50) {
showOverlay();
return true;
}
if (checkCount < MAX_CHECKS) {
setTimeout(detect, 2000);
}
return false;
}
// 启动
if (document.readyState === "complete") {
setTimeout(detect, 500);
} else {
window.addEventListener("load", function () {
setTimeout(detect, 500);
});
}
})();
我封装了一个开源库,一共实现了7种检测方法,需要的可以直接拿去用:
源码和详细文档在 GitHub: https://github.com/axiaoxin-com/adblock-shield
觉得好用的,给个Star吧!
transferSize 的定义广告被拦截是AdSense站长绕不开的问题。理解拦截原理、掌握检测方法,才能在用户体验和收入之间找到平衡。如果你有更好的检测思路,欢迎交流。
另外,你可能感兴趣的阅读: 自动刷新 Google AdSense 广告单元的实现方案(auto-refresh-gad.js)
作者: axiaoxin ,独立开发者,靠几个小网站养活自己。