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 仍然失败时按什么顺序排查
- 记录准确的失败位置,区分上下文创建失败、PFX 解密失败、页面导航时 TLS 握手失败,以及 TLS 成功但网站拒绝登录。保留错误消息,并记录 Playwright、实际浏览器和 Node.js 版本。代码中的 Windows Chrome/120 User-Agent 只是请求标识,不能据此判断实际浏览器版本。
- 确认认证请求的 origin。观察登录跳转和失败请求;如果能访问服务器或网关日志,检查哪个服务要求客户端证书。必要时为多个已经确认需要证书的 origin 分别添加配置项。
- 检查证书材料。确认 PFX 可读、密码正确,并包含目标证书及对应私钥;再检查有效期、证书链,以及证书用途是否符合目标服务要求。PEM 测试必须同时提供证书和私钥。
- 先分别运行两张证书,再并行运行。如果单独运行也失败,先解决认证问题;如果单独成功、并行失败,再检查上下文、目录、缓存和身份验证逻辑是否被共享。
可以先用以下命令检查 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 时共享已有会话。
判断是否发生证书串用,应以服务器收到的客户端证书信息或登录后的身份为依据。现有代码和“没有生效”的描述不足以确定具体根因。
备注:内容仅供参考。
