PAYATHON 2026

Ubuntu 上如何隔离 Playwright 并行上下文的客户端证书?

数据小陈

创建每个 BrowserContext 时,可以分别传入 clientCertificates,让不同上下文使用各自的客户端证书。如果还需要浏览器进程隔离,可以为每个任务启动独立浏览器,再创建带证书的上下文。

你贴出的 launchPersistentContext() 调用没有传入 clientCertificates,因此这段代码没有通过 Playwright API 绑定指定的 PFX。origin 也必须对应实际发起客户端证书认证的 HTTPS 地址。现有信息中没有具体错误或认证请求地址,还不能认定问题出在巴西证书、OpenSSL 或 NSS。

先确认 clientCertificates 的配置条件

Playwright 从 v1.46 开始支持 clientCertificates,应在创建上下文时传入。PFX 使用 pfxPath;PEM 需要同时提供 certPath 和 keyPath,加密私钥还需要正确的 passphrase。Playwright Browser API

origin 是精确的来源地址,包含协议、主机名以及适用时的端口,不包含路径,也不使用 * 通配符。例如:

new URL("https://sso.acesso.gov.br/login").origin
// "https://sso.acesso.gov.br"

这段代码只能计算该 URL 的 origin,不能证明该域名就是执行客户端证书认证的服务。如果登录过程跳转到另一个认证域名,需要为实际请求证书的 origin 配置证书,不要只根据最初打开的登录页面填写。Playwright origin 定义

用最小示例验证并行上下文

可以先用一个 Chromium 实例和多个独立上下文测试,排除手动复制 NSS 数据库、自动选择证书参数及其他启动参数的影响。下面的函数接收证书任务和实际登录逻辑,为每个任务创建独立的上下文,并在完成后关闭。

import { chromium, type Page } from "playwright";

type CertificateJob = {
  certId: number;
  pfxPath: string;
  passphrase: string;
  certificateOrigin: string;
  loginUrl: string;
};

export async function runCertificateJobs(
  jobs: CertificateJob[],
  authenticateAndVerify: (
    page: Page,
    job: CertificateJob
  ) => Promise<void>
): Promise<void> {
  const browser = await chromium.launch({
    headless: process.env.HEADLESS === "true",
  });

  try {
    const results = await Promise.allSettled(
      jobs.map(async (job) => {
        const context = await browser.newContext({
          clientCertificates: [
            {
              origin: new URL(job.certificateOrigin).origin,
              pfxPath: job.pfxPath,
              passphrase: job.passphrase,
            },
          ],
          locale: "pt-BR",
          timezoneId: "America/Sao_Paulo",
        });

        try {
          const page = await context.newPage();
          await page.goto(job.loginUrl);
          await authenticateAndVerify(page, job);
        } finally {
          await context.close();
        }
      })
    );

    const failure = results.find(
      (result) => result.status === "rejected"
    );
    if (failure?.status === "rejected") {
      throw failure.reason;
    }
  } finally {
    await browser.close();
  }
}

certificateOrigin 应填写经过确认的认证 origin,loginUrl 是登录流程的入口,两者可以不同。authenticateAndVerify 需要执行网站要求的登录步骤,并校验登录后的身份。仅检查页面能否打开,无法证明服务器接受了正确证书。

示例使用 Promise.allSettled() 等待所有任务结束后再关闭共享浏览器,避免某个任务失败时提前终止其他任务。示例展示的是上下文隔离;如果需要进程隔离,应将 chromium.launch() 移到每个任务内部,并分别关闭浏览器。

保留持久化上下文时怎样修改

如果需要保存登录状态,可以继续使用 launchPersistentContext(),将证书配置直接加到该调用中:

const context = await chromium.launchPersistentContext(
  certInfo.profilePath,
  {
    headless: process.env.HEADLESS === "true",
    clientCertificates: [
      {
        // 仅当此 origin 确实执行客户端证书认证时使用。
        origin: "https://sso.acesso.gov.br",
        pfxPath: certInfo.pfxPath,
        passphrase: certInfo.pass,
      },
    ],
    locale: "pt-BR",
    timezoneId: "America/Sao_Paulo",
  }
);

每个同时运行的持久化实例必须使用不同的 profilePath。Playwright 文档说明,多个浏览器实例不能同时使用同一个 User Data Directory;关闭持久化上下文也会关闭对应浏览器。Playwright launchPersistentContext

使用这种配置方式验证时,无需先准备 NSS 数据库。现有代码会在启动前强制检查 cert9.db 和 key4.db。应将这些检查与直接使用 clientCertificates 的测试分开,避免还没执行证书配置就提前报错。

clientCertificates 仍然失败时按什么顺序排查

  1. 记录准确的失败位置,区分上下文创建失败、PFX 解密失败、页面导航时 TLS 握手失败,以及 TLS 成功但网站拒绝登录。保留错误消息,并记录 Playwright、实际浏览器和 Node.js 版本。代码中的 Windows Chrome/120 User-Agent 只是请求标识,不能据此判断实际浏览器版本。
  2. 确认认证请求的 origin。观察登录跳转和失败请求;如果能访问服务器或网关日志,检查哪个服务要求客户端证书。必要时为多个已经确认需要证书的 origin 分别添加配置项。
  3. 检查证书材料。确认 PFX 可读、密码正确,并包含目标证书及对应私钥;再检查有效期、证书链,以及证书用途是否符合目标服务要求。PEM 测试必须同时提供证书和私钥。
  4. 先分别运行两张证书,再并行运行。如果单独运行也失败,先解决认证问题;如果单独成功、并行失败,再检查上下文、目录、缓存和身份验证逻辑是否被共享。

可以先用以下命令检查 PFX 的解析情况,密码由命令交互提示输入:

openssl pkcs12 -in client.pfx -info -noout

如果使用 OpenSSL 3,且错误明确指向旧加密算法或 provider 不可用,可以对比 legacy 模式:

openssl pkcs12 -in client.pfx -info -noout -legacy

-legacy 会尝试加载 legacy provider。如果只有 legacy 模式能成功解析,才有依据进一步调查 PFX 的旧算法兼容性,但这仍不能直接证明 Playwright 使用的运行环境具有相同表现。OpenSSL pkcs12 文档

为什么当前 NSS 和 policies 检查不能确认隔离有效

当前的 validateNSS(profilePath) 检查的是 profilePath,而准备给浏览器使用的数据库位于 profilePath/.pki/nssdb。两处路径不同,检查前者成功不能证明后者内容正确。数据库也只在目标目录不存在时才会复制;如果目标目录已存在,即使文件缺失或证书已更新,代码也不会同步。

certutil 输出非空不能确认存在预期证书及其对应私钥,还需核对实际条目。即使数据库内容正确,也要确认浏览器确实读取了它。Chromium 的 Linux 文档说明其使用 NSS Shared DB,且不同版本的默认路径可能不同,不能仅凭设置了 User Data Directory 就推定证书库也随之隔离。Chromium Linux 证书管理说明

如果继续调查 AutoSelectCertificateForUrls,应在实际启动的浏览器里打开 chrome://policy,确认策略是否已加载、值是否正确以及是否存在冲突。写入策略文件本身不能证明浏览器已应用配置。Chrome 策略验证步骤

工厂方法还存在并发创建风险:两个相同 certId 的调用可能同时通过 this.contexts.has(certId) 检查,随后各自启动浏览器。可以缓存创建中的 Promise<BrowserContext>,或对同一 certId 的创建加锁,避免重复启动。需要独立登录会话的任务也应分别创建上下文,避免复用同一个 certId 时共享已有会话。

判断是否发生证书串用,应以服务器收到的客户端证书信息或登录后的身份为依据。现有代码和“没有生效”的描述不足以确定具体根因。

备注:内容仅供参考。