隐藏Selenium特征的3种实操方案:mitmproxy拦截JS与补丁驱动详解 但凡你写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(optionsoptions)但这一步远远不够。因为提示条可以去掉但浏览器内核和页面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-featuresAutomationControlledoptions.add_argument(--disable-blink-featuresAutomationControlled)第二个是处理窗口大小和用户代理。默认的无头模式UA里带着“HeadlessChrome”走到哪里都是靶子。如果不需要真正无头可以直接设置普通UA和合理的窗口尺寸。第三个是处理启动时的--remote-debugging-port。如果你没有真正用到调试端口就不要留着它。否则等于给别人提供了一个“发现自动化环境”的入口。options.add_argument(--start-maximized) options.add_argument( user-agentMozilla/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], vendorGoogle Inc., platformWin32, webgl_vendorGoogle Inc. (Intel), rendererANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0), fix_hairlineTrue, )这种方式实现速度快、覆盖面广适合大多数自动化测试脚本。缺点也很明显它是“通用补丁”面对那些专门采集“补丁痕迹”的强防护系统效果会打折扣。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启动这个脚本需要把参数传给mitmproxymitmproxy -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-serverhttp://127.0.0.1:8080) driver webdriver.Chrome(optionsoptions)这里有一个容易忽略的坑代理对浏览器本身生效但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高出一截。如果你只是配合自己的业务做测试和巡检没必要把每一层防护都拉满够用就行。如果未来某天需要进一步扩展你会发现这个框架里可以替换的件还有很多比如指纹浏览器、真实设备特征上报、行为轨迹模拟都是顺理成章的下一站。