AlipayF2F.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. <?php
  2. /*
  3. * 作者:BrettonYe
  4. * 功能:ProxyPanel 支付宝面对面 【收单线下交易预创建】 【收单交易查询】 接口实现库
  5. * 时间:2022/12/28
  6. * 参考资料:https://opendocs.alipay.com/open/02ekfg?scene=19 riverslei/payment
  7. */
  8. namespace App\Utils\Library;
  9. use Illuminate\Support\Facades\Http;
  10. use RuntimeException;
  11. class AlipayF2F
  12. {
  13. private static string $gatewayUrl = 'https://openapi.alipay.com/gateway.do';
  14. private array $config;
  15. public function __construct(array $rawConfig = [])
  16. {
  17. $config = [
  18. 'app_id' => $rawConfig['app_id'],
  19. 'public_key' => '',
  20. 'private_key' => '',
  21. 'notify_url' => $rawConfig['notify_url'],
  22. ];
  23. if ($rawConfig['ali_public_key']) {
  24. $config['public_key'] = self::getRsaKeyValue($rawConfig['ali_public_key'], false);
  25. }
  26. if (empty($config['public_key'])) {
  27. throw new RuntimeException('please set ali public key');
  28. }
  29. // 初始 RSA私钥文件 需要检查该文件是否存在
  30. if ($rawConfig['rsa_private_key']) {
  31. $config['private_key'] = self::getRsaKeyValue($rawConfig['rsa_private_key']);
  32. }
  33. if (empty($config['private_key'])) {
  34. throw new RuntimeException('please set ali private key');
  35. }
  36. $this->config = $config;
  37. }
  38. /**
  39. * 获取rsa密钥内容.
  40. *
  41. * @param string $key 传入的密钥信息, 可能是文件或者字符串
  42. * @param bool $is_private 私钥/公钥
  43. */
  44. public static function getRsaKeyValue(string $key, bool $is_private = true): ?string
  45. {
  46. $keyStr = is_file($key) ? @file_get_contents($key) : $key;
  47. if (empty($keyStr)) {
  48. return null;
  49. }
  50. $keyStr = str_replace('\n', '', $keyStr);
  51. // 为了解决用户传入的密钥格式,这里进行统一处理
  52. if ($is_private) {
  53. $beginStr = "-----BEGIN RSA PRIVATE KEY-----\n";
  54. $endStr = "\n-----END RSA PRIVATE KEY-----";
  55. } else {
  56. $beginStr = "-----BEGIN PUBLIC KEY-----\n";
  57. $endStr = "\n-----END PUBLIC KEY-----";
  58. }
  59. return $beginStr.wordwrap($keyStr, 64, "\n", true).$endStr;
  60. }
  61. public function tradeQuery($content)
  62. {
  63. $this->setMethod('alipay.trade.query');
  64. $this->setContent($content);
  65. return $this->send();
  66. }
  67. private function setMethod($method): void
  68. {
  69. $this->config['method'] = $method;
  70. }
  71. private function setContent($content): void
  72. {
  73. $content = array_filter($content);
  74. ksort($content);
  75. $this->config['biz_content'] = json_encode($content);
  76. }
  77. private function send(): array
  78. {
  79. $response = Http::timeout(15)->get(self::$gatewayUrl, $this->buildParams())->json();
  80. $resKey = str_replace('.', '_', $this->config['method']).'_response';
  81. if (! isset($response[$resKey])) {
  82. throw new RuntimeException('请求错误-看起来是请求失败');
  83. }
  84. if (! $this->rsaVerify($response[$resKey], $response['sign'])) {
  85. throw new RuntimeException('验签错误-'.$response[$resKey]['msg'].' | '.($response[$resKey]['sub_msg'] ?? var_export($response, true)));
  86. }
  87. $response = $response[$resKey];
  88. if ($response['msg'] !== 'Success') {
  89. throw new RuntimeException($response[$resKey]['sub_msg'] ?? var_export($response, true));
  90. }
  91. return $response;
  92. }
  93. private function buildParams(): array
  94. {
  95. $params = [
  96. 'app_id' => $this->config['app_id'] ?? '',
  97. 'method' => $this->config['method'] ?? '',
  98. 'charset' => 'utf-8',
  99. 'sign_type' => 'RSA2',
  100. 'timestamp' => date('Y-m-d H:m:s'),
  101. 'biz_content' => $this->config['biz_content'] ?? [],
  102. 'version' => '1.0',
  103. 'notify_url' => $this->config['notify_url'] ?? '',
  104. ];
  105. $params = array_filter($params);
  106. $params['sign'] = $this->encrypt($this->buildQuery($params));
  107. return $params;
  108. }
  109. /**
  110. * RSA2签名.
  111. *
  112. * @param string $data 签名的数组
  113. *
  114. * @throws RuntimeException
  115. */
  116. private function encrypt(string $data): string
  117. {
  118. $privateKey = openssl_pkey_get_private($this->config['private_key']); // 私钥
  119. if (empty($privateKey)) {
  120. throw new RuntimeException('您使用的私钥格式错误,请检查RSA私钥配置');
  121. }
  122. openssl_sign($data, $sign, $privateKey, OPENSSL_ALGO_SHA256);
  123. return base64_encode($sign); // base64编码
  124. }
  125. private function buildQuery(array $params): string
  126. {
  127. ksort($params); // 排序
  128. return urldecode(http_build_query($params)); // 组合
  129. }
  130. /**
  131. * RSA2验签.
  132. *
  133. * @param array $data 待签名数据
  134. *
  135. * @throws RuntimeException
  136. */
  137. public function rsaVerify(array $data, $sign): bool
  138. {
  139. unset($data['sign'], $data['sign_type']);
  140. $publicKey = openssl_pkey_get_public($this->config['public_key']);
  141. if (empty($publicKey)) {
  142. throw new RuntimeException('支付宝RSA公钥错误。请检查公钥文件格式是否正确');
  143. }
  144. return (bool) openssl_verify(json_encode($data), base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
  145. }
  146. public function qrCharge($content)
  147. {
  148. $this->setMethod('alipay.trade.precreate');
  149. $this->setContent($content);
  150. return $this->send();
  151. }
  152. }