BasicWePay.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | WeChatDeveloper
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2014~2023 ThinkAdmin [ thinkadmin.top ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: https://thinkadmin.top
  8. // +----------------------------------------------------------------------
  9. // | 开源协议 ( https://mit-license.org )
  10. // | 免责声明 ( https://thinkadmin.top/disclaimer )
  11. // +----------------------------------------------------------------------
  12. // | gitee 代码仓库:https://gitee.com/zoujingli/WeChatDeveloper
  13. // | github 代码仓库:https://github.com/zoujingli/WeChatDeveloper
  14. // +----------------------------------------------------------------------
  15. namespace WePayV3\Contracts;
  16. use WeChat\Contracts\Tools;
  17. use WeChat\Exceptions\InvalidArgumentException;
  18. use WeChat\Exceptions\InvalidDecryptException;
  19. use WeChat\Exceptions\InvalidResponseException;
  20. use WePayV3\Cert;
  21. /**
  22. * 微信支付基础类
  23. * Class BasicWePay
  24. * @package WePayV3
  25. */
  26. abstract class BasicWePay
  27. {
  28. /**
  29. * 接口基础地址
  30. * @var string
  31. */
  32. protected $base = 'https://api.mch.weixin.qq.com';
  33. /**
  34. * 实例对象静态缓存
  35. * @var array
  36. */
  37. static $cache = [];
  38. /**
  39. * 自动配置平台证书
  40. * @var bool
  41. */
  42. protected $autoCert = true;
  43. /**
  44. * 配置参数
  45. * @var array
  46. */
  47. protected $config = [
  48. 'appid' => '', // 微信绑定APPID,需配置
  49. 'mch_id' => '', // 微信商户编号,需要配置
  50. 'mch_v3_key' => '', // 微信商户密钥,需要配置
  51. 'cert_serial' => '', // 商户证书序号,无需配置
  52. 'cert_public' => '', // 商户公钥内容,需要配置
  53. 'cert_private' => '', // 商户密钥内容,需要配置
  54. 'mp_cert_serial' => '', // 平台证书序号,无需配置
  55. 'mp_cert_content' => '', // 平台证书内容,无需配置
  56. ];
  57. /**
  58. * BasicWePayV3 constructor.
  59. * @param array $options [mch_id, mch_v3_key, cert_public, cert_private]
  60. * @throws \WeChat\Exceptions\InvalidResponseException
  61. * @throws \WeChat\Exceptions\LocalCacheException
  62. */
  63. public function __construct(array $options = [])
  64. {
  65. if (empty($options['mch_id'])) {
  66. throw new InvalidArgumentException("Missing Config -- [mch_id]");
  67. }
  68. if (empty($options['mch_v3_key'])) {
  69. throw new InvalidArgumentException("Missing Config -- [mch_v3_key]");
  70. }
  71. if (empty($options['cert_public'])) {
  72. throw new InvalidArgumentException("Missing Config -- [cert_public]");
  73. }
  74. if (empty($options['cert_private'])) {
  75. throw new InvalidArgumentException("Missing Config -- [cert_private]");
  76. }
  77. if (stripos($options['cert_public'], '-----BEGIN CERTIFICATE-----') === false) {
  78. if (file_exists($options['cert_public'])) {
  79. $options['cert_public'] = file_get_contents($options['cert_public']);
  80. } else {
  81. throw new InvalidArgumentException("File Non-Existent -- [cert_public]");
  82. }
  83. }
  84. if (stripos($options['cert_private'], '-----BEGIN PRIVATE KEY-----') === false) {
  85. if (file_exists($options['cert_private'])) {
  86. $options['cert_private'] = file_get_contents($options['cert_private']);
  87. } else {
  88. throw new InvalidArgumentException("File Non-Existent -- [cert_private]");
  89. }
  90. }
  91. $this->config['appid'] = isset($options['appid']) ? $options['appid'] : '';
  92. $this->config['mch_id'] = $options['mch_id'];
  93. $this->config['mch_v3_key'] = $options['mch_v3_key'];
  94. $this->config['cert_public'] = $options['cert_public'];
  95. $this->config['cert_private'] = $options['cert_private'];
  96. if (empty($options['cert_serial'])) {
  97. $this->config['cert_serial'] = openssl_x509_parse($this->config['cert_public'], true)['serialNumberHex'];
  98. } else {
  99. $this->config['cert_serial'] = $options['cert_serial'];
  100. }
  101. if (empty($this->config['cert_serial'])) {
  102. throw new InvalidArgumentException('Failed to parse certificate public key');
  103. }
  104. if (!empty($options['cache_path'])) {
  105. Tools::$cache_path = $options['cache_path'];
  106. }
  107. // 自动配置平台证书
  108. if ($this->autoCert) {
  109. $this->_autoCert();
  110. }
  111. // 服务商参数支持
  112. // if (!empty($options['sp_appid'])) {
  113. // $this->config['sp_appid'] = $options['sp_appid'];
  114. // }
  115. // if (!empty($options['sp_mchid'])) {
  116. // $this->config['sp_mchid'] = $options['sp_mchid'];
  117. // }
  118. // if (!empty($options['sub_appid'])) {
  119. // $this->config['sub_appid'] = $options['sub_appid'];
  120. // }
  121. // if (!empty($options['sub_mch_id'])) {
  122. // $this->config['sub_mch_id'] = $options['sub_mch_id'];
  123. // }
  124. }
  125. /**
  126. * 静态创建对象
  127. * @param array $config
  128. * @return static
  129. * @throws \WeChat\Exceptions\InvalidResponseException
  130. * @throws \WeChat\Exceptions\LocalCacheException
  131. */
  132. public static function instance($config)
  133. {
  134. $key = md5(get_called_class() . serialize($config));
  135. if (isset(self::$cache[$key])) return self::$cache[$key];
  136. return self::$cache[$key] = new static($config);
  137. }
  138. /**
  139. * 模拟发起请求
  140. * @param string $method 请求访问
  141. * @param string $pathinfo 请求路由
  142. * @param string $jsondata 请求数据
  143. * @param boolean $verify 是否验证
  144. * @param boolean $isjson 返回JSON
  145. * @return array|string
  146. * @throws \WeChat\Exceptions\InvalidResponseException
  147. */
  148. public function doRequest($method, $pathinfo, $jsondata = '', $verify = false, $isjson = true)
  149. {
  150. list($time, $nonce) = [time(), uniqid() . rand(1000, 9999)];
  151. $signstr = join("\n", [$method, $pathinfo, $time, $nonce, $jsondata, '']);
  152. // 生成数据签名TOKEN
  153. $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
  154. $this->config['mch_id'], $nonce, $time, $this->config['cert_serial'], $this->signBuild($signstr)
  155. );
  156. $location = (preg_match('|^https?://|', $pathinfo) ? '' : $this->base) . $pathinfo;
  157. list($header, $content) = $this->_doRequestCurl($method, $location, [
  158. 'data' => $jsondata, 'header' => [
  159. 'Accept: application/json',
  160. 'Content-Type: application/json',
  161. 'User-Agent: https://thinkadmin.top',
  162. "Authorization: WECHATPAY2-SHA256-RSA2048 {$token}",
  163. "Wechatpay-Serial: {$this->config['mp_cert_serial']}"
  164. ],
  165. ]);
  166. if ($verify) {
  167. $headers = [];
  168. foreach (explode("\n", $header) as $line) {
  169. if (stripos($line, 'Wechatpay') !== false) {
  170. list($name, $value) = explode(':', $line);
  171. list(, $keys) = explode('wechatpay-', strtolower($name));
  172. $headers[$keys] = trim($value);
  173. }
  174. }
  175. try {
  176. if (empty($headers)) {
  177. return $isjson ? json_decode($content, true) : $content;
  178. }
  179. $string = join("\n", [$headers['timestamp'], $headers['nonce'], $content, '']);
  180. if (!$this->signVerify($string, $headers['signature'], $headers['serial'])) {
  181. throw new InvalidResponseException('验证响应签名失败');
  182. }
  183. } catch (\Exception $exception) {
  184. throw new InvalidResponseException($exception->getMessage(), $exception->getCode());
  185. }
  186. }
  187. return $isjson ? json_decode($content, true) : $content;
  188. }
  189. /**
  190. * 通过CURL模拟网络请求
  191. * @param string $method 请求方法
  192. * @param string $location 请求方法
  193. * @param array $options 请求参数 [data, header]
  194. * @return array [header,content]
  195. */
  196. private function _doRequestCurl($method, $location, $options = [])
  197. {
  198. $curl = curl_init();
  199. // POST数据设置
  200. if (strtolower($method) === 'post') {
  201. curl_setopt($curl, CURLOPT_POST, true);
  202. curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data']);
  203. }
  204. // CURL头信息设置
  205. if (!empty($options['header'])) {
  206. curl_setopt($curl, CURLOPT_HTTPHEADER, $options['header']);
  207. }
  208. curl_setopt($curl, CURLOPT_URL, $location);
  209. curl_setopt($curl, CURLOPT_HEADER, true);
  210. curl_setopt($curl, CURLOPT_TIMEOUT, 60);
  211. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  212. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  213. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  214. $content = curl_exec($curl);
  215. $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  216. curl_close($curl);
  217. return [substr($content, 0, $headerSize), substr($content, $headerSize)];
  218. }
  219. /**
  220. * 生成数据签名
  221. * @param string $data 签名内容
  222. * @return string
  223. */
  224. protected function signBuild($data)
  225. {
  226. $pkeyid = openssl_pkey_get_private($this->config['cert_private']);
  227. openssl_sign($data, $signature, $pkeyid, 'sha256WithRSAEncryption');
  228. return base64_encode($signature);
  229. }
  230. /**
  231. * 验证内容签名
  232. * @param string $data 签名内容
  233. * @param string $sign 原签名值
  234. * @param string $serial 证书序号
  235. * @return int
  236. * @throws \WeChat\Exceptions\InvalidResponseException
  237. * @throws \WeChat\Exceptions\LocalCacheException
  238. */
  239. protected function signVerify($data, $sign, $serial)
  240. {
  241. $cert = $this->_getCert($serial);
  242. return @openssl_verify($data, base64_decode($sign), openssl_x509_read($cert), 'sha256WithRSAEncryption');
  243. }
  244. /**
  245. * 获取平台证书
  246. * @param string $serial
  247. * @return string
  248. * @throws \WeChat\Exceptions\InvalidResponseException
  249. * @throws \WeChat\Exceptions\LocalCacheException
  250. */
  251. protected function _getCert($serial = '')
  252. {
  253. $certs = $this->tmpFile("{$this->config['mch_id']}_certs");
  254. if (empty($certs) || empty($certs[$serial]['content'])) {
  255. Cert::instance($this->config)->download();
  256. $certs = $this->tmpFile("{$this->config['mch_id']}_certs");
  257. }
  258. if (empty($certs[$serial]['content']) || $certs[$serial]['expire'] < time()) {
  259. throw new InvalidResponseException("读取平台证书失败!");
  260. } else {
  261. return $certs[$serial]['content'];
  262. }
  263. }
  264. /**
  265. * 自动配置平台证书
  266. * @return void
  267. * @throws \WeChat\Exceptions\InvalidResponseException
  268. * @throws \WeChat\Exceptions\LocalCacheException
  269. */
  270. protected function _autoCert()
  271. {
  272. $certs = $this->tmpFile("{$this->config['mch_id']}_certs");
  273. if (is_array($certs)) foreach ($certs as $k => $v) {
  274. if ($v['expire'] < time()) unset($certs[$k]);
  275. }
  276. if (empty($certs)) {
  277. Cert::instance($this->config)->download();
  278. $certs = $this->tmpFile("{$this->config['mch_id']}_certs");
  279. }
  280. if (empty($certs) || !is_array($certs)) {
  281. throw new InvalidResponseException("读取平台证书失败!");
  282. }
  283. foreach ($certs as $k => $v) if ($v['expire'] > time() + 10) {
  284. $this->config['mp_cert_serial'] = $k;
  285. $this->config['mp_cert_content'] = $v['content'];
  286. break;
  287. }
  288. if (empty($this->config['mp_cert_serial']) || empty($this->config['mp_cert_content'])) {
  289. throw new InvalidResponseException("自动配置平台证书失败!");
  290. }
  291. }
  292. /**
  293. * 写入或读取临时文件
  294. * @param string $name
  295. * @param null|array|string $content
  296. * @param integer $expire
  297. * @return array|string
  298. * @throws \WeChat\Exceptions\LocalCacheException
  299. */
  300. protected function tmpFile($name, $content = null, $expire = 7200)
  301. {
  302. if (is_null($content)) {
  303. $text = Tools::getCache($name);
  304. if (empty($text)) return '';
  305. $json = json_decode(Tools::getCache($name) ?: '', true);
  306. return isset($json[0]) ? $json[0] : '';
  307. } else {
  308. return Tools::setCache($name, json_encode([$content], JSON_UNESCAPED_UNICODE), $expire);
  309. }
  310. }
  311. /**
  312. * RSA加密处理-平台证书
  313. * @param string $string
  314. * @return string
  315. * @throws \WeChat\Exceptions\InvalidDecryptException
  316. */
  317. protected function rsaEncode($string)
  318. {
  319. $publicKey = $this->config['mp_cert_content'];
  320. if (openssl_public_encrypt($string, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING)) {
  321. return base64_encode($encrypted);
  322. } else {
  323. throw new InvalidDecryptException('Rsa Encrypt Error.');
  324. }
  325. }
  326. }