chrome特征检测(window.chrome)



This content originally appeared on DEV Community and was authored by drake

window.chrome = {
    runtime: {},
    loadTimes: function() {},
    csi: function() {},
    app: {}
};

window.chrome 对象概述

在真实的 Chrome 浏览器中,window.chrome 是一个全局对象,包含了 Chrome 浏览器特有的 API 和功能。自动化工具(如 Playwright)启动的浏览器可能缺少这个对象,这会被反爬虫系统识别。

各属性详解

1. runtime: {}

runtime: {}

真实作用:

  • 在真实 Chrome 中,chrome.runtime 提供扩展程序运行时的 API
  • 包含消息传递、存储管理、生命周期管理等功能

真实示例:

// 真实 Chrome 中的 runtime 对象包含:
chrome.runtime = {
    onMessage: {...},
    sendMessage: function() {...},
    getManifest: function() {...},
    getURL: function() {...},
    id: "extension-id"
}

这里为什么是空对象:

  • 简单伪装,让检测代码认为存在 chrome.runtime
  • 避免实现复杂的 API,因为大多数检测只是检查对象是否存在

2. loadTimes: function() {}

loadTimes: function() {}

真实作用:

  • chrome.loadTimes() 是 Chrome 的性能监控 API
  • 返回页面加载的详细时间信息

真实返回数据:

chrome.loadTimes() // 真实返回:
{
    requestTime: 1642680000.123,
    startLoadTime: 1642680000.125,
    commitLoadTime: 1642680000.150,
    finishDocumentLoadTime: 1642680000.200,
    finishLoadTime: 1642680000.250,
    firstPaintTime: 1642680000.180,
    firstPaintAfterLoadTime: 0,
    navigationType: "navigate"
}

为什么是空函数:

  • 防止调用时报错(如果不存在会抛出异常)
  • 一些检测脚本只检查函数是否存在,不关心返回值

3. csi: function() {}

csi: function() {}

真实作用:

  • CSI = Chrome Speed Index
  • 提供客户端性能指标和统计信息
  • 用于 Chrome 内部的性能分析

真实返回数据:

chrome.csi() // 真实返回:
{
    pageT: 1234.56,        // 页面加载时间
    startE: 1642680000123, // 开始时间戳
    onloadT: 1234.56,      // onload 事件时间
    tran: 15               // 传输次数
}

4. app: {}

app: {}

真实作用:

  • 在 Chrome 应用环境中提供应用相关的 API
  • 主要用于 Chrome 打包应用(Chrome Apps,现已弃用)

真实内容:

// 在 Chrome 应用中包含:
chrome.app = {
    runtime: {
        onLaunched: {...},
        onRestarted: {...}
    },
    window: {
        create: function() {...}
    }
}

检测对抗原理

反爬虫检测可能的代码:

// 检测是否为真实 Chrome
if (typeof window.chrome === 'undefined') {
    console.log('可能是自动化浏览器');
}

if (typeof chrome.loadTimes !== 'function') {
    console.log('不是真实 Chrome');
}

// 更严格的检测
try {
    const loadTimes = chrome.loadTimes();
    if (!loadTimes || typeof loadTimes.requestTime === 'undefined') {
        console.log('chrome.loadTimes 返回异常');
    }
} catch (e) {
    console.log('chrome.loadTimes 调用失败');
}

我们的对策:

window.chrome = {
    runtime: {},                    // 通过存在性检测
    loadTimes: function() {},       // 通过函数类型检测
    csi: function() {},            // 通过函数类型检测  
    app: {}                        // 通过存在性检测
};

更完善的实现

如果要更真实地伪装,可以这样写:

window.chrome = {
    runtime: {
        onConnect: {},
        onMessage: {},
        id: undefined
    },
    loadTimes: function() {
        return {
            requestTime: Date.now() / 1000 - Math.random(),
            startLoadTime: Date.now() / 1000 - Math.random(),
            commitLoadTime: Date.now() / 1000 - Math.random(),
            finishDocumentLoadTime: Date.now() / 1000 - Math.random(),
            finishLoadTime: Date.now() / 1000,
            firstPaintTime: Date.now() / 1000 - Math.random(),
            firstPaintAfterLoadTime: 0,
            navigationType: "navigate"
        };
    },
    csi: function() {
        return {
            pageT: Math.random() * 1000,
            startE: Date.now() - Math.random() * 1000,
            onloadT: Math.random() * 1000,
            tran: Math.floor(Math.random() * 20)
        };
    },
    app: {
        isInstalled: false
    }
};

不过对于大多数反检测场景,简单的空对象和空函数就足够了。

完整使用样例

async def run_local(self, proxy=None):
    async with async_playwright() as p:
        # 更完善的启动参数
        launch_data = {
            "headless": False,
            "proxy": proxy,
            "args": [
                '--disable-blink-features=AutomationControlled',
                '--disable-dev-shm-usage',
                '--no-first-run',
                '--no-default-browser-check',
                '--disable-infobars',
                '--disable-extensions',
                '--disable-features=VizDisplayCompositor'
            ]
        }

        browser = await p.chromium.launch(**launch_data)

        # 创建上下文时添加更多参数
        context = await browser.new_context(
            user_agent=user_agent,
            viewport={'width': 1920, 'height': 1080},
            screen={'width': 1920, 'height': 1080},
            device_scale_factor=1,
            is_mobile=False,
            has_touch=False,
            locale='en-US',
            timezone_id='America/New_York',
            permissions=['notifications']
        )

        timeout = 30
        context.set_default_timeout(timeout * 1000)
        page = await context.new_page()

        await page.add_init_script("""
            // 模拟真实的 chrome 对象
            window.chrome = {
                runtime: {},
                loadTimes: function() {},
                csi: function() {},
                app: {}
            };
        """)

        # 监听请求流
        page.on('response', self.on_response)

        url = 'https://gmgn.ai/meme/LXtw30LM?chain=sol'

        # 先访问主页,模拟真实用户行为
        await page.goto('https://gmgn.ai/')
        await asyncio.sleep(2)

        # 再访问目标地址
        await page.goto(url)
        await asyncio.sleep(3)

        # 过反爬,如果不加就是被block的状态
        await page.reload()
        await asyncio.sleep(10)
        await self.detect(page)

        # 1小时后关闭浏览器
        await page.evaluate("setTimeout(() => window.x = 5, 60 * 60 * 1000)")
        await page.wait_for_function("() => window.x > 0", timeout=0)


This content originally appeared on DEV Community and was authored by drake