背景:iframe 限制

Google 广告内容是通过 iframe 展示和交互,利用 iframe 特性实现限制外部对广告内容的获取和交互。

iframe contentWindow 返回的 Window 受到浏览器同源策略规则约束,进一步获取 contentWindow.document 触发跨域安全错误,“Blocked a frame with origin "https://xx.com" from accessing a cross-origin frame.”,导致不能再进一步获取 contentWindow.document 子元素的广告内容。

// 例如 iframe 内嵌广告页来自 https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-8126932081996221&output=html&h=90&slotname=2801015661&adk=4006315846&adf=1431886828&pi=t.ma~as.2801015661&w=391&abgtt=6&lmt=1735795401&rafmt=12&format=391x90&url=https%3A%2F%2Fwww.trytheapps.com%2Fpages%2Fresearch%2Findex.html%23%2Fb&wgl=1&uach=WyJBbmRyb2lkIiwiNi4wIiwiIiwiTmV4dXMgNSIsIjEzMS4wLjY3NzguMTQwIixudWxsLDEsbnVsbCwiNjQiLFtbIkdvb2dsZSBDaHJvbWUiLCIxMzEuMC42Nzc4LjE0MCJdLFsiQ2hyb21pdW0iLCIxMzEuMC42Nzc4LjE0MCJdLFsiTm90X0EgQnJhbmQiLCIyNC4wLjAuMCJdXSwwXQ..&dt=1735801468091&bpp=31&bdt=2256&idt=-M&shv=r20241212&mjsv=m202412090101&ptt=9&saldr=aa&abxe=1&cookie=ID%3D6eab13b4702cd034%3AT%3D1735798711%3ART%3D1735799215%3AS%3DALNI_MahObO0v1dvzdoveX-SAgWmt6MJOA&gpic=UID%3D00000fd0d6a44a06%3AT%3D1735798711%3ART%3D1735799215%3AS%3DALNI_MYQf4efES5JJzqJO8QSpXT1-S9CDA&eo_id_str=ID%3D8bfdd561462d51be%3AT%3D1735798711%3ART%3D1735799215%3AS%3DAA-AfjYOEBdpbBUsf4-IKn7bZout&prev_fmts=0x0&nras=1&correlator=8789770548456&frm=20&pv=1&u_tz=480&u_his=10&u_h=602&u_w=411&u_ah=602&u_aw=411&u_cd=30&u_sd=2&dmc=8&adx=10&ady=165&biw=411&bih=602&scr_x=0&scr_y=0&eid=31089329%2C31089340%2C95332590%2C95332923%2C95345966%2C95347432&oid=2&pvsid=188917818573070&tmod=1441886544&uas=0&nvt=2&fc=1920&brdim=0%2C0%2C0%2C0%2C411%2C0%2C411%2C602%2C411%2C602&vis=1&rsz=%7C%7CeE%7C&abl=CS&pfx=0&fu=256&bc=31&bz=1&td=1&tdf=2&psd=W251bGwsbnVsbCxudWxsLDNd&nt=1&ifi=2&uci=a!2&fsb=1&dtd=14
// 此时 contentWindow 返回的受限对象信息
{
    0: global {window: global, self: global, location: Location, closed: false, frames: global, …},
    1: global {window: global, self: global, location: Location, closed: false, frames: global, …},
    2: global {window: global, self: global, location: Location, closed: false, frames: global, …},
    blur: ƒ blur(),
    close: ƒ close(),
    closed: false,
    focus: ƒ focus(),
    frames: global {0: global, 1: global, 2: global, window: global, self: global, …},
    length: 3,
    location: Location {Symbol(Symbol.toStringTag): undefined, …},
    opener: null,
    parent: Window {0: global, 1: global, 2: global, 3: global, 4: global, 5: Window, …},
    postMessage: ƒ postMessage(),
    self: global {0: global, 1: global, 2: global, window: global, …},
    then: [Exception: SecurityError: Blocked a frame with origin  …,
    top: Window {0: global, 1: global, 2: global, 3: global, …},
    window: global {0: global, 1: global, 2: global, window: global, …}
    Symbol(Symbol.hasInstance): undefined,
    Symbol(Symbol.isConcatSpreadable): undefined,
    Symbol(Symbol.toStringTag): undefined
}

一、广告填充

Google 通过 iframe postMessage 通信实现是否填充逻辑。

iframe 广告页

通信代码和数据由服务端直接渲染,在广告页 DOM 加载完时即触发消息发送,目标窗口无限制。

// 成功填充需满足2个条件:googMsgType = “adpnt”;返回的 iframe 源和注册监听时的 iframe 源是同一个
window.top.postMessage(
    '{"key_value":[{"key":"qid","value":"***"}],"googMsgType":"adpnt"}', 
    '*'
);

// 其他消息还有2个
window.parent.postMessage(
    '{"googMsgType":"pvt""token":"***"}', 
    '*'
);

window.top.postMessage(
    '{
        "msg_type":"adsense-labs",
        "key_value":[{"key":"settings","value":"[\\\"ca-pub-8126932081996221\\\",[[1]],null,[[\\\"ID=***:T=1735878120:RT=1735885265:S=***\\\",1769574120,\\\"/\\\",\\\"trytheapps.com\\\",1],[\\\"UID=***:T=1735878120:RT=1735885265:S=***\\\",1769574120,\\\"/\\\",\\\"trytheapps.com\\\",2]],[\\\"ID=***:T=1735878120:RT=1735885265:S=***\\\",1751430120,\\\"/\\\",\\\"trytheapps.com\\\"]]"}],
        "googMsgType":"sth"
    }', 
    '*'
);

父级页

// 源码 https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8126932081996221
// 调用 google_sa_impl 

// 源码 https://pagead2.googlesyndication.com/pagead/managed/js/adsense/m202412090101/show_ads_impl_fy2021.js
// 初始创建全局对象 google_sa_impl, 值为 $X 函数 
// => aY 函数 
// => eY 函数 
// => lY 函数 
// => zY 函数 
// => tY 函数 
// => oY 函数 
// => $R 函数,注册 message 监听 iframe 广告页广播的消息,同时监听 iframe 广告页的 load 情况,
// 广告页 load 完成后,延迟 1s 检查广告位填充情况,如果此时广告位的广告状态不是 filled,则设置为 unfilled。

目前是通过监听 iframe 广告页父级 ins 容器上的 data-ad-status 属性是否 filled 来判断是否填充成功。监听 Google iframe postMessage 通信方式已在调研页测试过(通过 postMsg 自定义事件上报),测试正常。


二、广告点击

限制点击事件监听 -- 浏览器

点击事件同样受浏览器同源策略规则约束,导致不能直接监听 Google iframe 里广告素材点击。

点击事件校验 -- 浏览器 + Goolge

每个 iframe 广告页会加载一套通用的点击事件保护代码(目前版本是 https://tpc.googlesyndication.com/pagead/js/r20241212/r20110914/client/qs_click_protection_fy2021.js)

事件派发方式无效原因:保护代码会在捕获阶段对广告素材各层级注入点击事件监听,通过点击事件对象的 isTrusted 属性(浏览器限制,浏览器还会限制派发事件伪装点击坐标等信息),识别点击是来自事件派发还是来自用户点击行为,如果是事件派发则通过阻止点击事件传播,使各层级均屏蔽掉此次点击响应,并通过 navigator.sendBeacon 上报 Google 后台。

点击交互后续还有对事件对象的触发时间、缩放、坐标偏移等校验,透明度似乎没有做校验。

// 源码 https://tpc.googlesyndication.com/pagead/js/r20241212/r20110914/client/qs_click_protection_fy2021.js
P(R, "click", e => {
    if (this.I) {
        var g = {
            cx: e.clientX,
            cy: e.clientY,
            et: Date.now(),
            qid: this.H
        };
        var h = dc;
        var k = "J";
        h.J && h.hasOwnProperty(k) || (k = new h,
        h.J = k);
        h = [];
        !g.eid && h.length && (g.eid = h.toString());
        gc(g);
        this.I = !1
    }
    if (e.isTrusted === !1 && K(this.data, 15))
        e.preventDefault ? e.preventDefault() : e.returnValue = !1,
        e.stopImmediatePropagation(),
        Nb();
    else {
        g = -1;
        h = [];
        for (var l of this.s) {
            k = l.B;
            var m = k !== -1;
            if (!(L(l.j, 3) <= g || l.u || m && h[k] === !1)) {
                var w = !m || h[k] || this.i[k].contains(e.target);
                m && w && (h[k] = !0);
                if (k = w)
                    if (k = e,
                    m = l.j,
                    L(m, 2) > 0 && N(m, 5) > 0)
                        k = this.A[N(m, 5)],
                        k = k !== void 0 && Date.now() < k + L(m, 2);
                    else if (M(m)) {
                        {
                            const v = (l.B >= 0 ? this.i[l.B] : R.body).getBoundingClientRect()
                                , F = Number(oc(R.body, "zoom") || "1")
                                , [Ac,Bc] = [k.clientX, k.clientY]
                                , [ja,ka,Fa,Ga] = [Ac / F - v.left, Bc / F - v.top, v.width, v.height];
                            if (!(Fa > 0 && Ga > 0) || isNaN(ja) || isNaN(ka) || ja < 0 || ka < 0)
                                k = !1;
                            else {
                                m = pc(M(l.j));
                                w = !(ja >= m.left && Fa - ja > m.right && ka >= m.top && Ga - ka > m.bottom);
                                var I = K(l.j, 12);
                                if (this.g && (K(this.data, 12) || I) && k.timeStamp - this.g.timeStamp < 300) {
                                    k = this.g.changedTouches[0];
                                    const [la,ma] = [k.clientX / F - v.left, k.clientY / F - v.top];
                                    !isNaN(la) && !isNaN(ma) && la >= 0 && ma >= 0 && (w = (w = K(this.data, 16) || I ? w : !1) || !(la >= m.left && Fa - la > m.right && ma >= m.top && Ga - ma > m.bottom))
                                }
                                k = w
                            }
                        }
                    } else
                        k = L(m, 11) > 0 ? uc(this, k, l) : !0;
                if (k) {
                    var t = l;
                    g = L(l.j, 3)
                }
            }
        }
        if (t)
            switch (l = t.j,
            N(l, 4)) {
            case 2:
            case 3:
                e.preventDefault ? e.preventDefault() : e.returnValue = !1;
                g = Date.now();
                g - t.F > 500 && (t.F = g,
                ++t.D);
                g = t.j;
                if (L(g, 8) && t.D >= L(g, 8))
                    if (t.u = !0,
                    this.h && L(g, 2) > 0)
                        tc(this);
                    else if (this.l.length > 0 && M(g))
                        for (var E of this.l)
                            Y(E, !1);
                Nb();
                E = ib(l);
                for (const v of this.C)
                    v(e, E)
            }
    }
}

目前获取广告区点击情况是通过监听 iframe blur 事件来间接实现的。