本文目录导读:

在 PHP 中实现证书自动轮换,主要有以下几种方案,我会从简单到复杂逐一介绍:
使用文件系统监控(最简单)
定期检查证书文件是否更新:
class CertificateRotator {
private $certPath;
private $keyPath;
private $lastModified = [];
public function __construct($certPath, $keyPath) {
$this->certPath = $certPath;
$this->keyPath = $keyPath;
$this->loadLastModified();
}
public function checkAndReload() {
$certModified = filemtime($this->certPath);
$keyModified = filemtime($this->keyPath);
if ($this->lastModified['cert'] != $certModified ||
$this->lastModified['key'] != $keyModified) {
// 重新加载证书
$this->reloadCertificate();
// 更新记录
$this->lastModified = [
'cert' => $certModified,
'key' => $keyModified
];
return true;
}
return false;
}
private function loadLastModified() {
$this->lastModified = [
'cert' = filemtime($this->certPath),
'key' = filemtime($this->keyPath)
];
}
private function reloadCertificate() {
// 重新加载证书的逻辑
// 重新创建SSL上下文、更新内存中的证书等
}
}
// 使用示例
$rotator = new CertificateRotator('/path/to/cert.pem', '/path/to/key.pem');
// 在每次请求或定时任务中调用
if ($rotator->checkAndReload()) {
echo "证书已更新";
}
使用 crontab 定时任务
创建一个证书轮换脚本:
// rotate_cert.php
<?php
class SwooleCertRotator {
public function rotate() {
// 1. 获取新证书
$newCert = $this->fetchNewCertificate();
if (!$newCert) {
throw new Exception('无法获取新证书');
}
// 2. 更新证书文件
file_put_contents('/path/to/cert.pem', $newCert['cert']);
file_put_contents('/path/to/key.pem', $newCert['key']);
// 3. 如果是Swoole,发送重载信号
$this->reloadSwoole();
// 4. 如果是Workerman,发送重载信号
$this->reloadWorkerman();
// 5. 记录日志
$this->logRotation();
}
private function fetchNewCertificate() {
// 从Let's Encrypt或其他CA获取新证书
// 示例:使用certbot或ACME协议
$result = shell_exec('certbot renew --dry-run 2>&1');
if (strpos($result, 'success') !== false) {
return [
'cert' => file_get_contents('/etc/letsencrypt/live/example.com/fullchain.pem'),
'key' => file_get_contents('/etc/letsencrypt/live/example.com/privkey.pem')
];
}
return null;
}
private function reloadSwoole() {
// Swoole 4.5+
if (function_exists('swoole_get_local_ip')) {
$manager = new Swoole\Server('0.0.0.0', 9501);
$manager->reload();
}
}
private function reloadWorkerman() {
// 发送信号给Workerman主进程
$pid = file_get_contents('/var/run/workerman.pid');
posix_kill(intval($pid), SIGUSR1);
}
private function logRotation() {
$log = date('Y-m-d H:i:s') . " - 证书已轮换\n";
file_put_contents('/var/log/cert_rotation.log', $log, FILE_APPEND);
}
}
在 PHP-FPM 中使用动态证书加载
对于高并发的场景,更推荐使用内存缓存:
class CertificateManager {
private $cacheDir;
private $certContent;
private $keyContent;
private $expiration;
public function __construct() {
$this->cacheDir = '/tmp/cert_cache/';
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
}
public function getCertificate() {
// 检查缓存是否过期
if ($this->isCacheExpired()) {
$this->loadNewCertificate();
}
return [
'cert' => $this->certContent,
'key' => $this->keyContent
];
}
private function isCacheExpired() {
// 检查配置文件的缓存时间
$cacheFile = $this->cacheDir . 'cert.cache';
if (!file_exists($cacheFile)) {
return true;
}
$cacheTime = unserialize(file_get_contents($cacheFile))['time'];
// 如果超过2小时,认为过期
return (time() - $cacheTime > 7200);
}
private function loadNewCertificate() {
// 从配置获取新证书
$config = require 'cert_config.php';
$this->certContent = file_get_contents($config['cert_path']);
$this->keyContent = file_get_contents($config['key_path']);
$this->expiration = strtotime('+2 hours');
// 写入缓存
$this->writeToCache();
}
private function writeToCache() {
$cacheData = [
'time' => time(),
'name' => 'mypem',
'cert_content' => $this->certContent,
'key_content' => $this->keyContent
];
file_put_contents(
$this->cacheDir . 'cert.cache',
serialize($cacheData)
);
}
}
使用 Swoole/Workerman 实现热更新(生产推荐)
Swoole 4.5+ 版本:
<?php
use Swoole\Coroutine\Http\Server;
class CertificateRotator {
public function start() {
$server = new Server('0.0.0.0', 9501, SWOOLE_PROCESS);
// 设置初始证书
$server->set([
'ssl_cert_file' => '/path/to/cert.pem',
'ssl_key_file' => '/path/to/key.pem',
]);
// 开启证书轮换机制
$server->on('Start', function ($server) {
// 创建协程定期检查证书更新
go(function () {
while (true) {
usleep(3600000); // 每小时检查一次
$this->checkAndRotate($server);
}
});
});
$server->on('Request', function ($request, $response) use ($server) {
$response->end("Hello World");
});
$server->start();
}
private function checkAndRotate($server) {
// 检查证书是否需要更新
if ($this->shouldRotate()) {
// 获取新证书
$newCert = $this->getNewCertificate();
// 写入临时文件
$tmpCert = '/tmp/cert_new.pem';
$tmpKey = '/tmp/key_new.pem';
file_put_contents($tmpCert, $newCert['cert']);
file_put_contents($tmpKey, $newCert['key']);
// 更新服务器证书(Swoole 4.5+ 支持)
$server->set([
'ssl_cert_file' => $tmpCert,
'ssl_key_file' => $tmpKey,
]);
}
}
}
Workerman 版本:
<?php
require_once __DIR__ . '/workerman/autoload.php';
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
class CertRotator {
public function rotate() {
$worker = new Worker('http://0.0.0.0:443');
$worker->transport = 'ssl';
$worker->ssl = [
'local_cert' => '/path/to/cert.pem',
'local_pk' => '/path/to/key.pem',
];
// 处理更新信号
$worker->onMessage = function (TcpConnection $connection, $data) {
$connection->send('Hello World');
};
// 监听SIGUSR1信号进行证书重载
$worker->onWorkerStart = function ($worker) {
// 设置信号处理
pcntl_signal(SIGUSR1, function () use ($worker) {
// 重新加载证书
$newCert = file_get_contents('/path/to/new-cert.pem');
file_put_contents('/path/to/cert.pem', $newCert);
// Workerman 会自动检测到证书文件变化
$this->reloadCertificates($worker);
});
};
}
private function reloadCertificates($worker) {
// 重新加载SSL上下文
$worker->ssl = [
'local_cert' => '/path/to/cert.pem',
'local_pk' => '/path/to/key.pem',
];
// 通知所有连接重新握手
foreach ($worker->connections as $connection) {
$connection->destroy();
}
}
}
容器化环境中的证书轮换
如果使用 Docker/Kubernetes,可以结合 Volume 和 Sidecar 模式:
// sidecar.php - 在Kubernetes Sidecar容器中运行
class CertSync {
public function watchAndSync() {
$watchPaths = '/mnt/certs/';
// 使用inotify监听文件变化
$inotify = inotify_init();
$watch = inotify_add_watch(
$inotify,
$watchPaths,
IN_CLOSE_WRITE | IN_DELETE | IN_MOVE
);
// 事件循环
while (true) {
$events = inotify_read($inotify);
foreach ($events as $event) {
if ($event['name'] == 'cert.pem' || $event['name'] == 'key.pem') {
$this->notifyApplication($event['name']);
}
}
}
}
}
推荐的最佳实践
- 使用环境变量:不要在代码中硬编码证书路径
- 加入重试机制:更新失败时要重试或回滚
- 监控告警:监控证书过期时间
- 日志记录:记录所有证书轮换操作
- 测试环境验证:先在小流量环境验证
// 监控证书过期时间
class CertMonitor {
public function checkExpiry() {
$certPath = '/path/to/cert.pem';
$cert = openssl_x509_read(file_get_contents($certPath));
$certData = openssl_x509_parse($cert);
$expiryDate = $certData['validTo_time_t'];
$daysToExpiry = ($expiryDate - time()) / 86400;
if ($daysToExpiry < 30) {
// 发起告警,准备轮换
$this->triggerRotation();
return "证书将在 {$daysToExpiry} 天后过期";
}
return "证书正常,剩余 {$daysToExpiry} 天";
}
}
选择哪种方案取决于你的部署环境、PHP框架和性能要求,对于生产环境,推荐结合方案3和4,使用内存缓存配合优雅重启机制。