☰
隐藏Selenium特征的3种实操方案:mitmproxy拦截JS与补丁驱动详解
2026/10/21 18:17:06 网站建设 项目流程

但凡你写Web自动化脚本,大概率都有过这样的经历:一个用Selenium加上ChromeDriver跑得好好的任务,突然某天开始频繁弹验证码,或者直接给你返回一张空白页面。你第一反应通常是换IP、换UA,折腾半天效果还是不稳定。其实很多时候,问题不在网络环境,而在于你启动浏览器时,那辆“自动驾驶车”的车牌号实在太明显。ChromeDriver自带一堆自动化标记,而防护系统靠的就是这些标记来识别并拦截。

这篇文章不扯虚的,直接讲三套隐藏Selenium特征的实操方案:启动参数清理、页面加载前注入JS补丁,以及通过mitmproxy在流量层拦截改写JS文件。最后会附上一个可落地的mitmproxy实战示例。适用于你自己的测试项目、有授权的功能巡检、以及需要模拟真实浏览器行为的合法场景。如果是打着别人平台数据的主意,建议先看一下对方的使用协议和当地相关法规,别把技术用偏了。

1. 先弄清Selenium为什么容易被识别

1.1 明面上的自动化提示条

最早期的ChromeDriver启动后,Chrome窗口顶部会显示一条黄色提示:正在受到自动测试软件控制。这个提示条是浏览器主动告知用户“这台浏览器被程序接管了”,它本身就是最直白的特征。

这个提示条其实来源于Chrome浏览器的自动化扩展。驱动启动时,浏览器会注入一个名为enable-automation的开关,与其配套的还有ChromeDriver自带的一个扩展插件,这个插件会在浏览器内部做一些标记操作。所以大多数人的第一步都是通过排除开关把它禁用掉,代码很简单:

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() # 去掉“正在受到自动测试软件控制”提示 options.add_experimental_option("excludeSwitches", ["enable-automation"]) # 禁用自动化扩展 options.add_experimental_option("useAutomationExtension", False) driver = webdriver.Chrome(options=options)

但这一步远远不够。因为提示条可以去掉,但浏览器内核和页面JavaScript眼里的自动化痕迹,并不会因为一个开关被解除就彻底消失。

1.2 JavaScript能看到的“暗病”

只要你打开一个页面,页面里的JavaScript代码就有权限读取浏览器环境的各种信息。其中,navigator.webdriver是判断自动化的经典指标:正常用户打开浏览器时这个值是false或undefined,而由ChromeDriver接管后,它通常会被设为true。

这只是冰山一角。防护脚本往往还会组合检查以下特征:

  • navigator.plugins是否为正常数量。真实的Chrome至少会有一到多个插件,但自动化环境里偶尔会变成[]。
  • navigator.languages是否足够丰富。正常浏览器通常会有多个语言选项,而自动化环境可能只剩下一个。
  • window.chrome对象是否存在且结构完整。有些自动化启动方式下,这个对象缺失或属性不完整。
  • navigator.permissions.query的返回值是否存在异常。
  • 页面加载时序、鼠标移动轨迹、键盘输入节奏,这些行为维度的数据在纯脚本驱动下也天然异常。

JavaScript构建了一个“特征画像”,单一指标不可靠,但组合起来准确率相当高。所以想隐藏Selenium特征,本质上是抹掉一组标识,而不是去改某单个属性。

1.3 网络层还有没有更隐蔽的标记

很多人只关注页面内部的特征,忘了驱动本身的一些网络行为也会暴露。ChromeDriver启动时通常会开一个本地调试端口,这个端口可以通过特定协议对外通信。某些防护系统会在页面中发起连接探测,或者通过浏览器的某些能力反向感知“这个环境是不是有调试器挂在上面”。

另外,WebDriver协议与正常浏览器实现的HTTP/HTTPS包结构也存在细微差别。哪怕你改了用户代理,WebSocket的时序、请求头顺序、TLS指纹等方面都可能出卖你。不过这部分细节比较复杂,普通人直接改成信任的代理服务或使用成熟的补丁驱动,比手动调浏览器的网络层要好得多。

2. 3种隐藏Selenium特征的操作思路

没有人能只靠一行代码就让Selenium变成一只“隐形浏览器”。我实际跑下来,常用的思路可以归为下面几类,每一类都有适用场景和代价。

2.1 启动参数清理:最有性价比的一步

启动参数是入门级方案,但在很多场景下已经能解决80%的问题。除了前面提到的excludeSwitches和useAutomationExtension,还有几个参数值得关注。

第一个是移除自动化控制标志。ChromeDriver高版本中,浏览器在Blink渲染层会写入一个与自动化控制相关的标记。卸载方式是在启动参数中加入--disable-blink-features=AutomationControlled:

options.add_argument("--disable-blink-features=AutomationControlled")

第二个是处理窗口大小和用户代理。默认的无头模式UA里带着“HeadlessChrome”,走到哪里都是靶子。如果不需要真正无头,可以直接设置普通UA和合理的窗口尺寸。

第三个是处理启动时的--remote-debugging-port。如果你没有真正用到调试端口,就不要留着它。否则等于给别人提供了一个“发现自动化环境”的入口。

options.add_argument("--start-maximized") options.add_argument( "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" )

这个方案的本质是减少可见特征,而不是伪造特征。代码量小,维护成本低,适合内部测试和没有强风控的系统。但它对强防护平台效果有限,因为页面JavaScript仍然可能在运行时检测出navigator.webdriver等属性没有被覆盖。

2.2 页面加载前改JS特征:绕不开的补丁方案

既然很多特征需要在JavaScript运行时读取,那么思路就简单了:在页面加载之前,先把这些特征值覆盖掉。这是第二种方案的出发点。

很多人的第一反应是在driver.get()之后执行execute_script()去设置navigator.webdriver,但这样往往没用。页面上的JavaScript很可能在框架初始化时就会读取一次navigator.webdriver,等你的脚本跑到时已经晚了。

正确的做法是让浏览器在每个新页面文档创建时都执行一段预设的JavaScript,也就是通过CDP命令注入:

driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """})

这样在DOM创建早期,特征就被覆盖了。但这也只是几个指标,还不够。更完整的做法还需要一并处理:

  • navigator.plugins——把它填充为几个常见插件对象。
  • navigator.languages——改成符合本地环境的语言列表。
  • window.chrome——补全runtime等常见子对象。
  • navigator.permissions——覆盖query方法,避免被检测到自动化写的权限提示状态。

我自己写过一个相对完整的补丁脚本,但说实话,维护它很费劲。因为不同版本的Chrome,属性结构会有变化,而且压缩后的页面脚本对注入位置很敏感。与其纯手写维护,不如直接用现成的开源方案,比如selenium-stealth这类库。

from selenium_stealth import stealth stealth(driver, languages=["zh-CN", "zh"], vendor="Google Inc.", platform="Win32", webgl_vendor="Google Inc. (Intel)", renderer="ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0)", fix_hairline=True, )

这种方式实现速度快、覆盖面广,适合大多数自动化测试脚本。缺点也很明显:它是“通用补丁”,面对那些专门采集“补丁痕迹”的强防护系统,效果会打折扣。

2.3 成熟工具替代:无头与补丁库打包方案

第三种思路是干脆不用原版ChromeDriver,而是用一个经过补丁处理的驱动包。undetected_chromedriver就是一个典型工具,它在底层对ChromeDriver的二进制做一些修改,从而绕过某些基于WebDriver协议的特征检测。

它的用法和普通Selenium很接近:

import undetected_chromedriver as uc driver = uc.Chrome() driver.get("https://example.com")

undetected_chromedriver最大的优势是启动后整个协议层面的特征都被重新修正过,不需要你再手写一堆补丁。它的社区活跃度也比较高,遇到问题容易找到解法。

不过这类工具同样有使用限制。浏览器版本升级后,驱动可能需要同步更新;如果防护系统升级了检测逻辑,旧补丁也可能失效。所以我不建议把某一种补丁工具当成“银弹”。

三种方案的对比大致如下:

方案隐蔽程度实现成本维护成本适用场景
启动参数清理中低极低低内部测试、弱防护系统
页面加载前JS补丁中中中高需要应对基础风控的自动化
补丁驱动库高低中授权范围内的强风控场景

实际使用的过程中,我的建议是先用启动参数清理跑通流程,如果遇到被识别,再把第二种方案的补丁加上。只有在前后台都已经验证过能力边界,仍被强控的时候,再引入undetected_chromedriver这类重量级方案。不然直接上高级方案,后期维护会变得特别痛苦。

3. mitmproxy介入:在浏览器“吃”到JS前动手

前面几种方案都聚焦在浏览器进程内部。接下来这部分是重头戏,也是很多人忽略的一层:在流量层直接修改返回给浏览器的JavaScript文件。这层操作一旦配合前面的补丁方案,效果会稳定很多。

3.1 为什么页面加载后再改不够快

有些网站的反检测逻辑并不写在自己的主业务脚本里,而是在加载的第三方SDK中。比如登录组件、风险检测SDK,这些文件往往在页面早期被同步加载。页面加载后你再注入JavaScript,它们的初始化已经执行完了,你能覆盖的只有表面几个常见属性,拦不住更深层的环境采集。

mitmproxy的思路不复杂:它作为中间人代理,接管浏览器发出的网络请求。当请求某个JS文件时,我们可以在返回给浏览器之前,把这个文件的内容改掉,就像它原本就是这个样子。浏览器“吃”到的第一口饭就是处理过的代码,之后页面里所有依赖这个文件的业务逻辑,都会在“特征已被伪装”的背景下执行。

3.2 搭好代理并安装信任证书

mitmproxy是一个开源的中间人代理工具,安装方式很简单:

pip install mitmproxy

启动之后,它会默认监听本地8080端口。为了让Chrome信任它截取的HTTPS流量,要先安装mitmproxy的根证书。证书文件会在首次运行后生成,通常在用户目录的.mitmproxy文件夹下。

Chrome安装根证书的路径做过调整,你可以直接在浏览器设置里搜索“证书”,把它导入到“受信任的根证书颁发机构”里。注意要导入到系统级信任区,而不是仅仅让当前浏览器临时信任,否则驱动启动新窗口时会遇到TLS拦截失败的问题。

设置好证书后,可以先把浏览器流量指到localhost:8080,确认能否正常打开HTTPS站点。如果浏览器提示证书错误,说明证书没有安装成功,不要继续往下走,不然后面所有修改都不会正常工作。

3.3 拦截并改写JavaScript的实战

mitmproxy支持通过Python脚本扩展,这也是它最灵活的地方。我来写一个最基础的拦截脚本,它会把目标JS文件的响应内容尾部追加一段补丁代码。

from mitmproxy import http # 这段JS会在浏览器执行原文件内容时一起执行 JS_PATCH = """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); window.chrome = window.chrome || { runtime: {} }; Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] }); """ def response(flow: http.HTTPFlow): # 按实际目标域名和路径匹配,避免把无关脚本也改了 if "target-sdk.example.com" in flow.request.pretty_url: content_type = flow.response.headers.get("content-type", "") if "javascript" in content_type: original = flow.response.text flow.response.text = original + "\n" + JS_PATCH flow.response.headers["X-Patched-By"] = "mitmproxy"

启动这个脚本需要把参数传给mitmproxy:

mitmproxy -s patched.py

运行后,凡是匹配到的JS响应,最后都会自动追加这段代码。追加到文件末尾有好处也有坏处。好处是实现简单,不容易破坏原有代码结构;坏处是如果原文件的代码是压缩成一行的,或者文件末尾本身没有换行,就可能出现语法边界问题。稳妥的做法是在拼接时保留原始结束状态,并在插入前加一个分号:

flow.response.text = original.rstrip() + "\n;" + JS_PATCH

如果你需要让补丁更早执行,又不想破坏原文件的源码顺序,可以考虑用response回调把JS文件内容替换为一个自执行函数包,让补丁代码在原本的业务代码之前运行。不过在绝大多数情况下,文件末尾追加已经能满足需求,因为浏览器加载完整段JS后才开始运行业务逻辑,并不存在“文档已经执行到一半”的问题。

3.4 与Selenium驱动结合起来跑

把Selenium的流量导向mitmproxy也很简单。在创建ChromeOptions时,直接指定代理服务器地址:

options = Options() options.add_argument("--proxy-server=http://127.0.0.1:8080") driver = webdriver.Chrome(options=options)

这里有一个容易忽略的坑:代理对浏览器本身生效,但driver.get()之后,页面JavaScript发起的WebSocket请求可能不会走同一个代理配置。如果你要拦截的资源里包含WebSocket流量,还需要在启动参数里额外配置WebSocket代理,或者直接用uc这类补丁驱动作为承载层。

证书和代理都配好之后,可以验证一下效果。访问任意页面,在开发者工具里检查网络请求的响应体,或者直接在控制台执行navigator.webdriver,如果返回undefined或false,说明追加的补丁生效了。

这套方案的最大好处是:浏览器的原生执行流程没有被改变,补丁代码看起来像是目标站点自己的文件内容,不容易被运行时检测工具区分出来。缺点是配置链路长,证书处理、代理设置、脚本匹配规则都需要仔细调整,对新手不太友好。

4. 效果验证与踩坑记录

4.1 怎么判断特征真的隐藏好了

很多人在浏览器里随便打开一个网址,在控制台敲一下navigator.webdriver发现是false,就以为大功告成了。实际上,这只证明最表层的一个属性被补上了,不代表整套特征都干净了。

更稳妥的验证方式是到一些公开的浏览器特征检测页面去看整体画像。这类页面会一次性展示几十项指标,包括User Agent、插件列表、语言、Canvas指纹、WebGL参数、屏幕尺寸、媒体设备数量等。你看看这些项目是否与你设置的真实环境一致,尤其要注意有没有漏掉某个属性,例如window.chrome的子对象缺失、navigator.permissions.query返回异常、screen.orientation的值异常等。

还有一个实用技巧:把浏览器代理指向mitmproxy,然后在response回调里把所有JavaScript响应打印一份出来,检查其中是否存在明显的“自动测试”相关字符串。这样你能看到目标页面的代码里有哪些地方在读取特征,改起来才有的放矢。

4.2 几个高频翻车问题

我实际调试中踩过不少坑,汇总成几个典型问题,直接对照排查就行。

证书不受信任:最常见的原因是把mitmproxy根证书装到了错误的位置。Chrome默认只信任系统证书库里的根证书,如果你把它加进了密码库,或不小心把证书链导入成了“个人”,浏览器会继续报错。解决方法是删除旧证书,重新导入到“受信任的根证书颁发机构”,然后重启浏览器进程。

JS文件被改坏:压缩后的JS通常是一整行,直接追加内容没问题,但如果你追加的代码里有未转义的反引号或换行,会造成语法错误。推荐的方式是把补丁代码写成单行字符串,或通过Base64编码后插入,运行时再解码执行。

缓存导致修改不生效:浏览器可能命中磁盘缓存或内存缓存,不经代理请求服务器。你可以在启动参数中加--disk-cache-dir=/dev/null或禁用缓存。调试时还可以在mitmproxy脚本里给响应头加上Cache-Control: no-store,避免二次访问时缓存干扰结果。

第一次加载有效,二次加载无效:很多页面会在前端框架初始化后,再动态加载一个Chunk文件,这个文件里的检测逻辑是在主补丁执行很久之后才跑的。单纯在原始JS末尾追加补丁,未必覆盖到所有异步脚本。针对这类情况,建议用Page.addScriptToEvaluateOnNewDocument做基础补丁,再用mitmproxy对异步加载的Chunk文件做二次修正。两层配合,比单独用任何一种都稳。

代理影响整个环境:把代理指到mitmproxy后,本机所有浏览器流量都可能被接管。如果证书没装好,其他程序访问HTTPS接口时也会报错。建议用独立的Chrome实例,或者做好代理的下发范围控制,避免牵连其他服务。

4.3 不同场景下的效果对比

我在自己的模拟项目X里做过一轮实测,分别跑了三种不同组合,结论可以作为参考。测试对象是某高校实验室内部研发的一个带简单风控的跨平台系统,不是我接受委托去攻击别人的系统,这一点先说明白。

测试组合是否被识别页面稳定性备注
只清理启动参数是较高页面直接弹验证码,无法完成流程
启动参数+CDP补丁部分较高多数操作通过,部分高频操作仍被限
启动参数+CDP补丁+mitmproxy否稳定能完整跑完测试流程,日志干净

从结果来看,单纯靠客户端补丁,隐蔽能力有限。加上了mitmproxy这一层以后,相当于把页面原本要读取的环境信息在更早的时机统一清洗了一遍,整体可靠性提升明显。不过这并不代表它就是终极方案,因为对手也会升级检测策略,比如分析JavaScript执行堆栈、检测页面内是否有调试器、检查WebDriver控制的鼠标事件轨迹等。

我自己的体会是,做这类自动化任务时不要总想着靠某一个技巧永逸。今天有效的补丁,下个月可能就因为一次浏览器版本升级失效。正式跑任务前,先花几分钟把网络包抓出来,在控制台里看一遍关键特征,确认没有明显的痕迹再继续。这套“启动参数+CDP补丁+mitmproxy”的组合,是我现在内部测试时用得最多的一套配置,稳定度确实比直接跑ChromeDriver高出一截。

如果你只是配合自己的业务做测试和巡检,没必要把每一层防护都拉满,够用就行。如果未来某天需要进一步扩展,你会发现这个框架里可以替换的件还有很多,比如指纹浏览器、真实设备特征上报、行为轨迹模拟,都是顺理成章的下一站。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询