You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

在Ubuntu环境下的Playwright中如何为并行执行的每个浏览器上下文隔离客户端证书?

解决多隔离Chromium实例绑定独立客户端证书的问题

看起来你已经走对了方向——用NSS证书数据库+隔离HOME目录来实现Chromium实例的完全隔离,这确实是Linux环境下解决多客户端证书并行问题的最优路径。结合你遇到的几个失效场景,我整理了几个关键的调整点和验证步骤,应该能帮你解决问题:

一、先排查Playwright clientCertificates失效的核心原因

你提到的clientCertificates配置没起作用,大概率和巴西Acesso Gov.br证书的特殊属性有关:

  • 先检查你的PFX是否包含完整证书链(根CA+中间CA+用户证书),很多政府类证书依赖特定中间链,缺失会导致Playwright无法识别证书的合法性
  • openssl pkcs12 -info -in yourcert.pfx命令查看证书的Key Usage字段,必须包含Digital SignatureKey Encipherment——这是客户端认证的必需属性
  • 注意:Playwright的clientCertificates只认PKCS#12格式(也就是你的PFX),而且passphrase绝对不能错,建议先单独跑一个实例测试,排除多实例的干扰

二、优化NSS数据库配置的细节(这是关键!)

你的NSS数据库复制逻辑有几个小问题,这可能是证书不被Chromium识别的核心原因:

  1. 严格设置NSS数据库的权限
    Chromium对NSS数据库的权限要求特别苛刻,必须是当前用户独占读写,不能有全局权限。复制完文件后一定要加这两步:
chmod -R 700 ${profilePath}/.pki/nssdb
chown -R $(whoami):$(whoami) ${profilePath}/.pki/nssdb
  1. 别手动复制NSS文件,直接用工具导入PFX
    手动复制现成的cert9.db/key4.db很容易因为NSS版本差异导致格式不兼容,不如直接在目标profile路径下重新导入PFX:
# 先创建空的NSS数据库
certutil -N -d sql:${profilePath}/.pki/nssdb --empty-password
# 导入PFX(会自动拆分证书和密钥到对应数据库文件)
pk12util -i yourcert.pfx -d sql:${profilePath}/.pki/nssdb -W "你的证书密码"

这种方式生成的数据库绝对适配当前系统的NSS版本,不会有兼容性问题。

  1. 强制Chromium用指定的NSS路径,避免系统默认库干扰
    除了设置HOME环境变量,直接加启动参数指定NSS路径更稳妥:
args: [
  // ...其他参数
  `--nss-db-path=${path.join(profilePath, ".pki", "nssdb")}`,
  // 注意这里的格式是JSON数组字符串,之前的写法可能因为引号转义被忽略
  '--auto-select-certificate-for-urls=\'[{"pattern":"https://sso.acesso.gov.br/*","filter":{}}]\''
]

三、调整Playwright PersistentContext的配置

  1. 禁用系统证书存储,强制用自定义NSS库
    添加参数禁止Chromium自动加载系统级证书,确保每个实例只用到你导入的证书:
args: [
  // ...其他参数
  "--disable-system-certificate-store",
  // 如果测试环境的证书不被信任,还可以加这个参数跳过验证
  "--ignore-certificate-errors-spki-list"
]
  1. 验证实例隔离的有效性
    创建上下文后,可以加一段代码验证证书是否正确加载:
// 上下文创建后,打开Chrome证书设置页面
const page = await context.newPage();
await page.goto("chrome://settings/certificates");
// 截图保存,直观确认证书是否存在
await page.screenshot({ path: `${certId}_loaded_certs.png` });

四、针对巴西ICP-Brasil证书的特殊处理

巴西的Acesso Gov.br证书由ICP-Brasil颁发,可能有特殊的OID扩展:

  • openssl x509 -in yourcert.pem -text -noout查看Extended Key Usage,必须包含1.3.6.1.5.5.7.3.2(这是客户端认证的标准OID)
  • 如果系统中没有ICP-Brasil的根CA证书,记得把根CA也导入到每个实例的NSS数据库中,否则Chromium会认为证书不被信任

最终优化后的关键代码片段

把原有的NSS复制逻辑替换成直接导入PFX的逻辑:

// 新增方法:准备NSS证书库
async prepareNSSProfile(certInfo: CertInfo) {
  const { profilePath, pfxPath, pass } = certInfo;
  const nssDbPath = path.join(profilePath, ".pki", "nssdb");
  
  if (!fs.existsSync(nssDbPath)) {
    fs.mkdirSync(nssDbPath, { recursive: true });
    // 创建空NSS数据库
    execSync(`certutil -N -d sql:${nssDbPath} --empty-password`);
    // 导入PFX证书
    execSync(`pk12util -i ${pfxPath} -d sql:${nssDbPath} -W "${pass}"`);
    // 设置严格权限
    execSync(`chmod -R 700 ${nssDbPath}`);
    execSync(`chown -R $(whoami):$(whoami) ${nssDbPath}`);
    // 验证导入结果
    this.validateNSS(nssDbPath);
  }
}

// 在getOrCreateContext中调用prepareNSSProfile
async getOrCreateContext(certInfo: CertInfo): Promise<BrowserContext> {
  const { certId, profilePath } = certInfo;
  if (this.contexts.has(certId)) {
    console.log(`[BrowserFactory] Reusing context for cert ${certId}`);
    return this.contexts.get(certId)!;
  }
  console.log(`[BrowserFactory] Creating new context for cert ${certId}`);
  const isHeadless = process.env.HEADLESS === "true";

  // 先准备NSS证书库
  await this.prepareNSSProfile(certInfo);

  const browserEnv = { ...process.env, HOME: profilePath };
  console.log(`[BrowserFactory] Forcing Chromium to use HOME=${profilePath}`);

  const context = await chromium.launchPersistentContext(profilePath, {
    headless: isHeadless,
    env: browserEnv,
    userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    viewport: { width: 1366, height: 768 },
    locale: "pt-BR",
    timezoneId: "America/Sao_Paulo",
    args: [
      "--disable-blink-features=AutomationControlled",
      "--no-sandbox",
      "--disable-dev-shm-usage",
      "--disable-gpu",
      "--disable-software-rasterizer",
      "--disable-setuid-sandbox",
      "--disable-infobars",
      "--window-size=1366,768",
      "--font-render-hinting=none",
      `--nss-db-path=${path.join(profilePath, ".pki", "nssdb")}`,
      '--auto-select-certificate-for-urls=\'[{"pattern":"https://sso.acesso.gov.br/*","filter":{}}]\'',
      "--disable-system-certificate-store"
    ],
  });
  this.contexts.set(certId, context);
  return context;
}

这些调整应该能解决你遇到的证书不生效和实例隔离问题,核心就是确保每个实例的NSS数据库完全独立,且证书导入过程严格符合Chromium的要求。

内容的提问来源于stack exchange,提问作者Victor Matheus

火山引擎 最新活动