使用方法
$wechatPayPlatformCertInstance = new \addons\unidrink\library\pay\overseas\wechat\Certificate();
try {
$platformCerts = $wechatPayPlatformCertInstance->getPlatformCerts();
if (!empty($platformCerts)) {
// 获取最新的证书
$latestCert = $platformCerts[0];
// 返回成功信息
return json([
'code' => 1,
'msg' => '平台证书生成成功',
'data' => [
'serial_no' => $latestCert['serial_no'],
'effective_time' => $latestCert['effective_time'],
'expire_time' => $latestCert['expire_time'],
'cert_saved_path' => $wechatPayPlatformCertInstance->getCertPath() . $latestCert['serial_no'] . '.pem',
]
]);
} else {
throw new Exception('未获取到平台证书');
}
} catch (Exception $e) {
// 错误处理
return json([
'code' => 0,
'msg' => '平台证书生成失败: ' . $e->getMessage(),
'data' => []
]);
}
下面是类
<?php
namespace addons\unidrink\library\pay\overseas\wechat;
use think\Exception;
use think\Log;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;
class Certificate
{
// 配置参数
private $config;
// 证书保存路径
private $certPath;
// 平台证书列表
private $platformCerts = [];
/**
* 构造函数
* @param array $config 微信支付配置
*/
public function __construct($config = [])
{
// 默认配置
$defaultConfig = [
'mch_id' => '', // 商户号
'serial_no' => '', // 商户API证书序列号
'private_key' => ROOT_PATH . 'public/cert/apiclient_key.pem', // 商户私钥内容或路径
'api_v3_key' => '', // APIv3密钥
'cert_path' => ROOT_PATH . 'public/cert/wechatpay/', // 证书保存目录
'ca_cert_path' => ROOT_PATH . 'public/cert/cacert.pem' // CA证书路径
];
$this->config = array_merge($defaultConfig, $config);
$this->certPath = $this->config['cert_path'];
// 验证APIv3密钥长度
$this->validateApiV3Key();
// 创建证书目录并确保权限
$this->prepareDirectories();
}
/**
* 准备目录并确保权限
*/
private function prepareDirectories()
{
// 创建证书目录
if (!is_dir($this->certPath)) {
if (!mkdir($this->certPath, 0755, true)) {
throw new Exception('无法创建证书目录: ' . $this->certPath);
}
}
// 检查证书目录权限
if (!is_writable($this->certPath)) {
throw new Exception('证书目录不可写: ' . $this->certPath);
}
// 确保CA证书目录可写
$caCertDir = dirname($this->config['ca_cert_path']);
if (!is_dir($caCertDir)) {
if (!mkdir($caCertDir, 0755, true)) {
throw new Exception('无法创建CA证书目录: ' . $caCertDir);
}
}
if (!is_writable($caCertDir)) {
throw new Exception('CA证书目录不可写: ' . $caCertDir);
}
}
/**
* 验证APIv3密钥长度是否正确
* APIv3密钥必须是32位字符串
* @throws Exception
*/
private function validateApiV3Key()
{
// 移除可能存在的空格
$apiV3Key = trim($this->config['api_v3_key']);
// 重新设置清理后的密钥
$this->config['api_v3_key'] = $apiV3Key;
// 检查长度
$keyLength = strlen($apiV3Key);
if ($keyLength !== 32) {
throw new Exception("APIv3密钥长度不正确,必须是32位,当前为{$keyLength}位");
}
}
/**
* 获取平台证书
* @return array 平台证书列表
*/
public function getPlatformCerts()
{
try {
// 如果有缓存的证书且未过期,直接返回
$cachedCerts = $this->getCachedCerts();
if (!empty($cachedCerts)) {
Log::info('使用缓存的平台证书,共' . count($cachedCerts) . '个');
return $cachedCerts;
}
Log::info('缓存的平台证书不存在或已过期,将从API获取');
// 从微信支付API获取证书
$this->fetchPlatformCerts();
// 缓存证书
$this->cacheCerts();
return $this->platformCerts;
} catch (Exception $e) {
Log::error('获取微信支付平台证书失败: ' . $e->getMessage() . ',堆栈信息: ' . $e->getTraceAsString());
throw new Exception('获取平台证书失败: ' . $e->getMessage());
}
}
/**
* 从微信支付API获取平台证书
*/
private function fetchPlatformCerts()
{
// 构建请求参数
$timestamp = time();
$nonce = $this->generateNonce();
$method = 'GET';
// $url = '/v3/certificates'; // 中国大陆境内
$url = '/v3/global/certificates'; // 注意:全球版API路径
$body = '';
Log::info("准备请求微信支付证书API: URL={$url}, 时间戳={$timestamp}, 随机串={$nonce}");
// 生成签名
$signature = $this->generateSignature($method, $url, $timestamp, $nonce, $body);
// 构建授权头
$token = sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$this->config['mch_id'],
$nonce,
$timestamp,
$this->config['serial_no'],
$signature
);
// 发送请求
$headers = [
'Accept: application/json',
'User-Agent: FastAdmin/WechatPay',
'Authorization: ' . $token,
];
// 根据商户号判断使用国内还是国际API
// $apiDomain = 'https://api.mch.weixin.qq.com';
$apiDomain = 'https://apihk.mch.weixin.qq.com';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiDomain . $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
// 检查私钥文件是否存在
$privateKeyPath = $this->config['private_key'];
if (!file_exists($privateKeyPath)) {
throw new Exception('私钥文件不存在: ' . $privateKeyPath);
}
// 检查私钥文件权限和内容
$this->validatePrivateKey($privateKeyPath);
// 设置SSL选项
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLKEY, $privateKeyPath);
// 获取并设置CA证书
$caCertPath = $this->getCACertPath();
if ($caCertPath && file_exists($caCertPath)) {
curl_setopt($ch, CURLOPT_CAINFO, $caCertPath);
Log::info('使用CA证书: ' . $caCertPath);
} else {
Log::warning('无法获取CA证书,临时关闭SSL验证');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$errorNumber = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
// 记录完整响应,便于调试
Log::info('微信支付平台证书API响应: HTTP状态码=' . $httpCode . ', 错误代码=' . $errorNumber . ', 错误信息=' . $error . ', 响应内容=' . $response);
if ($httpCode !== 200) {
// 特殊处理常见错误码
$errorMsg = "请求微信支付API失败,HTTP状态码: {$httpCode}";
switch ($httpCode) {
case 401:
$errorMsg .= ",可能是签名错误或商户信息不正确";
break;
case 403:
$errorMsg .= ",权限不足,可能是API未开通或IP白名单问题";
break;
case 404:
$errorMsg .= ",请求路径错误";
break;
case 500:
$errorMsg .= ",微信支付服务器内部错误";
break;
}
$errorMsg .= ",响应: {$response}";
throw new Exception($errorMsg);
}
if (empty($response)) {
throw new Exception('微信支付API返回空响应');
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('解析微信支付API响应失败: ' . json_last_error_msg() . ',原始响应: ' . $response);
}
if (!isset($data['data']) || !is_array($data['data'])) {
throw new Exception('微信支付API响应数据异常,未找到证书列表: ' . json_encode($data));
}
if (empty($data['data'])) {
throw new Exception('微信支付API返回空的证书列表');
}
// 处理证书数据前记录原始数据
Log::info('准备处理的证书数据数量: ' . count($data['data']) . ',数据: ' . json_encode($data['data']));
// 处理证书数据
$this->processCertificates($data['data']);
}
/**
* 验证私钥文件
*/
private function validatePrivateKey($privateKeyPath)
{
// 检查文件权限
if (!is_readable($privateKeyPath)) {
throw new Exception('私钥文件不可读: ' . $privateKeyPath);
}
// 检查文件大小
$fileSize = filesize($privateKeyPath);
if ($fileSize < 100) {
throw new Exception('私钥文件过小,可能不是有效的私钥: ' . $privateKeyPath);
}
// 检查私钥文件格式
$privateKeyContent = file_get_contents($privateKeyPath);
if (strpos($privateKeyContent, '-----BEGIN PRIVATE KEY-----') === false ||
strpos($privateKeyContent, '-----END PRIVATE KEY-----') === false) {
throw new Exception('私钥文件格式不正确,不是有效的PEM格式私钥');
}
// 尝试加载私钥验证有效性
$key = openssl_pkey_get_private($privateKeyContent);
if (!$key) {
throw new Exception('私钥无效,无法加载: ' . openssl_error_string());
}
// 检查密钥类型和长度
$keyDetails = openssl_pkey_get_details($key);
if ($keyDetails['type'] !== OPENSSL_KEYTYPE_RSA || $keyDetails['bits'] < 2048) {
throw new Exception('私钥必须是2048位或以上的RSA密钥');
}
Log::info('私钥验证通过');
}
/**
* 获取CA证书路径
*/
private function getCACertPath()
{
$caCertPath = $this->config['ca_cert_path'];
// 如果CA证书不存在,则尝试下载
if (!file_exists($caCertPath)) {
try {
Log::info('CA证书不存在,尝试下载: ' . $caCertPath);
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
],
'http' => [
'timeout' => 10,
]
]);
$cacert = file_get_contents('https://curl.se/ca/cacert.pem', false, $context);
if ($cacert && strlen($cacert) > 10000) { // 确保下载到有效内容
file_put_contents($caCertPath, $cacert);
Log::info('CA证书下载成功: ' . $caCertPath);
return $caCertPath;
} else {
Log::error('下载的CA证书内容无效');
}
} catch (Exception $e) {
Log::error('下载CA证书失败: ' . $e->getMessage());
}
} else {
Log::info('使用已存在的CA证书: ' . $caCertPath);
}
return $caCertPath;
}
/**
* 处理证书数据
* @param array $certificates 证书列表
*/
private function processCertificates($certificates)
{
$successCount = 0;
$errorDetails = [];
foreach ($certificates as $index => $certInfo) {
try {
// 记录当前处理的证书信息
$serialNo = $certInfo['serial_no'] ?? '未知';
Log::info('处理第' . ($index + 1) . '个证书,序列号: ' . $serialNo);
// 验证证书信息是否完整
if (!isset($certInfo['encrypt_certificate'])) {
throw new Exception('缺少encrypt_certificate字段');
}
$encryptCert = $certInfo['encrypt_certificate'];
$requiredFields = ['ciphertext', 'nonce', 'associated_data'];
foreach ($requiredFields as $field) {
if (!isset($encryptCert[$field]) || empty($encryptCert[$field])) {
throw new Exception("证书信息缺少{$field}字段");
}
}
// 解密证书
$cert = $this->decryptCertificate(
$encryptCert['ciphertext'],
$encryptCert['nonce'],
$encryptCert['associated_data']
);
// 解析证书
$parsedCert = openssl_x509_parse($cert);
if (!$parsedCert) {
$error = openssl_error_string();
throw new Exception('解析证书失败: ' . $error);
}
// 验证证书有效期
$now = time();
$effectiveTime = strtotime($certInfo['effective_time'] ?? '');
$expireTime = strtotime($certInfo['expire_time'] ?? '');
if ($effectiveTime === false || $expireTime === false) {
throw new Exception('证书有效期格式不正确');
}
if ($now < $effectiveTime) {
throw new Exception('证书尚未生效');
}
if ($now > $expireTime) {
throw new Exception('证书已过期');
}
// 保存证书信息
$this->platformCerts[] = [
'serial_no' => $serialNo,
'effective_time' => $certInfo['effective_time'],
'expire_time' => $certInfo['expire_time'],
'cert' => $cert,
'parsed_cert' => $parsedCert,
];
$successCount++;
Log::info('成功处理证书,序列号: ' . $serialNo);
} catch (Exception $e) {
$errorMsg = '处理证书失败: ' . $e->getMessage();
$errorDetails[] = $errorMsg . ',证书信息: ' . json_encode($certInfo);
Log::error($errorMsg);
}
}
Log::info("证书处理完成,成功: {$successCount}个,失败: " . (count($certificates) - $successCount) . "个");
if (empty($this->platformCerts)) {
throw new Exception('所有证书处理失败,没有可用的平台证书。详细错误: ' . implode('; ', $errorDetails));
}
// 按过期时间排序,最新的在前面
usort($this->platformCerts, function ($a, $b) {
return strtotime($b['expire_time']) - strtotime($a['expire_time']);
});
}
/**
* 解密证书
* @param string $ciphertext 密文
* @param string $nonce 随机串
* @param string $associatedData 附加数据
* @return string 解密后的证书内容
*/
private function decryptCertificate($ciphertext, $nonce, $associatedData)
{
// 记录解密参数,便于调试
Log::info("开始解密,nonce长度: " . strlen($nonce) . ", associatedData长度: " . strlen($associatedData) . ", 密文长度: " . strlen($ciphertext));
// 验证输入参数
if (empty($ciphertext)) {
throw new Exception('密文为空');
}
if (empty($nonce)) {
throw new Exception('随机串为空');
}
if (empty($associatedData)) {
throw new Exception('附加数据为空');
}
// 尝试解码base64
$decodedCiphertext = base64_decode($ciphertext, true);
if ($decodedCiphertext === false) {
throw new Exception('密文base64解码失败,可能不是有效的base64字符串');
}
if (strlen($decodedCiphertext) < 10) {
throw new Exception('解码后的密文长度过短,可能是无效数据');
}
// 使用正确的AEAD_AES_256_GCM解密方法
// 微信支付使用的是AEAD_AES_256_GCM,需要正确处理认证标签
$ciphertext = base64_decode($ciphertext);
$authTag = substr($ciphertext, -16);
$ciphertext = substr($ciphertext, 0, -16);
// 清除之前的OpenSSL错误
while (openssl_error_string() !== false) {
}
// 使用AEAD_AES_256_GCM解密
$cert = openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$this->config['api_v3_key'],
OPENSSL_RAW_DATA,
$nonce,
$authTag,
$associatedData
);
// 收集所有OpenSSL错误
$errors = [];
while ($error = openssl_error_string()) {
$errors[] = $error;
}
if ($cert === false) {
throw new Exception('解密平台证书失败: ' . implode('; ', $errors) .
'。检查APIv3密钥是否正确,密钥长度是否为32位');
}
// 验证解密结果是否为有效的证书
if (strpos($cert, '-----BEGIN CERTIFICATE-----') === false ||
strpos($cert, '-----END CERTIFICATE-----') === false) {
throw new Exception('解密结果不是有效的证书格式,可能是密钥错误');
}
Log::info('证书解密成功,解密结果长度: ' . strlen($cert));
return $cert;
}
/**
* 生成签名
* @param string $method 请求方法
* @param string $url 请求URL
* @param int $timestamp 时间戳
* @param string $nonce 随机串
* @param string $body 请求体
* @return string 签名
*/
private function generateSignature($method, $url, $timestamp, $nonce, $body)
{
$message = "{$method}\n{$url}\n{$timestamp}\n{$nonce}\n{$body}\n";
Log::info("生成签名的原始消息: " . base64_encode($message)); // 用base64避免特殊字符问题
// 加载私钥
$privateKey = $this->getPrivateKey();
// 生成签名
$signature = '';
$success = openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256);
if (!$success) {
throw new Exception('生成签名失败: ' . openssl_error_string());
}
$signatureBase64 = base64_encode($signature);
Log::info("生成签名成功: {$signatureBase64}");
return $signatureBase64;
}
/**
* 获取私钥
* @return resource 私钥资源
*/
private function getPrivateKey()
{
$privateKey = $this->config['private_key'];
// 如果私钥是文件路径,读取文件内容
if (is_file($privateKey)) {
$privateKey = file_get_contents($privateKey);
}
// 加载私钥
$key = openssl_pkey_get_private($privateKey);
if (!$key) {
throw new Exception('加载商户私钥失败: ' . openssl_error_string());
}
return $key;
}
/**
* 获取缓存的证书
* @return array 证书列表
*/
private function getCachedCerts()
{
$cacheFile = $this->certPath . 'platform_certs.cache';
if (!file_exists($cacheFile)) {
Log::info('平台证书缓存文件不存在: ' . $cacheFile);
return [];
}
if (!is_readable($cacheFile)) {
Log::warning('平台证书缓存文件不可读: ' . $cacheFile);
return [];
}
$cacheData = json_decode(file_get_contents($cacheFile), true);
if (json_last_error() !== JSON_ERROR_NONE) {
Log::error('解析平台证书缓存失败: ' . json_last_error_msg());
return [];
}
if (!isset($cacheData['expire_time']) || !isset($cacheData['certs']) || !is_array($cacheData['certs'])) {
Log::error('平台证书缓存格式不正确');
return [];
}
// 检查缓存是否过期(提前1小时刷新)
$expireTime = $cacheData['expire_time'];
$now = time();
if ($now >= ($expireTime - 3600)) {
Log::info("平台证书缓存已过期或即将过期,当前时间: {$now},过期时间: {$expireTime}");
return [];
}
return $cacheData['certs'];
}
/**
* 缓存证书
*/
private function cacheCerts()
{
if (empty($this->platformCerts)) {
Log::warning('没有可缓存的平台证书');
return;
}
// 使用最早过期的时间作为缓存过期时间
$expireTime = time();
foreach ($this->platformCerts as $cert) {
$certExpire = strtotime($cert['expire_time']);
if ($certExpire > $expireTime) {
$expireTime = $certExpire;
}
}
$cacheData = [
'expire_time' => $expireTime,
'certs' => $this->platformCerts,
];
$cacheFile = $this->certPath . 'platform_certs.cache';
$result = file_put_contents($cacheFile, json_encode($cacheData));
if ($result === false) {
Log::error('保存平台证书缓存失败: ' . $cacheFile);
} else {
Log::info('平台证书缓存保存成功,有效期至: ' . date('Y-m-d H:i:s', $expireTime));
}
// 保存证书文件
foreach ($this->platformCerts as $cert) {
$certFile = $this->certPath . $cert['serial_no'] . '.pem';
if (file_put_contents($certFile, $cert['cert']) === false) {
Log::error('保存平台证书文件失败: ' . $certFile);
} else {
Log::info('平台证书文件保存成功: ' . $certFile);
}
}
}
/**
* 生成随机字符串
*/
private function generateNonce($length = 16)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$nonce = '';
for ($i = 0; $i < $length; $i++) {
$nonce .= $chars[random_int(0, strlen($chars) - 1)];
}
return $nonce;
}
/**
* 获取最新的平台证书
* @return string 证书内容
*/
public function getLatestCert()
{
$certs = $this->getPlatformCerts();
if (empty($certs)) {
throw new Exception('没有可用的平台证书');
}
return $certs[0]['cert'];
}
/**
* 根据序列号获取平台证书
* @param string $serialNo 证书序列号
* @return string 证书内容
*/
public function getCertBySerialNo($serialNo)
{
$certs = $this->getPlatformCerts();
foreach ($certs as $cert) {
if ($cert['serial_no'] === $serialNo) {
return $cert['cert'];
}
}
throw new Exception('找不到序列号为 ' . $serialNo . ' 的平台证书');
}
/**
* 获取配置参数
* @param string $key 配置键名
* @return mixed 配置值
*/
public function getConfig($key = null)
{
if ($key === null) {
return $this->config;
}
return isset($this->config[$key]) ? $this->config[$key] : null;
}
/**
* 获取证书保存路径
* @return string 证书保存路径
*/
public function getCertPath()
{
return $this->certPath;
}
}
版权属于:
小破孩
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论 (0)