ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Selenium自动化测试绕过Chrome证书错误弹窗的完整指南

Selenium自动化测试绕过Chrome证书错误弹窗的完整指南

如果你维护过一段时间自动化测试脚本,大概率被 Chrome 那个写着“您的连接不是私密连接”的深灰色警告页卡过。脚本停在那个页面上,click 找不到元素,等待超时,任务挂掉,日志里只有一行孤零零的 SSL 错误。我早年在某公司维护端到端回归用例时,这个问题几乎每周都要跳出来一次,尤其是在访问测试环境、预发布环境或者内网搭建的服务时,浏览器动不动就给你脸色看。

这个问题的本质是浏览器做证书链校验时发现目标站点的 HTTPS 证书不可信,于是它宁可拒绝访问也不放行。对普通用户来说,这层保护是对的,但对 Selenium 自动化来说,它就是一个纯粹的“坑”:测试环境本来就是用自签名证书或过期证书搭起来的,脚本不可能每次都跑过去手动点一下“仍然继续”。所以我们需要在启动浏览器时,从 WebDriver 层面和浏览器进程层面把这道“安全门”打开,让测试脚本能顺畅地访问那些证书不规范的站点,同时又不影响正常环境下的安全检查。

这篇文章会把这个弹窗的成因、绕过它的几种方案、不同编程语言里的写法、以及我在实际项目中踩过的坑一次讲清楚。适合正在做 UI 自动化、接口自动化中需要调用浏览器页面的测试开发同学,也适合刚接触 Selenium 但已经被证书错误折磨过的新手。

1. 先弄清楚:这个弹窗到底是谁在拦你

1.1 浏览器做证书校验的三层逻辑

Chrome 之所以弹出“您的连接不是私密连接”,是因为它走完 TLS 握手之后,对服务器返回的数字证书做了三层检查:第一,证书是否由受信任的根证书颁发机构签发;第二,证书是否过期;第三,访问的域名和证书里的域名是否匹配。任何一个环节出问题,Chrome 都会直接终止页面加载,并在地址栏下方或整页显示安全警告。

举个例子,你本地启动了一个前后端联调服务,挂的是自签名证书。浏览器打开时看到的错误类型往往是NET::ERR_CERT_AUTHORITY_INVALID,意思是没有找到能追溯到受信根 CA 的证书链。如果证书已经过期了,出现的是NET::ERR_CERT_DATE_INVALID。如果证书是给dev.example.com签的,你却用 IP192.168.1.10去访问,就会报NET::ERR_CERT_COMMON_NAME_INVALID。这三种错误虽然文字不同,但在 Selenium 里表现都一样:driver.get()停住,后续任何元素操作都超时。

从技术底层来说,这个拦截是浏览器进程内部完成的,和 WebDriver 协议本身没有直接关系。但 WebDriver 规范里专门为自动化场景提供了一个 capability 用来“接受不安全的证书”,也就是acceptInsecureCerts。另外,Chrome 也保留了一组命令行启动参数,可以从浏览器进程层面直接关闭证书校验。理解了这一点,后面所有方案就都串起来了:要么走 Selenium 标准能力,要么走浏览器启动参数,要么两者一起上。

1.2 为什么自动化测试环境里最容易触发

生产环境的证书一般没问题,因为公网站点都接入了正规 CA。但测试环境基本都处于“能跑就行”的状态:有的人直接用openssl req -x509生成一张十年期的自签证书,有的人在服务器上把 HTTPS 配置的证书链少贴了一节,有的甚至把前一年的过期证书原封不动搬过来用。内网 IP 访问、临时域名、随手写的CN字段,这些都是自动化用例的定时炸弹。

还有一个很容易被忽略的场景:公司对内网流量做了 SSL 解密审计,代理服务器会在中间替换证书。这种情况下浏览器看到的是代理机构签发的证书,而这份证书没有安装到测试机器的系统信任库中,于是访问任何 HTTPS 站点都会报证书错误。这种场景下,即使你跳过了证书校验,还会跟代理本身发生各种奇奇怪怪的交互问题,我在后面排查章节里会单独说。

说白了,自动化测试跑得越多,遇到这个弹窗的概率就越高。它不是偶发问题,而是测试环境安全配置不完整时的一种”必然结果“。所以解决方案不能是“每次遇到就手工点一下继续”,那样根本无法融入持续集成流水线。

2. 方案一:用 Selenium 标准能力绕过去

2.1 acceptInsecureCerts 的用法与原理

acceptInsecureCerts是 W3C WebDriver 规范中定义的一个标准 capability,专门用来指示浏览器在会话内自动接受不安全的证书。在 Selenium 4 的 Python 版本里,配置方式非常简单,直接在Options对象上把属性置为True:

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.accept_insecure_certs = True driver = webdriver.Chrome(options=options) driver.get("https://self-signed.dev.local")

这段代码的底层逻辑是:ChromeDriver 在创建新会话时,会把acceptInsecureCerts转成 Chrome 内部的--ignore-certificate-errors开关,并在会话生命周期内始终生效。它不像某些 hack 方案那样需要你在启动后再执行额外步骤,而是从一开始就告诉浏览器“这个会话里,有证书问题的站点也可以继续加载”。

这个方案最明显的优势是写法干净、语义清晰。测试脚本阅读者一看就知道这个会话允许访问不安全证书,而不会误以为你放弃了所有安全保护。而且它只作用于当前 WebDriver 会话,不会全局修改浏览器的默认行为,也不影响你在同一台机器上手动打开 Chrome 时的安全级别。

2.2 实操:完整的最小可运行示例

光设置一个属性还不够,真实项目中还需要处理浏览器驱动路径和隐式等待等问题。下面这个例子是典型的、可以直接运行的最小验证脚本:

import time from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.accept_insecure_certs = True options.add_argument("--ignore-certificate-errors") driver = webdriver.Chrome(options=options) driver.implicitly_wait(10) try: driver.get("https://self-signed.dev.local") time.sleep(2) print("当前页面标题:", driver.title) print("当前页面URL:", driver.current_url) finally: driver.quit()

注意这里我同时加了accept_insecure_certs和--ignore-certificate-errors。可能有人会觉得重复,但我在实测中发现,某些 Chrome 版本里只设置前者、在访问带 HSTS 头的站点时依然会被拦,加上后者就稳了。既然两个参数不冲突,为什么不在一次启动时都传进去?代价接近于零,收益是一致性大幅提升。

这个脚本跑完后,如果你发现标题能正常打印出来,说明证书拦截已经被成功绕过。如果仍然停在错误页,那就要看是不是 Chrome 版本太新、参数被策略限制的情况,下面的章节会专门讲。

3. 方案二:Chrome 启动参数这把大杀器

3.1 关键的启动参数组合与执行细节

Chrome 有一组命令行参数,可以在浏览器启动时直接修改安全策略。最核心的是--ignore-certificate-errors,它会让 Chrome 忽略站点证书的所有错误,包括过期、域名不匹配、证书链断裂等。但在较新的 Chrome 版本里,Chrome 团队对命令行参数做了一些限制,单独使用这个参数有时并不会生效。实操中的稳妥做法是再加一个--test-type:

--ignore-certificate-errors --test-type

--test-type是 Chrome 内部用于测试模式的参数,它告诉浏览器“当前这个进程是用作自动化测试的”,因此可以放行那些日常模式下会被拒绝的实验性开关和敏感策略。两个参数组合使用,能覆盖绝大多数证书错误场景,哪怕目标是 HSTS 站点也会被强制忽略证书问题。

这里要特别提醒一点:--ignore-certificate-errors是全局性的“大杀器”,它会关闭整个浏览器进程的证书校验能力。如果你在同一个 Chrome 会话里既要访问测试环境的自签名站点,又要访问公网的真实站点,那么公网站点也会跳过校验。对测试环境来说这不是问题,但如果你在脚本里还要做支付流程、订单提交这类对安全要求很高的操作,最好单独起一个测试专用的 Chrome 实例,而不要直接在你日常浏览器配置上启动。

3.2 各语言中的落地写法

这个参数在 Python、Java、JavaScript 里写法不同,但核心思路完全一致。这里把三个主流语言的示例都放一下,方便不同技术栈的团队直接参考。

Python:

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--ignore-certificate-errors") options.add_argument("--test-type") driver = webdriver.Chrome(options=options)

Java:

import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; ChromeOptions options = new ChromeOptions(); options.addArguments("--ignore-certificate-errors"); options.addArguments("--test-type"); WebDriver driver = new ChromeDriver(options); driver.get("https://self-signed.dev.local");

JavaScript:

const { Builder } = require('selenium-webdriver'); const chrome = require('selenium-webdriver/chrome'); let options = new chrome.Options(); options.addArguments('--ignore-certificate-errors', '--test-type'); let driver = new Builder() .forBrowser('chrome') .setChromeOptions(options) .build();

除此之外,还有一个偏“底层”的方式是直接调用 Chrome DevTools Protocol,在会话建立后动态关闭证书校验:

driver.execute_cdp_cmd("Security.setIgnoreCertificateErrors", {"ignore": True})

这个方式适合你已经用driver.get()访问了一个页面、发现被拦截、然后又想临时放行的情况。不过它属于事后补救,没有启动参数那么干净,而且每次新会话都要重新执行一遍,我的建议是把它当作调试技巧,而不是常规方案。

3.3 方案横向对比表

方案原理层级是否支持远程会话是否影响全局推荐指数
accept_insecure_certs = TrueWebDriver 标准能力支持,可随会话传递仅当前会话高
--ignore-certificate-errorsChrome 进程启动参数仅本地启动时可指定整个浏览器进程中
--ignore-certificate-errors --test-type启动参数+测试模式仅本地启动时可指定整个浏览器进程高
Security.setIgnoreCertificateErrorsCDP 底层协议支持当前页面会话中

从表里能直观看出,accept_insecure_certs在远程 WebDriver、Selenium Grid 场景中更占优势,因为它属于会话级别的能力描述,可以跨节点传递;而命令行参数则更适合本地跑一个独立测试进程的情况。我个人的习惯是把它们组合使用,既保证本地直接跑,也保证上传到 CI 执行节点后有兜底。

4. 特殊场景的补充打法

4.1 HSTS 站点和 302 跳转劫持

如果你的被测站点启用了 HSTS(HTTP Strict Transport Security),浏览器会强制使用 HTTPS,并且不允许用户轻易“继续访问”,因为 HSTS 的设计目标就是对抗 SSL 剥离攻击。这时候单独设置accept_insecure_certs大概率无效,必须配合--ignore-certificate-errors才能让 Chrome 放弃 HSTS 检查。

我在某次测试一个内部单点登录系统时,就遇到过这种问题。登录入口本身是 HTTPS,但页面里有一段302跳转,把用户从https://转到http://的静态资源域名上,然后再弹回来。这一来一回,Chrome 直接拦截了后面的证书错误,脚本就卡在“您的连接不是私密连接”的页面。最后我在启动参数里加了--ignore-certificate-errors和--test-type,问题才真正解决。

另外一个容易忽略的点是:如果被测页面里嵌入了iframe或静态资源,而这部分资源用的也是自签名证书,那么即使主页面能被访问,iframe里的内容也可能加载不出来。Selenium 的switch_to.frame()在切换到这些iframe时会直接报错。处理方式依然是靠全局参数或确认测试环境证书统一正确,没有别的捷径。

4.2 远程 WebDriver 和 Selenium Grid 环境下的传导问题

在本地用ChromeDriver时,启动参数非常直接。但如果你把测试跑在 Selenium Grid 或第三方云测平台上,情况就不一样了。远程节点收到的不是本地的options.add_argument(),而是一个 JSON Wire Protocol 里的 capabilities 描述。如果你只在远程节点本地的ChromeOptions里写了参数,而创建请求时没有把它随 capabilities 一起发送,那么远端浏览器根本不会加载这些参数。

正确的做法是在创建远程会话时显式传入acceptInsecureCerts:

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.accept_insecure_certs = True # 远程 WebDriver / Grid 场景 driver = webdriver.Remote( command_executor="http://grid.example.com:4444/wd/hub", options=options )

同时,在远端节点上最好也把系统级信任库配好,否则即使 WebDriver 忽略了证书错误,部分http请求仍可能被节点所在环境的代理截断。这个又回到了我在开头提到的“SSL 解密”场景。云测平台的浏览器进程通常是从预置镜像里拉起来的,如果你没有权限修改远程节点的启动脚本,优先保证acceptInsecureCerts能传上去,是最可控的做法。

4.3 更彻底的方案:把测试环境证书装进系统信任库

如果证书是团队内部自建的,可以直接把根证书导出后安装到运行测试的机器或容器镜像的信任库中。这样浏览器和 WebDriver 都无需任何特殊参数,访问自签名环境就像访问正式环境一样,安全模型保持完整。

在 Linux 容器里,通常是这么做的:

cp self-signed-ca.crt /usr/local/share/ca-certificates/ update-ca-certificates

实测下来,这个方案的稳定性最高,因为它解决的是证书“根因”,而不是在浏览器层面对错误视而不见。缺点是需要维护镜像、更新证书周期,如果测试环境隔三差五换一张证书,运维成本就会变大。我建议保留这个方案作为小团队长期稳定跑的基线,而把acceptInsecureCerts作为快速响应手段。

5. 高频坑位排查速查表

5.1 参数明明设了却不生效

最典型的“坑”是:你在Options里加了accept_insecure_certs = True,但脚本还是被证书错误页卡住。第一步要确认你用的 Selenium 版本。Selenium 3 和 Selenium 4 在Options的属性和参数传递方式上有差异,旧接口的写法可能不会把 capability 正确传到 ChromeDriver。推荐先升级到 Selenium 4,然后用driver.capabilities打印一下实际会话状态:

print(driver.capabilities.get("acceptInsecureCerts"))

如果输出是True,说明 capability 已经传进去了,问题多半出在 Chrome 版本对参数的限制上。这时候再叠加--ignore-certificate-errors --test-type启动参数,通常能解决。

如果输出是None或False,那说明你的Options没有正确传递,检查一下是不是用了Capabilities对象合并时覆盖了设置。

5.2 代理、无头模式、版本冲突三类常见问题

下面这张速查表,基本覆盖了我在实际项目和社区帮人排查时遇到的高频问题:

现象常见原因处理方法
设置了accept_insecure_certs仍在错误页Chrome 版本较新,单参数被策略限制同时加--ignore-certificate-errors和--test-type
无头模式下证书错误依旧headless模式参数传递方式不同显式添加--ignore-certificate-errors,再验证--headless=new语法
访问 HTTPS 站点出现ERR_SSL_PROTOCOL_ERROR代理服务器介入但证书未安装配置代理信息、安装代理证书,或在测试网络中禁用 SSL 解密
Chrome 升级后脚本先前的处理失效浏览器版本与 ChromeDriver 版本不匹配用 webdriver-manager 自动匹配驱动版本
远程 Grid 中参数无效未将 options 随远程会话一起传递改为webdriver.Remote(options=options),并显式传 capability
页面能访问但 iframe 资源加载失败内嵌资源仍是自签名证书用全局忽略参数,或修复测试环境资源证书

其中“Chrome 升级后脚本失效”这个问题的隐蔽性最强。Chrome 每隔几周就自动更新,而 ChromeDriver 如果还是旧版,两者版本不匹配时甚至不会正常启动浏览器,表现出的症状五花八门。强烈建议在项目里接入 webdriver-manager 这类工具:

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options)

它会自动读取本机 Chrome 版本号,并下载匹配的 ChromeDriver,能少掉很多环境层面的麻烦。

5.3 识别“是否还停留在错误页”的调试技巧

最后分享一个调试小技巧:Chrome 的证书错误页地址栏并不是正常网址,而是chrome-error://chromewebdata/这种内部地址。所以你在脚本里可以通过判断driver.current_url快速识别是否被拦截:

def is_cert_error_page(driver): return driver.current_url.startswith("chrome-error://")

如果判断为True,再执行任何页面交互都没有意义,应该直接刷新或重新driver.get()。搭配page_load_strategy = 'eager'使用,可以避免页面在加载阶段长时间卡住,让脚本能更快地进入后续等待逻辑。我在平时调试时也会把driver.current_url打印到日志里,一旦发现被拦截,日志里立刻能看到错误页地址,不用去猜到底卡在哪一步。

另外,如果你需要在同一个会话里访问多个不同域名的测试站点,而其中一个站点证书有问题,那最好把证书处理放到启动阶段一次性解决,而不是在脚本中途用 CDP 命令临时开启忽略。临时开启的忽略状态不会自动重置,后续访问其他站点时可能会掩盖真正的证书问题,排查起来更难。

就我个人经验来说,最稳的组合是启动时同时设置accept_insecure_certs和--ignore-certificate-errors --test-type,然后配合 webdriver-manager 管好浏览器驱动版本,再在脚本里加一个chrome-error://的判断兜底。这套方案在本地 Windows、macOS、Linux CI 上都验证过,除了个别代理网络环境需要额外配置系统信任库之外,几乎没有再被“您的连接不是私密连接”卡过。如果你也被这个弹窗折磨过,不妨按这个顺序把代码过一遍,大概率直接就能救回来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进