首页
/ Puppeteer HTTPResponse.securityDetails() 深度解析:读取 HTTPS 响应的 TLS 证书安全详情

Puppeteer HTTPResponse.securityDetails() 深度解析:读取 HTTPS 响应的 TLS 证书安全详情

2026-09-07 19:27:47作者:平淮齐Percy

导读

HTTPResponse.securityDetails() 是 Puppeteer 中用于判断并读取某个 HTTP 响应是否经由**安全连接(HTTPS/TLS)**送达的 API:当响应建立在安全通道之上时,它返回一个封装了服务器证书完整信息的 SecurityDetails 对象;当响应来自普通 HTTP 连接时则返回 null。本文以官方 API 文档为主体,结合仓库中 CDP 与 WebDriver BiDi 两套协议的底层实现与测试用例,讲解该方法的签名、返回结构、六个证书信息字段的真实含义与取值来源,并给出可复制的实战代码。读完你可以用十行以内的代码在任何 Puppeteer 页面中审计 TLS 版本、证书颁发者、有效期与域名列表。

securityDetails() 方法签名与语义

HTTPResponse 类文档 中,securityDetails() 被描述为:

Returns SecurityDetails if the response was received over the secure connection, or null otherwise.

其声明位于抽象基类 packages/puppeteer-core/src/api/HTTPResponse.ts

abstract class HTTPResponse {
  abstract securityDetails(): SecurityDetails | null;
}

三个需要理解的关键点:

  1. 方法返回两类值:安全连接下的 SecurityDetails 对象;非安全连接下的 null。因此调用端应始终做好空值判断(详见下文实战示例)。
  2. HTTPResponse 是抽象类:它的构造函数被标记为 @internal,第三方代码不能直接 new HTTPResponse(),实际拿到的是具体协议实现——Chromium 协议下是 CdpHTTPResponse,Firefox/WebDriver BiDi 下是 BidiHTTPResponse
  3. securityDetails() 是抽象方法:真正行为由两条实现路径各自给出(见下文"底层原理")。

该文档还提供了完整的方法签名与返回类型表格。一个 HTTPResponse 还拥有 url()status()ok()headers()request()frame() 等配套方法(见 HTTPResponse 类文档),securityDetails() 通常与 url()status() 组合使用,用于在 page.on('response') 监听中对所有网络资源做安全审计。

返回对象 SecurityDetails 的六个字段

当返回非 null 时,拿到的是一个 SecurityDetails 类 实例。官方文档说明其"代表了通过安全连接收到的响应的安全细节",并给出了六个公开读取方法:

方法 返回类型 含义 对应文档
issuer() string 证书颁发者(Issuer)名称 issuer()
subjectName() string 证书颁发给的对象(Subject)名称 subjectName()
subjectAlternativeNames() string[] 证书的主题备用名称(SAN)列表 subjectAlternativeNames()
validFrom() number 证书有效期起点的 Unix 时间戳 validFrom()
validTo() number 证书有效期终点的 Unix 时间戳 validTo()
protocol() string 使用的安全协议,如 "TLS 1.2" protocol()

时间戳需要自行换算

validFrom()validTo() 返回的是秒级 Unix 时间戳(并非毫秒)。若需转成可读时间,需要乘以 1000 后交给 Date

const details = response.securityDetails();
if (details) {
  console.log('有效期起始:', new Date(details.validFrom() * 1000).toISOString());
  console.log('有效期截止:', new Date(details.validTo() * 1000).toISOString());
}

测试数据可佐证该换算的必要性:在仓库的 HTTPS 测试证书中,validFrom() 返回 1589357069(约 2020 年 5 月),validTo() 返回 1904717069(约 2030 年 5 月,间隔恰为 10 年,见 acceptInsecureCerts.test.ts)。

构造方式被内部化

文档专门强调:SecurityDetails 的构造函数标记为 internal,第三方代码不应直接调用其构造函数或继承该类。也就是说,SecurityDetails 只能作为 securityDetails() 的返回值来消费,这一点从实现上也能印证——它的六个私有字段全部来自浏览器底层协议的安全载荷(见下文源码分析)。

从源码看 SecurityDetails 的数据来源

packages/puppeteer-core/src/common/SecurityDetails.ts 是唯一的 SecurityDetails 实现文件,可完整看到字段映射关系:

// packages/puppeteer-core/src/common/SecurityDetails.ts
constructor(securityPayload: Protocol.Network.SecurityDetails) {
  this.#subjectName = securityPayload.subjectName;
  this.#issuer = securityPayload.issuer;
  this.#validFrom = securityPayload.validFrom;
  this.#validTo = securityPayload.validTo;
  this.#protocol = securityPayload.protocol;
  this.#sanList = securityPayload.sanList;
}

可以推断出其设计结构:

  • 六个公开方法背后是六个使用 ES 私有字段(#subjectName#issuer#validFrom#validTo#protocol#sanList)保存的不可变快照,类本身不暴露任何 setter,一旦由协议事件构造完毕便只读;
  • 数据源是 devtools-protocolProtocol.Network.SecurityDetails 载荷——这正是 CDP(Chrome DevTools Protocol)网络域在 Network.responseReceived 等事件中携带的 TLS 握手结果;
  • protocol() 对应浏览器实测得到的 TLS 版本字符串(如 "TLS 1.2""TLS 1.3"),sanList 则是证书中 Subject Alternative Name 扩展的域名数组。

CDP 路径:构造时机决定 null 语义

Chromium 侧的实现在 packages/puppeteer-core/src/cdp/HTTPResponse.ts

this.#securityDetails = responsePayload.securityDetails
  ? new SecurityDetails(responsePayload.securityDetails)
  : null;

随后由 securityDetails() 直接返回该字段。这段代码揭示了 null 的真正来源:当且仅当 CDP 的 Network.Response 载荷中没有 securityDetails 字段时(即连接并非 TLS),该值为 null。也就是说,判断逻辑发生在浏览器内核,Puppeteer 只是如实转发。

BiDi 路径:一个值得注意的差异

Firefox(WebDriver BiDi)侧的实现在 packages/puppeteer-core/src/bidi/HTTPResponse.ts

override securityDetails(): SecurityDetails | null {
  if (!this.#cdpSupported) {
    throw new UnsupportedOperation();
  }
  return this.#securityDetails ?? null;
}

这段代码有两个与 CDP 实现显著不同的点,值得开发者注意:

  1. 依赖非标准扩展字段:BiDi 本身并未把 TLS 安全细节纳入标准网络事件,Puppeteer 从响应数据的 goog:securityDetails 非标准属性中读取,且只有在该连接同时启用了 CDP 支持cdpSupported)时才会构建 SecurityDetails
  2. 能力不足时抛异常:当连接不支持 CDP(例如纯 BiDi 连接)时,调用会抛出 UnsupportedOperation 而不是返回 null。因此跨浏览器代码若想兼容 Firefox 的纯 BiDi 场景,需对异常做兜底,仅凭 === null 判断"非安全连接"并不总是成立。

实战示例:完整读取一次 HTTPS 请求的证书详情

下面是一个可直接运行的最小完整示例,覆盖了"安全连接读取证书 + 普通 HTTP 返回 null + 非安全连接判空"三种分支:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// 方案 A:监听页面内所有响应
page.on('response', response => {
  const details = response.securityDetails();
  if (details === null) {
    // 该响应未走安全连接(可能是 http:// 资源或非 TLS 流量)
    console.log(`[非安全] ${response.url()} status=${response.status()}`);
    return;
  }
  console.log(`[TLS] ${response.url()}`);
  console.log('  协议:', details.protocol());
  console.log('  颁发者:', details.issuer());
  console.log('  主体:', details.subjectName());
  console.log('  SANs:', details.subjectAlternativeNames().join(', '));
  console.log('  有效期:', details.validFrom(), '→', details.validTo());
});

// 方案 B:直接拿到主文档的响应并判断
const response = await page.goto('https://example.com', {
  waitUntil: 'networkidle0',
});
const securityDetails = response!.securityDetails();

if (securityDetails) {
  const protocol = securityDetails.protocol();
  const subject = securityDetails.subjectName();

  // 可在此追加自定义校验,例如:
  // 1) 要求 TLS 1.2 及以上
  if (!protocol.startsWith('TLS 1.2') && !protocol.startsWith('TLS 1.3')) {
    console.warn(`TLS 版本过低: ${protocol}`);
  }
  // 2) 校验证书域名列表是否包含目标主机
  const host = new URL(response!.url()).hostname;
  const matches = securityDetails.subjectAlternativeNames().some(
    name => name === host || name === `*.${host.split('.').slice(1).join('.')}`,
  );
  console.log(`证书是否覆盖 ${host}:`, matches);
}

await browser.close();

注意 page.goto 失败(如证书校验失败、网络错误)时返回的是 null,所以上例中对 response 用了非空断言;实际代码建议先判空再调用 securityDetails()。此外,securityDetails() 本身是同步方法,不需要 await

结合测试理解边界行为

仓库中关于该方法的行为约定都有对应测试支撑,可作为"行为契约"参考。

测试 1:非安全请求返回 null

test/src/acceptInsecureCerts.test.ts 中,对普通 HTTP 服务器发起导航后断言:

const response = (await page.goto(server.EMPTY_PAGE))!;
expect(response.securityDetails()).toBe(null);

这验证了文档所述"非安全连接返回 null"。

测试 2:安全连接返回完整证书字段

test/src/acceptInsecureCerts.test.ts 用自签名 HTTPS 服务器验证了全部六个字段,且把 securityDetails.protocol() 与 Node 端 TLSSocket.getProtocol() 的实测结果做了一致性比对:

const [serverRequest, response] = await Promise.all([
  httpsServer.waitForRequest('/empty.html'),
  page.goto(httpsServer.EMPTY_PAGE),
]);
const securityDetails = response!.securityDetails()!;
expect(securityDetails.issuer()).toBe('puppeteer-tests');
expect(securityDetails.subjectName()).toBe('puppeteer-tests');
expect(securityDetails.subjectAlternativeNames()).toEqual([
  'www.puppeteer-tests.test',
  'www.puppeteer-tests-1.test',
]);
// protocol 与真实 TLS 会话比对
const protocol = (serverRequest.socket as TLSSocket).getProtocol()!.replace('v', ' ');
expect(securityDetails.protocol()).toBe(protocol);

这组断言说明:issuer/subjectName 在此返回证书主题字符串,protocol() 返回的是形如 TLS 1.2 的字符串(Node 的 getProtocol() 返回 TLSv1.2,故测试中做了 v → 空格替换)。

测试 3:网络重定向同样携带安全详情

同一测试文件还断言,HTTPS 下发生 302 重定向时,中间重定向响应同样能取到 SecurityDetails(test/src/acceptInsecureCerts.test.ts):

httpsServer.setRedirect('/plzredirect', '/empty.html');
// ...收集全部 response 事件...
expect(responses).toHaveLength(2);
expect(responses[0]!.status()).toBe(302);
const securityDetails = responses[0]!.securityDetails()!;
expect(securityDetails.protocol()).toBe(protocol);

这意味着在 page.on('response') 中做安全审计时,重定向链上的每一跳响应都值得检查,不能只看最终响应。

测试 4:acceptInsecureCerts 与远程连接

test/src/launcher.test.ts 则展示了另一种常见场景:在 puppeteer.connect({ acceptInsecureCerts: true }) 建立的远程连接中,访问自签名 HTTPS 站点后 securityDetails() 有值且 protocol() 与真实会话一致。这与启动参数 acceptInsecureCerts(在 BrowserLauncher.ts 中透传至浏览器偏好)配合使用——该参数决定是否信任自签名证书,而 securityDetails() 决定连接建立后能否读出证书信息,二者是独立的两个维度。

使用要点小结

  • 判空优先:对每个响应先判 null,再访问证书字段;null 表示连接未使用 TLS(CDP 路径),此时任何字段都不可用。
  • 只读对象SecurityDetails 无公开构造与修改入口,字段本质是构造时的快照,适用于审计与日志。
  • 时间戳单位validFrom()/validTo() 是秒级 Unix 时间戳,转 Date* 1000
  • 跨协议注意:CDP(Chromium)路径稳定返回详情或 null;BiDi(Firefox 纯 WebDriver BiDi)路径在无 CDP 支持时调用 securityDetails() 会抛 UnsupportedOperation
  • 与证书校验解耦:能否读取到 SecurityDetails 不等于证书可信;自签名/过期证书是否放行由 acceptInsecureCerts 等启动与连接参数控制,而 securityDetails() 只负责把内核已完成的 TLS 握手结果原样暴露给你,供上层自行决策。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388