一句话总结:浏览器指纹是网站识别你的”数字身份”,CDP 可以修改这些指纹参数让你看起来像一个真实的用户浏览器,而不是自动化脚本。


目录

  1. 什么是浏览器指纹
  2. 自动化检测的常见手段
  3. CDP 修改指纹的两种方式
  4. 实战一:绕过 navigator.webdriver 检测
  5. 实战二:修改 User-Agent 与平台信息
  6. 实战三:Canvas 指纹修改
  7. 实战四:WebGL 指纹修改
  8. 实战五:AudioContext 指纹修改
  9. 实战六:修改屏幕分辨率与视口
  10. 实战七:修改时区与语言环境
  11. 实战八:完整反检测工具类
  12. 局限性分析

什么是浏览器指纹

当你访问一个网站时,浏览器会泄露大量信息:操作系统版本、浏览器类型、屏幕分辨率、安装的字体、显卡型号、时区、语言等。这些信息组合起来,足以在数十万访客中唯一识别你的设备。

常见的指纹维度:

指纹类型 来源 唯一性
Canvas 指纹 HTMLCanvasElement.toDataURL() ⭐⭐⭐⭐
WebGL 指纹 WebGLRenderingContext.getParameter() ⭐⭐⭐⭐⭐
AudioContext 指纹 AudioContext.getChannelData() ⭐⭐⭐
字体指纹 document.fonts / Flash ⭐⭐⭐
屏幕信息 screen.width / screen.height / colorDepth ⭐⭐
时区 Intl.DateTimeFormat / Date.getTimezoneOffset() ⭐⭐
语言 navigator.language / navigator.languages
硬件并发 navigator.hardwareConcurrency ⭐⭐
设备内存 navigator.deviceMemory ⭐⭐
触控支持 navigator.maxTouchPoints ⭐⭐

自动化检测的常见手段

在讨论如何绕过之前,先了解网站如何检测自动化工具:

1. WebDriver 标志

1
2
// 正常浏览器:navigator.webdriver = undefined 或 false
// 自动化浏览器:navigator.webdriver = true

这是最基本的检测。Selenium、Puppeteer 默认都会设置这个标志。

2. Chrome 特有属性

1
2
3
4
5
// 真实 Chrome 有 chrome 对象
typeof window.chrome !== 'undefined'

// 但自动化工具可能暴露过多属性
window.chrome.runtime // Puppeteer 模式下存在这个

3. 行为特征

1
2
3
4
// 鼠标是否真实移动?
// 滚动是否太"完美"?
// 点击间隔是否太规律?
// 页面加载后是否立即触发所有事件?

4. Permissions API

1
2
3
4
// 自动化浏览器通常返回提示状态
navigator.permissions.query({name: 'notifications'})
// 真实用户:prompt(默认)
// headless 浏览器:denied

5. CDP 检测

一些高级检测工具能发现 CDP 连接的存在:

1
2
3
// 检测 DevTools 是否打开
// 检测 WebSocket 调试连接
// 检测 performance API 中的异常

CDP 修改指纹的两种方式

CDP 提供了两种修改浏览器指纹的方式:

Page.addScriptToEvaluateOnNewDocument

在每个新文档执行之前注入一段脚本。这里修改的属性和方法会覆盖原生实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CMD_ID = [0]

async def cdp(ws, method, params=None):
"""发送 CDP 命令并等待返回结果"""
CMD_ID[0] += 1
cmd_id = CMD_ID[0]
request = {'id': cmd_id, 'method': method, 'params': params or {}}
await ws.send(json.dumps(request))
async for msg in ws:
response = json.loads(msg)
if response.get('id') == cmd_id:
return response.get('result', {})

await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// 这段代码在每个页面加载前执行
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
'''
})

适合:修改 JavaScript 层面的属性和方法

Emulation 域

CDP 的 Emulation 域可以修改浏览器内核层面的参数,比 JS 注入更底层。

1
2
3
await cdp(ws, 'Emulation.setUserAgentOverride', {
'userAgent': 'Mozilla/5.0 ...'
})

适合:修改 User-Agent、分辨率、时区等引擎级参数

原则:能用 Emulation 域的就用 Emulation,它的修改更深层、更难被检测。


实战一:绕过 navigator.webdriver 检测

这是最基本的绕过,几乎每个反检测方案都需要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def override_webdriver(ws):
"""注入脚本覆盖 navigator.webdriver"""
await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// 覆盖 webdriver 属性
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
configurable: true
});

// 因为一些检测会用 Object.getOwnPropertyDescriptor 来检查
// 所以需要确保看起来像是原生未定义的属性
'''
})

# 在 Page.enable 之后、Page.navigate 之前调用
override_webdriver(ws)

验证效果:

1
2
3
4
5
result = await cdp(ws, 'Runtime.evaluate', {
'expression': 'navigator.webdriver',
'returnByValue': True
})
print(result['result']['value']) # 输出:undefined

进阶:处理更深入的检测

一些网站会检查 navigator.webdriver属性描述符

1
2
3
4
// 检测是否被 Object.defineProperty 覆盖过
const desc = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
// 如果是原生 undefined,desc 应该是 undefined
// 如果被覆盖过,desc 会是一个属性描述符对象

更彻底的绕过方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def stealth_webdriver(ws):
"""深度隐藏 webdriver 痕迹"""
await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// ⚠️ 注意:window.navigator 是只读属性,无法用 Proxy 替换。
// 正确的方式是在 Navigator 原型上做手脚:

// 1. 覆盖 webdriver 属性(用 defineProperty 在 navigator 上创建自有属性来遮蔽原型链上的 getter)
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
configurable: true
});

// 2. 手动模拟 plugins 数组(自动化浏览器通常长度为 0)
if (navigator.plugins.length === 0) {
const fakePlugins = [
{name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer'},
{name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai'},
{name: 'Native Client', filename: 'internal-nacl-plugin'},
];
fakePlugins.item = i => fakePlugins[i];
fakePlugins.namedItem = n => fakePlugins.find(p => p.name === n);
Object.defineProperty(navigator, 'plugins', {
get: () => fakePlugins,
configurable: true
});
}
'''
})

实战二:修改 User-Agent 与平台信息

User-Agent 是最明显的指纹之一,CDP 的 Emulation 域提供了专门的修改方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def set_user_agent(ws, ua=None, platform=None):
"""设置自定义 User-Agent 和平台"""

if ua is None:
# Windows 11 + Chrome 125 的标准 UA
ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'

if platform is None:
platform = 'Win32'

await cdp(ws, 'Emulation.setUserAgentOverride', {
'userAgent': ua,
'platform': platform,
'acceptLanguage': 'zh-CN,zh;q=0.9,en;q=0.8'
})

# 调用
set_user_agent(ws)

这里比较容易被忽略的是 platform 参数。只改 UA 不改 platform 的话:

1
2
// navigator.platform 仍然是 'MacIntel'(如果你在用 Mac)
// 但 UA 写的是 Windows,这就露出马脚了

使用真实设备 UA

为了更真实,可以从真实设备上采集 UA。或者使用一些知名的 UA 列表:

1
2
3
Windows Chrome 125:  Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
macOS Chrome 125: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
Windows Edge 125: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0

实战三:Canvas 指纹修改

Canvas 指纹的原理:网站用 JavaScript 在画布上绘制文本和图形,然后调用 toDataURL() 获取像素数据。不同的显卡、驱动、操作系统绘制的结果有细微差异,这个差异就是指纹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def override_canvas_fingerprint(ws):
"""修改 Canvas 指纹,每次返回略有差异的结果"""
await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// 保存原始方法
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
const originalToBlob = HTMLCanvasElement.prototype.toBlob;

// 添加少量噪声来改变指纹
function addNoise(canvas) {
// 在画布角落添加一个几乎不可见的像素
const ctx = canvas.getContext('2d');
if (!ctx) return canvas;

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 修改第一个像素的蓝色通道
if (imageData.data.length > 3) {
imageData.data[0] = imageData.data[0] ^ 1; // XOR 1 来翻转最低位
ctx.putImageData(imageData, 0, 0);
}
return canvas;
}

HTMLCanvasElement.prototype.toDataURL = function(...args) {
addNoise(this);
return originalToDataURL.apply(this, args);
};

HTMLCanvasElement.prototype.toBlob = function(...args) {
addNoise(this);
return originalToBlob.apply(this, args);
};
'''
})

更精细的方案:每次启动时变化噪声量,避免返回固定的指纹值(因为固定值本身也是一种指纹)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def override_canvas_with_variation(ws, seed=None):
"""每次注入不同的噪声来产生变化"""
import random
if seed is None:
seed = random.randint(1, 255)

await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': f'''
const CANVAS_NOISE = {seed};

HTMLCanvasElement.prototype.toDataURL = (function(original) {{
return function(...args) {{
const canvas = this;
const ctx = canvas.getContext('2d');
if (ctx) {{
const imageData = ctx.getImageData(0, 0, 1, 1);
if (imageData.data.length > 0) {{
imageData.data[3] = imageData.data[3] ^ CANVAS_NOISE;
ctx.putImageData(imageData, 0, 0);
}}
}}
return original.apply(this, args);
}};
}})(HTMLCanvasElement.prototype.toDataURL);
'''
})

实战四:WebGL 指纹修改

WebGL 提供了大量信息:显卡型号、渲染器、供应商、支持的功能列表等。这是最具区分度的指纹之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def override_webgl_fingerprint(ws):
"""修改 WebGL 参数,隐藏真实显卡信息"""
await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// 修改 WebGLRenderingContext 的 getParameter 方法
const originalGetParameter = WebGLRenderingContext.prototype.getParameter;

WebGLRenderingContext.prototype.getParameter = function(param) {
const result = originalGetParameter.call(this, param);

// UNMASKED_VENDOR (0x9245) 和 UNMASKED_RENDERER (0x9246) 暴露显卡信息
if (param === 0x9245) { // UNMASKED_VENDOR_WEBGL
return 'Intel Inc.';
}
if (param === 0x9246) { // UNMASKED_RENDERER_WEBGL
return 'Intel Iris OpenGL Engine';
}

// RENDERER (0x1F01) 和 VENDOR (0x1F00)
if (param === 0x1F00) { // VENDOR
return 'WebKit';
}
if (param === 0x1F01) { // RENDERER
return 'WebKit WebGL';
}

// 修改 extensions 返回结果
if (param === 0x1F03) { // EXTENSIONS
// 删掉一些不常见的扩展
const extStr = String(result);
const blocked = ['WEBGL_debug_renderer_info', 'WEBGL_debug_shaders'];
return extStr.split(' ').filter(e => !blocked.includes(e)).join(' ');
}

return result;
};

// 还要处理 WebGL2
if (WebGL2RenderingContext) {
WebGL2RenderingContext.prototype.getParameter = WebGLRenderingContext.prototype.getParameter;
}
'''
})

获取真实 WebGL 参数

你可以先从一台真实的 Windows 机器上获取这些参数:

1
2
3
4
5
6
7
8
9
10
11
// 在真实浏览器中执行,记录返回值
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
console.log({
vendor: gl.getParameter(gl.VENDOR),
renderer: gl.getParameter(gl.RENDERER),
unmaskedVendor: gl.getExtension('WEBGL_debug_renderer_info')
?.getParameter(gl.UNMASKED_VENDOR_WEBGL),
unmaskedRenderer: gl.getExtension('WEBGL_debug_renderer_info')
?.getParameter(gl.UNMASKED_RENDERER_WEBGL)
});

然后用这些真实值来覆盖自动化浏览器的 WebGL 参数。


实战五:AudioContext 指纹修改

AudioContext 指纹通过处理音频信号,获取设备音频栈的微小差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def override_audio_fingerprint(ws):
"""修改 AudioContext 指纹"""
await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// 修改 AudioContext 的方法来改变指纹
const originalGetChannelData = AudioBuffer.prototype.getChannelData;

AudioBuffer.prototype.getChannelData = function(channel) {
const data = originalGetChannelData.call(this, channel);
// 在返回的数据中添加微小的噪声(0.0001% 级别)
for (let i = 0; i < data.length; i += 10) {
data[i] *= 1.000001; // 几乎不可感知的差异
}
return data;
};

// 覆盖 AudioContext 的创建,使其使用修改后的方法
// 或者直接覆盖 createOscillator / createAnalyser
const originalCreateOscillator = BaseAudioContext.prototype.createOscillator;
BaseAudioContext.prototype.createOscillator = function() {
console.log('[CDP] AudioContext access blocked');
return originalCreateOscillator.call(this);
};
'''
})

实战六:修改屏幕分辨率与视口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def set_viewport(ws, width=1920, height=1080, device_scale_factor=1.0):
"""设置浏览器视口和屏幕参数"""

# 使用 Emulation 域设置
await cdp(ws, 'Emulation.setDeviceMetricsOverride', {
'width': width,
'height': height,
'deviceScaleFactor': device_scale_factor,
'mobile': False,
'screenWidth': width,
'screenHeight': height,
'positionX': 0,
'positionY': 0,
'screenOrientation': {'type': 'landscapePrimary', 'angle': 0}
})

# 用法
set_viewport(ws, width=1920, height=1080)

为什么重要

  • 很多反欺诈系统会检查 screen.widthscreen.height
  • 默认的 CDP 窗口可能只有 800x600,暴露了自动化特征
  • 视口和屏幕参数不匹配也是疑点

常见分辨率配置

1
2
3
4
5
6
VIEWPORTS = {
'desktop_1080p': {'width': 1920, 'height': 1080, 'scale': 1.0},
'desktop_2k': {'width': 2560, 'height': 1440, 'scale': 1.0},
'macbook_pro': {'width': 1440, 'height': 900, 'scale': 2.0},
'thinkpad': {'width': 1366, 'height': 768, 'scale': 1.0},
}

实战七:修改时区与语言环境

有些网站通过时区和语言来判断用户是否使用了代理或自动化工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def set_timezone_and_locale(ws, timezone='Asia/Shanghai', locale='zh-CN'):
"""设置时区和语言环境"""

# 修改时区(Emulation 域)
await cdp(ws, 'Emulation.setTimezoneOverride', {
'timezoneId': timezone
})

# 修改语言(通过脚本注入)
await cdp(ws, 'Page.addScriptToEvaluateOnNewDocument', {
'source': f'''
Object.defineProperties(navigator, {{
language: {{ get: () => '{locale}' }},
languages: {{ get: () => ['{locale}', 'en'] }},
}});
'''
})

# 常见时区
# 'Asia/Shanghai' — 中国
# 'America/New_York' — 美东
# 'Europe/London' — 伦敦
# 'Asia/Tokyo' — 东京

注意Emulation.setTimezoneOverride 修改的是浏览器内核级别的时区,Date.toString()Intl.DateTimeFormat 等都会被影响,比 JS 注入更彻底。

Emulation 域没有直接的语言设置接口,所以语言需要配合 JS 注入来修改。


实战八:完整反检测工具类

把以上所有技巧整合成一个完整的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import asyncio
import json
import urllib.request
import websockets
import base64
import random

CMD_ID = [0]

async def cdp(ws, method, params=None):
"""发送 CDP 命令并等待返回结果"""
CMD_ID[0] += 1
cmd_id = CMD_ID[0]
request = {'id': cmd_id, 'method': method, 'params': params or {}}
await ws.send(json.dumps(request))
async for msg in ws:
response = json.loads(msg)
if response.get('id') == cmd_id:
return response.get('result', {})

class CDPAntiDetect:
"""CDP 反检测工具类(异步版)"""

# 常用分辨率
VIEWPORTS = {
'desktop': {'width': 1920, 'height': 1080, 'scale': 1.0},
'laptop': {'width': 1366, 'height': 768, 'scale': 1.0},
'macbook': {'width': 1440, 'height': 900, 'scale': 2.0},
}

# 常用 User-Agent
USER_AGENTS = {
'win_chrome': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
'mac_chrome': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
}

def __init__(self, host='localhost:9222'):
self.host = host
self.ws = None
self.script_ids = []

async def connect(self):
"""连接 Chrome"""
data = json.loads(urllib.request.urlopen(f'http://{self.host}/json', timeout=5).read())
ws_url = data[0]['webSocketDebuggerUrl']
self.ws = await websockets.connect(ws_url, max_size=2**24)
await self.cdp('Page.enable')
await self.cdp('Runtime.enable')
return self

async def cdp(self, method, params=None):
"""发送 CDP 命令"""
return await cdp(self.ws, method, params)

async def _inject_js(self, source):
"""注入页面脚本"""
result = await self.cdp('Page.addScriptToEvaluateOnNewDocument', {'source': source})
self.script_ids.append(result.get('identifier'))
return self

async def apply_preset(self, preset='desktop'):
"""应用完整预设"""
preset_actions = {
'desktop': {
'viewport': self.VIEWPORTS['desktop'],
'ua': self.USER_AGENTS['win_chrome'],
'platform': 'Win32',
'timezone': 'Asia/Shanghai',
'locale': 'zh-CN',
},
'mac': {
'viewport': self.VIEWPORTS['macbook'],
'ua': self.USER_AGENTS['mac_chrome'],
'platform': 'MacIntel',
'timezone': 'Asia/Shanghai',
'locale': 'zh-CN',
},
}

config = preset_actions.get(preset, preset_actions['desktop'])

# 1. 设置 User-Agent 和平台
await self.cdp('Emulation.setUserAgentOverride', {
'userAgent': config['ua'],
'platform': config['platform'],
})

# 2. 设置视口和分辨率
vp = config['viewport']
await self.cdp('Emulation.setDeviceMetricsOverride', {
'width': vp['width'], 'height': vp['height'],
'deviceScaleFactor': vp['scale'],
'mobile': False,
'screenWidth': vp['width'], 'screenHeight': vp['height'],
'positionX': 0, 'positionY': 0,
})

# 3. 设置时区
await self.cdp('Emulation.setTimezoneOverride', {'timezoneId': config['timezone']})

# 4. 注入反检测脚本
seed = random.randint(1, 255)
await self._inject_js(f'''
// 覆盖 webdriver
Object.defineProperty(navigator, 'webdriver', {{ get: () => undefined }});

// 设置语言
Object.defineProperties(navigator, {{
language: {{ get: () => '{config["locale"]}' }},
languages: {{ get: () => ['{config["locale"]}', 'en', 'en-US'] }},
}});

// Canvas 指纹噪声
const CANVAS_NOISE = {seed};
const _toDataURL = HTMLCanvasElement.prototype.toDataURL.bind(HTMLCanvasElement.prototype);
HTMLCanvasElement.prototype.toDataURL = function(...args) {{
const ctx = this.getContext('2d');
if (ctx) {{
try {{
const imageData = ctx.getImageData(0, 0, 1, 1);
if (imageData.data.length > 0) {{
imageData.data[0] ^= 1;
ctx.putImageData(imageData, 0, 0);
}}
}} catch(e) {{}}
}}
return _toDataURL.apply(this, args);
}};

// WebGL 指纹覆盖
if (WebGLRenderingContext) {{
const _getParam = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(p) {{
if (p === 0x9245) return 'Intel Inc.';
if (p === 0x9246) return 'Intel Iris OpenGL Engine';
if (p === 0x1F00) return 'WebKit';
if (p === 0x1F01) return 'WebKit WebGL';
return _getParam.call(this, p);
}};
}}

// 覆盖 plugins 数组(非自动化浏览器通常有插件)
if (navigator.plugins.length === 0) {{
Object.defineProperty(navigator, 'plugins', {{
get: () => {{
const arr = [
{{name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer'}},
{{name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai'}},
{{name: 'Native Client', filename: 'internal-nacl-plugin'}},
];
arr.length = 3;
arr.item = i => arr[i];
arr.namedItem = n => arr.find(p => p.name === n);
return arr;
}}
}});
}}
''')

print(f'Anti-detect preset applied: {preset}')
return self

async def navigate(self, url):
"""导航到目标页面(已应用反检测)"""
await self.cdp('Page.navigate', {'url': url})
await asyncio.sleep(2)
return self

async def close(self):
if self.ws:
await self.ws.close()


# ====== 使用示例 ======
async def demo():
ad = CDPAntiDetect()
await ad.connect()

# 应用桌面端反检测预设
await ad.apply_preset('desktop')

# 访问目标网站
await ad.navigate('https://bot.sannysoft.com/') # 一个检测自动化浏览器的页面
await asyncio.sleep(3)

# 截屏验证
screenshot = await ad.cdp('Page.captureScreenshot', {'format': 'png'})
with open('anti_detect_result.png', 'wb') as f:
f.write(base64.b64decode(screenshot['data']))

await ad.close()

asyncio.run(demo())

访问 bot.sannysoft.compixelscan.net 可以检测你的反伪装效果。


局限性分析

1. CDP 连接本身可以被检测

1
2
3
// 检查 WebSocket 连接
// 自动化浏览器通常有活跃的 WebSocket 连接到调试端口
// 一些高级检测可以尝试发现这个连接

对于普通网站无需担心,但对于 Cloudflare 5 秒盾、Akamai 等高级防护系统,仅靠 CDP 注入可能不够。

2. 行为特征难以模拟

指纹参数可以改,但人类行为难以模拟:

  • 鼠标移动轨迹太直 → 被识破
  • 点击间隔太均匀 → 被识破
  • 没有滚动行为 → 被识破

建议配合 Input.dispatchMouseEvent 发送真实的鼠标轨迹,而不是直接跳转坐标。

3. 指纹一致性

如果你每次访问都产生不同的指纹,反而会被标记为”指纹变化异常”。建议:

  • 为每个目标网站固定一个指纹
  • 用同一指纹多次访问同一网站
  • 定期更换指纹(比如每天)

4. WebGL 修改的副作用

修改 WebGL 参数可能导致某些使用 WebGL 的网站出现渲染异常(比如 3D 模型查看器)。这种情况下可以选择不拦截 WebGL。

5. TLS 指纹

CDP 无法修改 TLS 层的指纹(JA3/JA3S)。一些高级安全系统通过分析 TLS 握手包的特征来识别自动化流量。这需要更底层的方案(如修改 Chrome 源码或使用 Go/rust 重写 TLS 栈)。


总结

CDP 提供了丰富的指纹修改能力,足以应对大多数网站的反爬和反自动化检测:

  • Navigator 属性 — webdriver、plugins、language 等
  • WebGL 参数 — 显卡供应商、渲染器
  • Canvas 指纹 — 像素噪声注入
  • AudioContext — 音频数据微调
  • 屏幕与视口 — 分辨率、缩放
  • 时区与语言 — Emulation 域支持
  • ⚠️ TLS 指纹 — CDP 无法触及,需要额外方案

建议的防护等级:

目标类型 方案
普通网站(新闻/博客) 只改 User-Agent + webdriver
中等级别(电商/社交) Apply desktop preset
高等级别(银行/风控) Desktop preset + 行为模拟
企业级(Cloudflare/Akamai) 上述所有 + TLS 指纹处理

下一篇文章将深入 CDP 的性能追踪与 Lighthouse 集成,敬请期待。

上一篇回顾:CDP 网络拦截与请求篡改实战:Python 控制 Chrome 抓包改包完全指南。

下一篇预告:CDP 性能追踪与 Lighthouse 集成:用 Python 测量 Web Vitals 与自动化性能审计。