PHP 怎么证书自动轮换

wen PHP项目 1

本文目录导读:

PHP 怎么证书自动轮换

  1. 使用文件系统监控(最简单)
  2. 使用 crontab 定时任务
  3. 在 PHP-FPM 中使用动态证书加载
  4. 使用 Swoole/Workerman 实现热更新(生产推荐)
  5. 容器化环境中的证书轮换
  6. 推荐的最佳实践

在 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']);
                }
            }
        }
    }
}

推荐的最佳实践

  1. 使用环境变量:不要在代码中硬编码证书路径
  2. 加入重试机制:更新失败时要重试或回滚
  3. 监控告警:监控证书过期时间
  4. 日志记录:记录所有证书轮换操作
  5. 测试环境验证:先在小流量环境验证
// 监控证书过期时间
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,使用内存缓存配合优雅重启机制。

抱歉,评论功能暂时关闭!