CDN-SSL

发布时间:2026-07-08 14:18 最后更新:2026-07-08 15:04 浏览量:共 42 次浏览

我直接给你完整的脚本。这个脚本属于服务器基础设施,不放进项目代码库,你按下面的路径保存即可。

保存路径

/www/server/panel/script/sync_cdn_ssl.php

完整脚本

<!--?php &lt;br ?--> /**
* 同步宝塔 Let's Encrypt SSL 证书到阿里云 CDN
*
* 用法:
* php sync_cdn_ssl.php # 正常执行(检测指纹,无变化则跳过)
* php sync_cdn_ssl.php --force # 强制同步(忽略指纹检测)
* php sync_cdn_ssl.php --check # 仅检测证书状态,不执行同步
*
* 退出码:
* 0 = 成功 / 无需更新
* 1 = 失败
*
* 部署位置: /www/server/panel/script/sync_cdn_ssl.php
* 日志位置: /www/server/panel/logs/cdn_ssl_sync.log
*/

// ==================== 配置区 ====================

// 阿里云 RAM 子账号 AccessKey(建议仅授权 AliyunDNSFullAccess + AliyunCDNFullAccess)
$accessKeyId = 'LTAI5tXXXXXXXXXXXXXXXX'; // TODO: 替换为你的 AccessKeyId
$accessKeySecret = 'XXXXXXXXXXXXXXXXXXXXXXXXXX'; // TODO: 替换为你的 AccessKeySecret

// CDN 加速域名
$cdnDomain = 'img.hao.hsx.ltd';

// 宝塔证书目录(网站域名应与 CDN 域名一致)
$certDir = '/www/server/panel/vhost/cert/img.hao.hsx.ltd';

// 证书文件路径
$certPubPath = $certDir . '/fullchain.pem';
$certPriPath = $certDir . '/privkey.pem';

// 状态记录文件(记录上次同步的证书指纹,避免重复调用 API)
$stateFile = '/www/server/panel/script/.cdn_ssl_sync_state';

// 日志文件
$logFile = '/www/server/panel/logs/cdn_ssl_sync.log';

// 阿里云 CDN API 端点
$apiEndpoint = 'https://cdn.aliyuncs.com/';

// ==================== 主流程 ====================

$force = in_array('--force', $argv ?? [], true);
$check = in_array('--check', $argv ?? [], true);

logMsg("====== 同步任务开始 ======");
logMsg("CDN 域名: {$cdnDomain}");
logMsg("证书目录: {$certDir}");
logMsg("运行模式: " . ($check ? '仅检测' : ($force ? '强制同步' : '常规同步')));

// 1. 检查证书文件是否存在
if (!file_exists($certPubPath) || !file_exists($certPriPath)) {
logMsg("[ERROR] 证书文件不存在: {$certPubPath} 或 {$certPriPath}", 'ERROR');
exit(1);
}

// 2. 读取证书内容
$sslPub = file_get_contents($certPubPath);
$sslPri = file_get_contents($certPriPath);
if ($sslPub === false || $sslPri === false || $sslPub === '' || $sslPri === '') {
logMsg("[ERROR] 证书文件读取失败或为空", 'ERROR');
exit(1);
}

// 3. 解析证书信息
$x509 = openssl_x509_parse($sslPub);
if ($x509 === false) {
logMsg("[ERROR] 证书解析失败,可能文件损坏", 'ERROR');
exit(1);
}

$validFrom = $x509['validFrom_time_t'];
$validTo = $x509['validTo_time_t'];
$daysLeft = intval(($validTo - time()) / 86400);
$issuer = $x509['issuer']['O'] ?? 'Unknown';
$subject = $x509['subject']['CN'] ?? 'Unknown';

logMsg("证书 Subject: {$subject}");
logMsg("签发机构: {$issuer}");
logMsg("有效期: " . date('Y-m-d H:i:s', $validFrom) . " ~ " . date('Y-m-d H:i:s', $validTo));
logMsg("剩余天数: {$daysLeft}");

// 4. 证书过期检查
if ($validTo &lt; time()) { logMsg("[ERROR] 证书已过期,不同步到 CDN", 'ERROR'); exit(1); } // 5. 仅检测模式 if ($check) { logMsg("仅检测模式,不执行同步"); logMsg("====== 检测完成 ======\n"); exit(0); } // 6. 指纹检测(避免重复同步) $fp = md5($sslPub . $sslPri); if (!$force &amp;&amp; file_exists($stateFile) &amp;&amp; trim(file_get_contents($stateFile)) === $fp) { logMsg("证书指纹未变化,无需同步"); logMsg("====== 同步跳过 ======\n"); exit(0); } // 7. 调用阿里云 CDN API 部署证书 logMsg("开始调用阿里云 CDN API 部署证书..."); $result = callAliyunCdnApi($accessKeyId, $accessKeySecret, $cdnDomain, $sslPub, $sslPri, $apiEndpoint); if (!$result['success']) { logMsg("[ERROR] 阿里云 API 调用失败: " . $result['message'], 'ERROR'); logMsg("[ERROR] 原始响应: " . $result['response'], 'ERROR'); logMsg("====== 同步失败 ======\n"); exit(1); } // 8. 记录指纹 $writeResult = file_put_contents($stateFile, $fp); if ($writeResult === false) { logMsg("[WARN] 指纹记录写入失败,但不影响本次同步结果", 'WARN'); } logMsg("[SUCCESS] 证书已成功同步到阿里云 CDN ({$cdnDomain})"); logMsg("====== 同步成功 ======\n"); exit(0); // ==================== 函数定义 ==================== /** * 调用阿里云 CDN API: SetCdnDomainSSLCertificate */ function callAliyunCdnApi($accessKeyId, $accessKeySecret, $domain, $sslPub, $sslPri, $endpoint) { // 公共参数 $params = [ 'Action' =&gt; 'SetCdnDomainSSLCertificate',
'DomainName' =&gt; $domain,
'SSLProtocol' =&gt; 'https',
'SSLPub' =&gt; $sslPub,
'SSLPri' =&gt; $sslPri,
'Format' =&gt; 'JSON',
'Version' =&gt; '2018-05-10',
'AccessKeyId' =&gt; $accessKeyId,
'SignatureMethod' =&gt; 'HMAC-SHA1',
'Timestamp' =&gt; gmdate('Y-m-d\TH:i:s\Z'),
'SignatureVersion' =&gt; '1.0',
'SignatureNonce' =&gt; uniqid('ssl_', true),
];

// 计算签名
$params['Signature'] = signRpcRequest($params, $accessKeySecret);

// 发送 POST 请求
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST =&gt; true,
CURLOPT_POSTFIELDS =&gt; http_build_query($params),
CURLOPT_RETURNTRANSFER =&gt; true,
CURLOPT_TIMEOUT =&gt; 30,
CURLOPT_CONNECTTIMEOUT =&gt; 10,
CURLOPT_HTTPHEADER =&gt; [
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
],
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);

if ($response === false) {
return [
'success' =&gt; false,
'message' =&gt; 'HTTP 请求失败: ' . $curlErr,
'response' =&gt; '',
];
}

$data = json_decode($response, true);

// 阿里云成功响应: HTTP 200 且无 Code 字段
if ($httpCode === 200 &amp;&amp; !isset($data['Code'])) {
return [
'success' =&gt; true,
'message' =&gt; 'OK',
'response' =&gt; $response,
];
}

$errMsg = $data['Message'] ?? $data['ErrorMessage'] ?? '未知错误';
$errCode = $data['Code'] ?? 'Unknown';
return [
'success' =&gt; false,
'message' =&gt; "[{$errCode}] {$errMsg}",
'response' =&gt; $response,
];
}

/**
* 阿里云 RPC API 签名算法
* 参考: https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature
*/
function signRpcRequest(array $params, $accessKeySecret) {
// 1. 按参数名升序排序
ksort($params);

// 2. 构造规范化请求字符串
$canonicalized = '';
foreach ($params as $k =&gt; $v) {
$canonicalized .= '&amp;' . rpcEncode($k) . '=' . rpcEncode($v);
}
$canonicalized = substr($canonicalized, 1);

// 3. 构造待签名字符串
$stringToSign = 'POST&amp;' . rpcEncode('/') . '&amp;' . rpcEncode($canonicalized);

// 4. 计算签名
$signature = base64_encode(hash_hmac('sha1', $stringToSign, $accessKeySecret . '&amp;', true));

return $signature;
}

/**
* 阿里云 RPC API 特殊 URL 编码
* - 使用 rawurlencode
* - + → %20
* - * → %2A
* - %7E → ~
*/
function rpcEncode($s) {
return str_replace(['+', '*', '%7E'], ['%20', '%2A', '~'], rawurlencode($s));
}

/**
* 记录日志(同时输出到 stdout 和日志文件)
*/
function logMsg($msg, $level = 'INFO') {
$line = '[' . date('Y-m-d H:i:s') . '] [' . $level . '] ' . $msg;

// 输出到 stdout
echo $line . "\n";

// 追加到日志文件
global $logFile;
$dir = dirname($logFile);
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
@file_put_contents($logFile, $line . "\n", FILE_APPEND);
}

使用方法

1. 保存脚本

将上面的脚本保存到服务器:

# 在服务器上执行
mkdir -p /www/server/panel/script
nano /www/server/panel/script/sync_cdn_ssl.php
# 粘贴脚本内容,保存退出

2. 填写配置

编辑脚本,修改这两行:

$accessKeyId = 'LTAI5tXXXXXXXXXXXXXXXX'; // 你的 AccessKeyId
$accessKeySecret = 'XXXXXXXXXXXXXXXXXXXXXXXXXX'; // 你的 AccessKeySecret

3. 设置权限

chmod 600 /www/server/panel/script/sync_cdn_ssl.php
chown root:root /www/server/panel/script/sync_cdn_ssl.php

4. 测试脚本

# 先检测证书状态(不会执行同步)
/usr/bin/php /www/server/panel/script/sync_cdn_ssl.php --check

# 正式同步
/usr/bin/php /www/server/panel/script/sync_cdn_ssl.php

# 强制重新同步(忽略指纹)
/usr/bin/php /www/server/panel/script/sync_cdn_ssl.php --force

5. 验证 CDN 证书

# 查看 CDN 证书签发方(应显示 Let's Encrypt)
curl -vI https://img.hao.hsx.ltd 2&gt;&amp;1 | grep -E "issuer|subject"

脚本特性说明

特性 说明
指纹检测 用 MD5 记录上次同步的证书指纹,证书未变化时秒级跳过,不浪费 API 调用
强制模式 --force 参数可忽略指纹强制同步(用于排错)
检测模式 --check 参数仅查看证书状态,不执行同步
过期保护 证书已过期则不同步,避免把坏证书推到 CDN
双重日志 同时输出到 stdout(便于计划任务邮件)和文件(便于回溯)
完整签名 严格按阿里云 RPC API V1 签名规范实现,含特殊编码
退出码 0/1 标准退出码,便于在 Shell 脚本中判断成功失败

配套计划任务

在宝塔 → 计划任务 中添加:

任务 1:每周一 3:00 续签并同步

#!/bin/bash
/www/server/panel/pyenv/bin/python -u /www/server/panel/class/acme_v2.py --renew=1 &gt;&gt; /www/server/panel/logs/ssl_renew.log 2&gt;&amp;1
sleep 10
/usr/bin/php /www/server/panel/script/sync_cdn_ssl.php &gt;&gt; /www/server/panel/logs/cdn_ssl_sync.log 2&gt;&amp;1

任务 2:每天 4:00 兜底检测同步

#!/bin/bash
/usr/bin/php /www/server/panel/script/sync_cdn_ssl.php &gt;&gt; /www/server/panel/logs/cdn_ssl_sync.log 2&gt;&amp;1

配置好后,证书的续签和 CDN 同步将完全自动化,90 天周期内无需人工干预。

相关文章

0708做的安全检测
我把此套程序,部署在阿里云的 2核 4G 5M的ECS上,部署OSS和REDIS。请问,这台服务器可以带动200家门店使用吗?
小黑内网服务器影射的NGINX规则
电子台账相关说明
利润测算软件的配置说明
密码保护:联想 System X3850 X6 阵列卡组建一例