BasicWePay.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | WeChatDeveloper
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2014~2020 广州楚才信息科技有限公司 [ http://www.cuci.cc ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: http://think.ctolog.com
  8. // +----------------------------------------------------------------------
  9. // | 开源协议 ( https://mit-license.org )
  10. // +----------------------------------------------------------------------
  11. // | github开源项目:https://github.com/zoujingli/WeChatDeveloper
  12. // +----------------------------------------------------------------------
  13. namespace WePayV3\Contracts;
  14. use WeChat\Contracts\Tools;
  15. use WeChat\Exceptions\InvalidArgumentException;
  16. use WeChat\Exceptions\InvalidResponseException;
  17. use WeChat\Exceptions\LocalCacheException;
  18. use WePayV3\Cert;
  19. /**
  20. * 微信支付基础类
  21. * Class BasicWePay
  22. * @package WePayV3
  23. */
  24. abstract class BasicWePay
  25. {
  26. /**
  27. * 接口基础地址
  28. * @var string
  29. */
  30. protected $base = 'https://api.mch.weixin.qq.com';
  31. /**
  32. * 实例对象静态缓存
  33. * @var array
  34. */
  35. static $cache = [];
  36. /**
  37. * 配置参数
  38. * @var array
  39. */
  40. protected $config = [
  41. 'appid' => '', // 微信绑定APPID,需配置
  42. 'mch_id' => '', // 微信商户编号,需要配置
  43. 'mch_v3_key' => '', // 微信商户密钥,需要配置
  44. 'cert_serial' => '', // 商户证书序号,无需配置
  45. 'cert_public' => '', // 商户公钥内容,需要配置
  46. 'cert_private' => '', // 商户密钥内容,需要配置
  47. ];
  48. /**
  49. * BasicWePayV3 constructor.
  50. * @param array $options [mch_id, mch_v3_key, cert_public, cert_private]
  51. */
  52. public function __construct(array $options = [])
  53. {
  54. if (empty($options['mch_id'])) {
  55. throw new InvalidArgumentException("Missing Config -- [mch_id]");
  56. }
  57. if (empty($options['mch_v3_key'])) {
  58. throw new InvalidArgumentException("Missing Config -- [mch_v3_key]");
  59. }
  60. if (empty($options['cert_private'])) {
  61. throw new InvalidArgumentException("Missing Config -- [cert_private]");
  62. }
  63. if (empty($options['cert_public'])) {
  64. throw new InvalidArgumentException("Missing Config -- [cert_public]");
  65. }
  66. if (stripos($options['cert_public'], '-----BEGIN CERTIFICATE-----') === false) {
  67. if (file_exists($options['cert_public'])) {
  68. $options['cert_public'] = file_get_contents($options['cert_public']);
  69. } else {
  70. throw new InvalidArgumentException("File Non-Existent -- [cert_public]");
  71. }
  72. }
  73. if (stripos($options['cert_private'], '-----BEGIN PRIVATE KEY-----') === false) {
  74. if (file_exists($options['cert_private'])) {
  75. $options['cert_private'] = file_get_contents($options['cert_private']);
  76. } else {
  77. throw new InvalidArgumentException("File Non-Existent -- [cert_private]");
  78. }
  79. }
  80. $this->config['appid'] = isset($options['appid']) ? $options['appid'] : '';
  81. $this->config['mch_id'] = $options['mch_id'];
  82. $this->config['mch_v3_key'] = $options['mch_v3_key'];
  83. $this->config['cert_public'] = $options['cert_public'];
  84. $this->config['cert_private'] = $options['cert_private'];
  85. $this->config['cert_serial'] = openssl_x509_parse($this->config['cert_public'])['serialNumberHex'];
  86. if (empty($this->config['cert_serial'])) {
  87. throw new InvalidArgumentException("Failed to parse certificate public key");
  88. }
  89. }
  90. /**
  91. * 静态创建对象
  92. * @param array $config
  93. * @return static
  94. */
  95. public static function instance($config)
  96. {
  97. $key = md5(get_called_class() . serialize($config));
  98. if (isset(self::$cache[$key])) return self::$cache[$key];
  99. return self::$cache[$key] = new static($config);
  100. }
  101. /**
  102. * 模拟发起请求
  103. * @param string $method 请求访问
  104. * @param string $pathinfo 请求路由
  105. * @param string $jsondata 请求数据
  106. * @param bool $verify 是否验证
  107. * @return array
  108. * @throws InvalidResponseException
  109. */
  110. public function doRequest($method, $pathinfo, $jsondata = '', $verify = false)
  111. {
  112. list($time, $nonce) = [time(), uniqid() . rand(1000, 9999)];
  113. $signstr = join("\n", [$method, $pathinfo, $time, $nonce, $jsondata, '']);
  114. // 生成数据签名TOKEN
  115. $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
  116. $this->config['mch_id'], $nonce, $time, $this->config['cert_serial'], $this->signBuild($signstr)
  117. );
  118. list($header, $content) = $this->_doRequestCurl($method, $this->base . $pathinfo, [
  119. 'data' => $jsondata, 'header' => [
  120. "Accept: application/json", "Content-Type: application/json",
  121. 'User-Agent: https://thinkadmin.top', "Authorization: WECHATPAY2-SHA256-RSA2048 {$token}",
  122. ],
  123. ]);
  124. if ($verify) {
  125. $headers = [];
  126. foreach (explode("\n", $header) as $line) {
  127. if (stripos($line, 'Wechatpay') !== false) {
  128. list($name, $value) = explode(':', $line);
  129. list(, $keys) = explode('wechatpay-', strtolower($name));
  130. $headers[$keys] = trim($value);
  131. }
  132. }
  133. try {
  134. $string = join("\n", [$headers['timestamp'], $headers['nonce'], $content, '']);
  135. if (!$this->signVerify($string, $headers['signature'], $headers['serial'])) {
  136. throw new InvalidResponseException("验证响应签名失败");
  137. }
  138. } catch (\Exception $exception) {
  139. throw new InvalidResponseException($exception->getMessage(), $exception->getCode());
  140. }
  141. }
  142. return json_decode($content, true);
  143. }
  144. /**
  145. * 通过CURL模拟网络请求
  146. * @param string $method 请求方法
  147. * @param string $location 请求方法
  148. * @param array $options 请求参数 [data, header]
  149. * @return array [header,content]
  150. */
  151. private function _doRequestCurl($method, $location, $options = [])
  152. {
  153. $curl = curl_init();
  154. // POST数据设置
  155. if (strtolower($method) === 'post') {
  156. curl_setopt($curl, CURLOPT_POST, true);
  157. curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data']);
  158. }
  159. // CURL头信息设置
  160. if (!empty($options['header'])) {
  161. curl_setopt($curl, CURLOPT_HTTPHEADER, $options['header']);
  162. }
  163. curl_setopt($curl, CURLOPT_URL, $location);
  164. curl_setopt($curl, CURLOPT_HEADER, true);
  165. curl_setopt($curl, CURLOPT_TIMEOUT, 60);
  166. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  167. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  168. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  169. $content = curl_exec($curl);
  170. $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  171. curl_close($curl);
  172. return [substr($content, 0, $headerSize), substr($content, $headerSize)];
  173. }
  174. /**
  175. * 生成数据签名
  176. * @param string $data 签名内容
  177. * @return string
  178. */
  179. protected function signBuild($data)
  180. {
  181. $pkeyid = openssl_pkey_get_private($this->config['cert_private']);
  182. openssl_sign($data, $signature, $pkeyid, 'sha256WithRSAEncryption');
  183. return base64_encode($signature);
  184. }
  185. /**
  186. * 验证内容签名
  187. * @param string $data 签名内容
  188. * @param string $sign 原签名值
  189. * @param string $serial 证书序号
  190. * @return int
  191. * @throws InvalidResponseException
  192. * @throws LocalCacheException
  193. */
  194. protected function signVerify($data, $sign, $serial = '')
  195. {
  196. $cert = $this->tmpFile($serial);
  197. if (empty($cert)) {
  198. Cert::instance($this->config)->download();
  199. $cert = $this->tmpFile($serial);
  200. }
  201. return @openssl_verify($data, base64_decode($sign), openssl_x509_read($cert), 'sha256WithRSAEncryption');
  202. }
  203. /**
  204. * 写入或读取临时文件
  205. * @param string $name
  206. * @param null|string $content
  207. * @return string
  208. * @throws LocalCacheException
  209. */
  210. protected function tmpFile($name, $content = null)
  211. {
  212. if (is_null($content)) {
  213. return base64_decode(Tools::getCache($name) ?: '');
  214. } else {
  215. return Tools::setCache($name, base64_encode($content), 7200);
  216. }
  217. }
  218. }