PlatformCertDown.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2024 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\common\library\wechat\payment;
  13. use WeChatPay\Builder;
  14. use WeChatPay\Crypto\AesGcm;
  15. use WeChatPay\ClientDecoratorInterface;
  16. use GuzzleHttp\Middleware;
  17. use GuzzleHttp\Exception\RequestException;
  18. use Psr\Http\Message\ResponseInterface;
  19. use cores\exception\BaseException;
  20. /**
  21. * 下载「微信支付平台证书」
  22. * PlatformCertDown class
  23. * @package app\common\library\wechat
  24. */
  25. class PlatformCertDown
  26. {
  27. // 微信支付API网关
  28. private const DEFAULT_BASE_URI = 'https://api.mch.weixin.qq.com/';
  29. // 配置参数
  30. private array $opts = [];
  31. /**
  32. * 构造方法
  33. * PlatformCertDown constructor.
  34. * @param array $opts
  35. * @throws BaseException
  36. */
  37. public function __construct(array $opts)
  38. {
  39. $this->opts = $opts + [
  40. // 读取公钥中的序列号
  41. 'serialno' => $this->serialno($opts['publicKey'])
  42. ];
  43. }
  44. /**
  45. * 执行下载
  46. */
  47. public function run(): void
  48. {
  49. // 执行下载
  50. $this->job($this->opts);
  51. }
  52. /**
  53. * 读取公钥中的序列号
  54. * @return mixed
  55. * @throws BaseException
  56. */
  57. public function getPlatformCertSerial()
  58. {
  59. $outputDir = $this->opts['output'] ?? \sys_get_temp_dir();
  60. return $this->serialno($outputDir . \DIRECTORY_SEPARATOR . $this->opts['fileName']);
  61. }
  62. /**
  63. * 读取公钥中的序列号
  64. * @param string $publicKey
  65. * @return mixed
  66. * @throws BaseException
  67. */
  68. private function serialno(string $publicKey)
  69. {
  70. $content = file_get_contents($publicKey);
  71. $plaintext = !empty($content) ? openssl_x509_parse($content) : false;
  72. empty($plaintext) && throwError('证书文件(CERT)不正确');
  73. return $plaintext['serialNumberHex'];
  74. }
  75. /**
  76. * @param array<string,string|true> $opts
  77. *
  78. * @return void
  79. */
  80. private function job(array $opts): void
  81. {
  82. static $certs = ['any' => null];
  83. $outputDir = $opts['output'] ?? \sys_get_temp_dir();
  84. $apiv3Key = (string)$opts['key'];
  85. $instance = Builder::factory([
  86. 'mchid' => $opts['mchid'],
  87. 'serial' => $opts['serialno'],
  88. 'privateKey' => \file_get_contents((string)$opts['privatekey']),
  89. 'certs' => &$certs,
  90. 'base_uri' => (string)($opts['baseuri'] ?? self::DEFAULT_BASE_URI),
  91. ]);
  92. /** @var \GuzzleHttp\HandlerStack $stack */
  93. $stack = $instance->getDriver()->select(ClientDecoratorInterface::JSON_BASED)->getConfig('handler');
  94. // The response middle stacks were executed one by one on `FILO` order.
  95. $stack->after('verifier', Middleware::mapResponse(self::certsInjector($apiv3Key, $certs)), 'injector');
  96. $stack->before('verifier', Middleware::mapResponse(
  97. self::certsRecorder((string)$outputDir, $opts['fileName'], $certs)),
  98. 'recorder'
  99. );
  100. $instance->chain('v3/certificates')->getAsync(
  101. // ['debug' => true]
  102. )->otherwise(static function ($exception) {
  103. if ($exception instanceof RequestException && $exception->hasResponse()) {
  104. /** @var ResponseInterface $response */
  105. $response = $exception->getResponse();
  106. throwError((string)$response->getBody());
  107. }
  108. throwError($exception->getMessage());
  109. })->wait();
  110. }
  111. /**
  112. * 在`verifier`执行之前, 解密平台证书
  113. *
  114. * @param string $apiv3Key
  115. * @param array<string,?string> $certs
  116. *
  117. * @return callable(ResponseInterface)
  118. */
  119. private static function certsInjector(string $apiv3Key, array &$certs): callable
  120. {
  121. return static function (ResponseInterface $response) use ($apiv3Key, &$certs): ResponseInterface {
  122. $body = (string)$response->getBody();
  123. /** @var object{data:array<object{encrypt_certificate:object{serial_no:string,nonce:string,associated_data:string}}>} $json */
  124. $json = \json_decode($body);
  125. $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
  126. \array_map(static function ($row) use ($apiv3Key, &$certs) {
  127. $cert = $row->encrypt_certificate;
  128. try {
  129. $certs[$row->serial_no] = AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data);
  130. } catch (\Throwable $e) {
  131. throwError('支付密钥(APIKEY) 或 证书文件(KEY)不正确,请重新输入');
  132. }
  133. }, $data);
  134. return $response;
  135. };
  136. }
  137. /**
  138. * 将平台证书写入硬盘
  139. *
  140. * @param string $outputDir
  141. * @param string fileName
  142. * @param array<string,?string> $certs
  143. *
  144. * @return callable(ResponseInterface)
  145. */
  146. private static function certsRecorder(string $outputDir, string $fileName, array &$certs): callable
  147. {
  148. return static function (ResponseInterface $response) use ($outputDir, $fileName, &$certs): ResponseInterface {
  149. $body = (string)$response->getBody();
  150. /** @var object{data:array<object{effective_time:string,expire_time:string:serial_no:string}>} $json */
  151. $json = \json_decode($body);
  152. $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
  153. \array_walk($data, static function ($row, $index, $certs) use ($outputDir, $fileName) {
  154. $serialNo = $row->serial_no;
  155. $outpath = $outputDir . \DIRECTORY_SEPARATOR . $fileName;
  156. // self::prompt(
  157. // 'Certificate #' . $index . ' {',
  158. // ' Serial Number: ' . $serialNo,
  159. // ' Not Before: ' . (new \DateTime($row->effective_time))->format('Y-m-d H:i:s'),
  160. // ' Not After: ' . (new \DateTime($row->expire_time))->format('Y-m-d H:i:s'),
  161. // ' Saved to: ' . $outpath,
  162. // ' Content:', $certs[$serialNo] ?? '',
  163. // '}'
  164. // );
  165. \file_put_contents($outpath, $certs[$serialNo]);
  166. }, $certs);
  167. return $response;
  168. };
  169. }
  170. /**
  171. * 输出信息
  172. * @param string $messages
  173. */
  174. private static function prompt(...$messages): void
  175. {
  176. \array_walk($messages, static function (string $message): void {
  177. \printf('%s%s', $message, \PHP_EOL);
  178. });
  179. }
  180. }