PHP实现网站SSL证书信息获取与解析:监控、验证与安全实践344


在当今互联网世界,HTTPS已成为网站安全和用户信任的基石。无论是大型企业网站还是个人博客,部署SSL/TLS证书都变得至关重要。作为一名专业的程序员,我们不仅需要确保网站正确配置了HTTPS,有时还需要通过编程方式获取、解析并监控其他网站或服务端的SSL证书信息,以实现证书到期预警、安全审计、域名验证或调试等目的。

本文将深入探讨如何使用PHP语言获取远程网站的SSL/TLS证书信息,并对其内容进行解析。我们将涵盖多种获取方法,从基础的socket操作到高级的cURL库,并详细讲解如何提取证书中的关键数据,例如颁发者、有效期、主题信息、备用名称(SANs)等。通过本文,您将能够构建健壮的PHP应用程序,用于自动化SSL证书管理和监控。

一、理解SSL/TLS证书基础

在深入PHP实现之前,我们首先需要对SSL/TLS证书有一个清晰的认识。SSL(Secure Sockets Layer)及其继任者TLS(Transport Layer Security)是用于在互联网上提供安全通信的加密协议。网站的SSL/TLS证书是数字身份的证明,它由受信任的证书颁发机构(CA)签发,用于验证服务器的身份并将公共密钥与该身份绑定。

一个标准的SSL/TLS证书通常遵循X.509标准,包含以下核心信息:
主题(Subject):证书所有者的信息,包括通用名称(Common Name, CN),通常是域名。
颁发者(Issuer):签发该证书的CA的信息。
有效期(Validity Period):证书的起始日期和到期日期。
公钥(Public Key):与服务器私钥配对的公钥,用于加密通信。
签名算法(Signature Algorithm):CA用于签发证书的算法。
指纹(Fingerprint):证书的唯一哈希值。
主题备用名称(Subject Alternative Names, SANs):除了通用名称外,证书可以绑定到多个域名或IP地址。

获取这些信息对于证书管理至关重要,例如检查证书是否即将过期,验证证书是否由受信任的CA颁发,或者确认证书是否覆盖了所有预期的域名。

二、PHP获取SSL证书信息的核心方法

PHP提供了多种机制来与远程服务器建立连接并获取SSL证书信息。我们将重点介绍以下三种主要方法:

1. 使用 `stream_socket_client` 和 `stream_context_create` (推荐)


这是获取原始SSL证书信息最直接和灵活的方法。通过PHP的流上下文选项,我们可以在建立TLS连接时捕获对等方的证书。

```php
<?php
function get_ssl_certificate_info_via_stream(string $hostname, int $port = 443): ?array
{
$contextOptions = [
'ssl' => [
'capture_peer_cert' => true, // 捕获对等方的证书
'verify_peer' => false, // 在获取证书信息时,通常不需要严格验证证书链
'verify_peer_name' => false, // 同上,可根据需求设置为true
]
];
$context = stream_context_create($contextOptions);
$socket = @stream_socket_client(
"ssl://{$hostname}:{$port}",
$errno,
$errstr,
30, // 超时时间
STREAM_CLIENT_CONNECT,
$context
);
if (!$socket) {
// echo "无法连接到 {$hostname}:{$port} - 错误: {$errstr} ({$errno})";
return null;
}
$params = stream_context_get_params($socket);
fclose($socket); // 连接成功后即可关闭,我们只需要证书信息
if (isset($params['options']['ssl']['peer_certificate'])) {
$certResource = $params['options']['ssl']['peer_certificate'];
$certificateInfo = openssl_x509_parse($certResource);
return $certificateInfo;
}
return null;
}
// 示例用法
$hostname = ''; // 替换为您想查询的域名
$certificateData = get_ssl_certificate_info_via_stream($hostname);
if ($certificateData) {
echo "<p>--- 从 {$hostname} 获取的证书信息 (Stream 方法) ---</p>";
echo "<p>颁发者: " . htmlspecialchars($certificateData['issuer']['CN'] ?? 'N/A') . "</p>";
echo "<p>主题: " . htmlspecialchars($certificateData['subject']['CN'] ?? 'N/A') . "</p>";
echo "<p>有效期从: " . date('Y-m-d H:i:s', $certificateData['validFrom_time_t']) . "</p>";
echo "<p>有效期至: " . date('Y-m-d H:i:s', $certificateData['validTo_time_t']) . "</p>";

if (isset($certificateData['extensions']['subjectAltName'])) {
echo "<p>主题备用名称 (SANs): " . htmlspecialchars($certificateData['extensions']['subjectAltName']) . "</p>";
}
// print_r($certificateData); // 打印所有解析出的证书信息
} else {
echo "<p>无法获取 {$hostname} 的证书信息。</p>";
}
?>
```

解释:
`stream_context_create`:用于创建流上下文资源,我们可以在其中设置SSL选项。
`capture_peer_cert`:设置为`true`时,PHP会在SSL握手完成后将远程服务器的证书存储到上下文参数中。
`verify_peer` 和 `verify_peer_name`:在仅获取证书内容时,可以设置为`false`以避免因为证书链问题而阻止连接(但这不推荐用于生产环境中的实际安全验证)。如果需要验证证书的有效性,应设置为`true`并提供CA证书路径。
`stream_socket_client`:用于建立一个客户端socket连接。`ssl://`前缀表示使用SSL/TLS协议。
`stream_context_get_params`:从已建立的流中获取上下文参数,其中就包含了捕获到的证书资源。
`openssl_x509_parse`:这是解析证书内容的关键函数,它将证书资源解析成一个关联数组,方便我们访问各项信息。

2. 使用 cURL (适用于HTTP/HTTPS请求,并获取证书信息)


cURL是PHP中最强大的HTTP客户端库之一,它也提供了获取SSL证书信息的功能。如果您的目标是进行完整的HTTPS请求并同时获取证书信息,cURL是一个不错的选择。

```php
<?php
function get_ssl_certificate_info_via_curl(string $url): ?array
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 不直接输出响应
curl_setopt($ch, CURLOPT_HEADER, false); // 不返回HTTP头部
curl_setopt($ch, CURLOPT_NOBODY, true); // 只请求头部,不下载body
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 验证对等方的证书
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // 验证CN或SANs
curl_setopt($ch, CURLOPT_CERTINFO, true); // 请求证书信息
// 可选: 设置CA证书,如果您的系统CA列表不全或需要自定义CA
// curl_setopt($ch, CURLOPT_CAINFO, '/path/to/your/');
$result = curl_exec($ch);
if (curl_errno($ch)) {
// echo "cURL 错误: " . curl_error($ch) . "";
curl_close($ch);
return null;
}
$info = curl_getinfo($ch);
curl_close($ch);
if (isset($info['certinfo']) && !empty($info['certinfo'][0]['Cert'])) {
// cURL返回的是PEM格式的证书字符串,需要解析
$certPem = $info['certinfo'][0]['Cert'];
$certResource = openssl_x509_read($certPem);
if ($certResource) {
return openssl_x509_parse($certResource);
}
}
return null;
}
// 示例用法
$url = ''; // 替换为您想查询的URL
$certificateData = get_ssl_certificate_info_via_curl($url);
if ($certificateData) {
echo "<p>--- 从 {$url} 获取的证书信息 (cURL 方法) ---</p>";
echo "<p>颁发者: " . htmlspecialchars($certificateData['issuer']['CN'] ?? 'N/A') . "</p>";
echo "<p>主题: " . htmlspecialchars($certificateData['subject']['CN'] ?? 'N/A') . "</p>";
echo "<p>有效期从: " . date('Y-m-d H:i:s', $certificateData['validFrom_time_t']) . "</p>";
echo "<p>有效期至: " . date('Y-m-d H:i:s', $certificateData['validTo_time_t']) . "</p>";
if (isset($certificateData['extensions']['subjectAltName'])) {
echo "<p>主题备用名称 (SANs): " . htmlspecialchars($certificateData['extensions']['subjectAltName']) . "</p>";
}
// print_r($certificateData);
} else {
echo "<p>无法获取 {$url} 的证书信息。</p>";
}
?>
```

解释:
`CURLOPT_CERTINFO`:设置为`true`时,cURL会在`curl_getinfo`结果中返回一个`certinfo`数组,其中包含PEM格式的证书链信息。
`CURLOPT_SSL_VERIFYPEER` 和 `CURLOPT_SSL_VERIFYHOST`:建议在cURL中保持`true`和`2`,以确保进行完整的SSL验证,这更符合实际应用场景中的安全性要求。
`openssl_x509_read`:由于cURL返回的是PEM字符串,我们需要先将其转换为证书资源,然后才能使用`openssl_x509_parse`。

3. 执行外部 `openssl` 命令 (不推荐作为主要方法,但功能强大)


虽然PHP内置了OpenSSL扩展,但在某些复杂情况下,或者您想直接利用系统上强大的`openssl`命令行工具时,也可以通过`shell_exec`或`proc_open`等函数来执行外部命令。但这种方法通常伴随着性能开销和安全风险(需要确保输入参数安全)。

```php
<?php
function get_ssl_certificate_info_via_shell(string $hostname, int $port = 443): ?array
{
// 注意:shell_exec 存在安全风险,请确保 $hostname 是经过严格验证的
// 此处使用 /dev/null 来避免 hangs
$command = "echo | openssl s_client -connect " . escapeshellarg($hostname) . ":" . escapeshellarg((string)$port) . " 2>/dev/null | openssl x509 -noout -text";
$output = shell_exec($command);
if (empty($output)) {
return null;
}
// openssl x509 -text 的输出是人类可读的,需要更复杂的解析逻辑
// 为了简化,我们只提取部分关键信息
$data = [];
if (preg_match('/Subject:.*?CN=([^,]+)/', $output, $matches)) {
$data['subject']['CN'] = trim($matches[1]);
}
if (preg_match('/Issuer:.*?CN=([^,]+)/', $output, $matches)) {
$data['issuer']['CN'] = trim($matches[1]);
}
if (preg_match('/Not Before:.*?([A-Za-z]{3}\s+\d+\s+\d{2}:d{2}:d{2}\s+\d{4}\s+GMT)/', $output, $matches)) {
$data['validFrom_time_t'] = strtotime($matches[1]);
}
if (preg_match('/Not After :.*?([A-Za-z]{3}\s+\d+\s+\d{2}:d{2}:d{2}\s+\d{4}\s+GMT)/', $output, $matches)) {
$data['validTo_time_t'] = strtotime($matches[1]);
}
if (preg_match('/X509v3 Subject Alternative Name:s*(.*)/', $output, $matches)) {
$data['extensions']['subjectAltName'] = trim($matches[1]);
}
return $data;
}
// 示例用法
$hostname = ''; // 替换为您想查询的域名
$certificateData = get_ssl_certificate_info_via_shell($hostname);
if ($certificateData) {
echo "<p>--- 从 {$hostname} 获取的证书信息 (Shell 方法) ---</p>";
echo "<p>颁发者: " . htmlspecialchars($certificateData['issuer']['CN'] ?? 'N/A') . "</p>";
echo "<p>主题: " . htmlspecialchars($certificateData['subject']['CN'] ?? 'N/A') . "</p>";
echo "<p>有效期从: " . date('Y-m-d H:i:s', $certificateData['validFrom_time_t'] ?? 0) . "</p>";
echo "<p>有效期至: " . date('Y-m-d H:i:s', $certificateData['validTo_time_t'] ?? 0) . "</p>";
if (isset($certificateData['extensions']['subjectAltName'])) {
echo "<p>主题备用名称 (SANs): " . htmlspecialchars($certificateData['extensions']['subjectAltName']) . "</p>";
}
// print_r($certificateData);
} else {
echo "<p>无法获取 {$hostname} 的证书信息。</p>";
}
?>
```

解释:
`openssl s_client -connect ...`:用于建立SSL连接并打印证书信息。
`openssl x509 -noout -text`:将前面命令输出的证书内容以可读文本格式打印出来。
`shell_exec`:执行外部命令并返回其输出。
正则解析:由于`openssl x509 -text`的输出是格式化的文本,需要使用正则表达式来提取所需信息,这比`openssl_x509_parse`要复杂和脆弱。

三、深入解析 `openssl_x509_parse()` 的输出

`openssl_x509_parse()`函数是PHP中解析X.509证书的核心。它返回一个关联数组,包含了证书的丰富信息。以下是其一些关键字段的解释和常见用法:

```php
<?php
// 假设 $certificateData 是 openssl_x509_parse() 的输出
// 1. 证书版本
$version = $certificateData['version'] + 1; // X.509 版本通常从0开始计数,所以+1
// 2. 序列号
$serialNumber = $certificateData['serialNumber'];
// 3. 颁发者 (Issuer) 信息
$issuerCN = $certificateData['issuer']['CN'] ?? 'N/A'; // 通用名称
$issuerOrg = $certificateData['issuer']['O'] ?? 'N/A'; // 组织名称
// 4. 主题 (Subject) 信息
$subjectCN = $certificateData['subject']['CN'] ?? 'N/A'; // 通用名称
$subjectOrg = $certificateData['subject']['O'] ?? 'N/A'; // 组织名称
// 5. 有效期
$validFrom = date('Y-m-d H:i:s', $certificateData['validFrom_time_t']);
$validTo = date('Y-m-d H:i:s', $certificateData['validTo_time_t']);
// 6. 扩展 (Extensions) 信息,尤其是主题备用名称 (SANs)
$subjectAltNameRaw = $certificateData['extensions']['subjectAltName'] ?? '';
$sanDomains = [];
if (!empty($subjectAltNameRaw)) {
// SANs 通常以逗号分隔,并带有前缀 (DNS:, IP Address:)
$sanEntries = explode(', ', $subjectAltNameRaw);
foreach ($sanEntries as $entry) {
if (strpos($entry, 'DNS:') === 0) {
$sanDomains[] = substr($entry, 4);
} elseif (strpos($entry, 'IP Address:') === 0) {
// 如果需要,也可以提取IP地址
// $sanDomains[] = substr($entry, 11);
}
}
}
echo "<h3>证书详细信息解析示例</h3>";
echo "<p>版本: " . htmlspecialchars($version) . "</p>";
echo "<p>序列号: " . htmlspecialchars($serialNumber) . "</p>";
echo "<p>颁发者 CN: " . htmlspecialchars($issuerCN) . "</p>";
echo "<p>颁发者 组织: " . htmlspecialchars($issuerOrg) . "</p>";
echo "<p>主题 CN: " . htmlspecialchars($subjectCN) . "</p>";
echo "<p>主题 组织: " . htmlspecialchars($subjectOrg) . "</p>";
echo "<p>有效期开始: " . htmlspecialchars($validFrom) . "</p>";
echo "<p>有效期结束: " . htmlspecialchars($validTo) . "</p>";
echo "<p>主题备用名称 (SANs): " . (empty($sanDomains) ? '无' : htmlspecialchars(implode(', ', $sanDomains))) . "</p>";
// 判断证书是否即将过期 (例如,在未来30天内)
$expiresSoon = false;
if (isset($certificateData['validTo_time_t'])) {
$expirationTimestamp = $certificateData['validTo_time_t'];
$thirtyDaysFromNow = time() + (30 * 24 * 60 * 60);
if ($expirationTimestamp < $thirtyDaysFromNow) {
$expiresSoon = true;
}
}
echo "<p>是否在30天内过期: " . ($expiresSoon ? '<strong>是</strong>' : '否') . "</p>";
?>
```

关键点:
`validFrom_time_t` 和 `validTo_time_t`:这两个字段提供了证书有效期的Unix时间戳,非常适合进行日期计算和过期检查。
`extensions['subjectAltName']`:这是获取SANs的关键。它的值通常是一个字符串,需要进一步解析以提取各个域名或IP。
`subject` 和 `issuer`:它们是数组,包含CN (通用名称), O (组织), OU (组织单位), L (城市), ST (省份), C (国家) 等信息。

四、实际应用场景

掌握了PHP获取和解析SSL证书信息的方法后,我们可以将其应用于多种实际场景:

1. 证书有效期监控与预警


这是最常见的应用。可以定期(例如每天)运行一个PHP脚本,扫描您关注的域名列表,获取它们的SSL证书信息,并检查证书的到期日期。如果某个证书在未来30天、15天或7天内即将过期,系统可以自动发送邮件、短信或Webhook通知,提醒管理员及时更新证书。

2. 验证证书是否有效及颁发者


在连接外部API或第三方服务时,除了常规的HTTPS验证,您可能还需要进一步确认所连接服务器的证书是否由特定的CA颁发,或者其通用名称是否与预期相符,以增加安全性。通过解析`issuer`字段,可以轻松实现这一点。

3. 检查域名的SANs是否覆盖


有时一个证书会绑定多个子域名或别名。通过解析`subjectAltName`,可以确认证书是否包含了所有您希望被覆盖的域名,这对于确保所有子服务都能正常使用该证书至关重要。

4. 自动化HTTPS配置验证


在部署或更新网站的HTTPS配置后,可以运行PHP脚本来自动验证证书是否正确安装、是否链完整、是否与期望的配置一致,从而减少手动检查的工作量。

5. 安全审计与报告


定期对公司内部或外部依赖的SSL证书进行审计,生成报告。例如,找出使用了不安全签名算法(如MD5)的证书,或由不受信任CA签发的证书。

五、注意事项与最佳实践
错误处理:网络连接问题、域名解析失败、SSL握手失败等都可能导致证书获取失败。在代码中应加入健壮的错误处理机制,例如使用`@`抑制符捕获错误信息,并进行相应的日志记录或重试。
超时设置:外部网络请求可能会很慢。为`stream_socket_client`或cURL设置合理的超时时间(例如30秒),避免脚本长时间阻塞。
性能考量:频繁地获取和解析证书信息会消耗网络和CPU资源。如果需要监控大量域名,考虑引入缓存机制,例如将证书信息存储到数据库或Redis中,并在下次请求时检查缓存的有效期。
安全性 (`shell_exec`):虽然`shell_exec`功能强大,但执行外部命令可能带来安全风险,尤其是当命令参数来源于用户输入时。务必对所有输入进行严格的验证和过滤,并使用`escapeshellarg()`和`escapeshellcmd()`函数。推荐优先使用PHP内置的OpenSSL扩展函数。
证书链验证:`stream_context_create`和cURL都支持证书链的验证。在生产环境中,始终将`verify_peer`和`verify_peer_name`设置为`true`,并确保您的PHP或系统配置了正确的CA证书包(例如`CURLOPT_CAINFO`)。
处理证书链:一个网站可能返回一个完整的证书链(服务器证书 -> 中间CA证书 -> 根CA证书)。`openssl_x509_parse`通常只解析服务器证书。如果需要整个链的信息,cURL的`certinfo`可能会包含多个证书,或者需要更复杂的`stream_socket_client`实现来捕获所有证书。

六、总结

通过本文,我们详细学习了如何利用PHP的`stream_socket_client`、cURL以及简单的外部命令执行来获取远程网站的SSL/TLS证书信息。我们深入探讨了`openssl_x509_parse()`函数的输出结构,并演示了如何提取证书的关键字段,如颁发者、有效期、主题和主题备用名称(SANs)。

这些技术在实际开发中具有广泛的应用前景,尤其是在构建证书监控系统、进行安全审计或自动化部署验证等方面。作为专业的程序员,熟练掌握这些技能将有助于我们更好地管理和维护网站的安全性,确保数据传输的加密和用户对服务的信任。

2025-10-16


上一篇:PHP与数据库交互:掌握数据排序(升序与降序)的艺术与实践

下一篇:PHP与MySQL文件交互:上传、存储与下载的深度解析