【PHP】境外微信支付生成平台证书 - wechatpay

小破孩
2025-09-05 / 0 评论 / 9 阅读 / 正在检测是否收录...
使用方法
 $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;
    }
}


0

评论 (0)

取消