BasicAliPay.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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 WeChat\Contracts;
  16. use WeChat\Exceptions\InvalidArgumentException;
  17. use WeChat\Exceptions\InvalidResponseException;
  18. /**
  19. * 支付宝支付基类
  20. * Class AliPay
  21. * @package AliPay\Contracts
  22. */
  23. abstract class BasicAliPay
  24. {
  25. /**
  26. * 支持配置
  27. * @var DataArray
  28. */
  29. protected $config;
  30. /**
  31. * 当前请求数据
  32. * @var DataArray
  33. */
  34. protected $options;
  35. /**
  36. * DzContent数据
  37. * @var DataArray
  38. */
  39. protected $params;
  40. /**
  41. * 静态缓存
  42. * @var static
  43. */
  44. protected static $cache;
  45. /**
  46. * 正常请求网关
  47. * @var string
  48. */
  49. protected $gateway = 'https://openapi.alipay.com/gateway.do?charset=utf-8';
  50. /**
  51. * AliPay constructor.
  52. * @param array $options
  53. */
  54. public function __construct($options)
  55. {
  56. if (empty($options['appid'])) {
  57. throw new InvalidArgumentException('Missing Config -- [appid]');
  58. }
  59. if (empty($options['public_key']) && !empty($options['alipay_cert_path']) && is_file($options['alipay_cert_path'])) {
  60. $options['public_key'] = file_get_contents($options['alipay_cert_path']);
  61. }
  62. if (empty($options['private_key']) && !empty($options['private_key_path']) && is_file($options['private_key_path'])) {
  63. $options['private_key'] = file_get_contents($options['private_key_path']);
  64. }
  65. if (empty($options['public_key'])) {
  66. throw new InvalidArgumentException('Missing Config -- [public_key]');
  67. }
  68. if (empty($options['private_key'])) {
  69. throw new InvalidArgumentException('Missing Config -- [private_key]');
  70. }
  71. if (!empty($options['debug'])) {
  72. $this->gateway = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?charset=utf-8';
  73. }
  74. $this->params = new DataArray([]);
  75. $this->config = new DataArray($options);
  76. $this->options = new DataArray([
  77. 'app_id' => $this->config->get('appid'),
  78. 'charset' => empty($options['charset']) ? 'utf-8' : $options['charset'],
  79. 'format' => 'JSON',
  80. 'version' => '1.0',
  81. 'sign_type' => empty($options['sign_type']) ? 'RSA2' : $options['sign_type'],
  82. 'timestamp' => date('Y-m-d H:i:s'),
  83. ]);
  84. if (isset($options['notify_url']) && $options['notify_url'] !== '') {
  85. $this->options->set('notify_url', $options['notify_url']);
  86. }
  87. if (isset($options['return_url']) && $options['return_url'] !== '') {
  88. $this->options->set('return_url', $options['return_url']);
  89. }
  90. if (isset($options['app_auth_token']) && $options['app_auth_token'] !== '') {
  91. $this->options->set('app_auth_token', $options['app_auth_token']);
  92. }
  93. // 证书模式读取证书
  94. $appCertPath = $this->config->get('app_cert_path');
  95. $aliRootPath = $this->config->get('alipay_root_path');
  96. if (!$this->config->get('app_cert') && !empty($appCertPath) && is_file($appCertPath)) {
  97. $this->config->set('app_cert', file_get_contents($appCertPath));
  98. }
  99. if (!$this->config->get('root_cert') && !empty($aliRootPath) && is_file($aliRootPath)) {
  100. $this->config->set('root_cert', file_get_contents($aliRootPath));
  101. }
  102. }
  103. /**
  104. * 静态创建对象
  105. * @param array $config
  106. * @return static
  107. */
  108. public static function instance(array $config)
  109. {
  110. $key = md5(get_called_class() . serialize($config));
  111. if (isset(self::$cache[$key])) return self::$cache[$key];
  112. return self::$cache[$key] = new static($config);
  113. }
  114. /**
  115. * 查询支付宝订单状态
  116. * @param string $out_trade_no
  117. * @return array|boolean
  118. * @throws \WeChat\Exceptions\InvalidResponseException
  119. * @throws \WeChat\Exceptions\LocalCacheException
  120. */
  121. public function query($out_trade_no = '')
  122. {
  123. $this->options->set('method', 'alipay.trade.query');
  124. return $this->getResult(['out_trade_no' => $out_trade_no]);
  125. }
  126. /**
  127. * 支付宝订单退款操作
  128. * @param array|string $options 退款参数或退款商户订单号
  129. * @param null $refund_amount 退款金额
  130. * @return array|boolean
  131. * @throws \WeChat\Exceptions\InvalidResponseException
  132. * @throws \WeChat\Exceptions\LocalCacheException
  133. */
  134. public function refund($options, $refund_amount = null)
  135. {
  136. if (!is_array($options)) $options = ['out_trade_no' => $options, 'refund_amount' => $refund_amount];
  137. $this->options->set('method', 'alipay.trade.refund');
  138. return $this->getResult($options);
  139. }
  140. /**
  141. * 关闭支付宝进行中的订单
  142. * @param array|string $options
  143. * @return array|boolean
  144. * @throws \WeChat\Exceptions\InvalidResponseException
  145. * @throws \WeChat\Exceptions\LocalCacheException
  146. */
  147. public function close($options)
  148. {
  149. if (!is_array($options)) $options = ['out_trade_no' => $options];
  150. $this->options->set('method', 'alipay.trade.close');
  151. return $this->getResult($options);
  152. }
  153. /**
  154. * 获取通知数据
  155. *
  156. * @param boolean $needSignType 是否需要sign_type字段
  157. * @param array $parameters
  158. * @return array
  159. * @throws \WeChat\Exceptions\InvalidResponseException
  160. */
  161. public function notify($needSignType = false, array $parameters = [])
  162. {
  163. $data = empty($parameters) ? $_POST : $parameters;
  164. if (empty($data) || empty($data['sign'])) {
  165. throw new InvalidResponseException('Illegal push request.', 0, $data);
  166. }
  167. $string = $this->getSignContent($data, $needSignType);
  168. if (openssl_verify($string, base64_decode($data['sign']), $this->getAliPublicKey(), OPENSSL_ALGO_SHA256) !== 1) {
  169. throw new InvalidResponseException('Data signature verification failed.', 0, $data);
  170. }
  171. return $data;
  172. }
  173. /**
  174. * 验证接口返回的数据签名
  175. * @param array $data 通知数据
  176. * @param null|string $sign 数据签名
  177. * @return array
  178. * @throws \WeChat\Exceptions\InvalidResponseException
  179. */
  180. protected function verify($data, $sign)
  181. {
  182. unset($data['sign']);
  183. if ($this->options->get('sign_type') === 'RSA2') {
  184. if (openssl_verify(json_encode($data, 256), base64_decode($sign), $this->getAliPublicKey(), OPENSSL_ALGO_SHA256) !== 1) {
  185. throw new InvalidResponseException('Data signature verification failed by RSA2.');
  186. }
  187. } else {
  188. if (openssl_verify(json_encode($data, 256), base64_decode($sign), $this->getAliPublicKey(), OPENSSL_ALGO_SHA1) !== 1) {
  189. throw new InvalidResponseException('Data signature verification failed by RSA.');
  190. }
  191. }
  192. return $data;
  193. }
  194. /**
  195. * 获取数据签名
  196. * @return string
  197. */
  198. protected function getSign()
  199. {
  200. if ($this->options->get('sign_type') === 'RSA2') {
  201. openssl_sign($this->getSignContent($this->options->get(), true), $sign, $this->getAppPrivateKey(), OPENSSL_ALGO_SHA256);
  202. } else {
  203. openssl_sign($this->getSignContent($this->options->get(), true), $sign, $this->getAppPrivateKey(), OPENSSL_ALGO_SHA1);
  204. }
  205. return base64_encode($sign);
  206. }
  207. /**
  208. * 去除证书前后内容及空白
  209. * @param string $sign
  210. * @return string
  211. */
  212. protected function trimCert($sign)
  213. {
  214. return preg_replace(['/\s+/', '/-{5}.*?-{5}/'], '', $sign);
  215. }
  216. /**
  217. * 数据签名处理
  218. * @param array $data 需要进行签名数据
  219. * @param boolean $needSignType 是否需要sign_type字段
  220. * @return string
  221. */
  222. private function getSignContent(array $data, $needSignType = false)
  223. {
  224. list($attrs,) = [[], ksort($data)];
  225. if (isset($data['sign'])) unset($data['sign']);
  226. if (empty($needSignType)) unset($data['sign_type']);
  227. foreach ($data as $key => $value) {
  228. if ($value === '' || is_null($value)) continue;
  229. $attrs[] = "{$key}={$value}";
  230. }
  231. return join('&', $attrs);
  232. }
  233. /**
  234. * 数据包生成及数据签名
  235. * @param array $options
  236. */
  237. protected function applyData($options)
  238. {
  239. if ($this->config->get('app_cert') && $this->config->get('root_cert')) {
  240. $this->setAppCertSnAndRootCertSn();
  241. }
  242. $this->options->set('biz_content', json_encode($this->params->merge($options), 256));
  243. $this->options->set('sign', $this->getSign());
  244. }
  245. /**
  246. * 请求接口并验证访问数据
  247. * @param array $options
  248. * @return array|boolean
  249. * @throws \WeChat\Exceptions\InvalidResponseException
  250. * @throws \WeChat\Exceptions\LocalCacheException
  251. */
  252. protected function getResult($options)
  253. {
  254. $this->applyData($options);
  255. $method = str_replace('.', '_', $this->options['method']) . '_response';
  256. $data = json_decode(Tools::get($this->gateway, $this->options->get()), true);
  257. if (!isset($data[$method]['code']) || $data[$method]['code'] !== '10000') {
  258. throw new InvalidResponseException(
  259. "Error: " .
  260. (empty($data[$method]['code']) ? '' : "{$data[$method]['msg']} [{$data[$method]['code']}]\r\n") .
  261. (empty($data[$method]['sub_code']) ? '' : "{$data[$method]['sub_msg']} [{$data[$method]['sub_code']}]\r\n"),
  262. $data[$method]['code'], $data
  263. );
  264. }
  265. return $data[$method];
  266. // 返回结果签名检查
  267. // return $this->verify($data[$method], $data['sign']);
  268. }
  269. /**
  270. * 生成支付HTML代码
  271. * @return string
  272. */
  273. protected function buildPayHtml()
  274. {
  275. $html = "<form id='alipaysubmit' name='alipaysubmit' action='{$this->gateway}' method='post'>";
  276. foreach ($this->options->get() as $key => $value) {
  277. $value = str_replace("'", '&apos;', $value);
  278. $html .= "<input type='hidden' name='{$key}' value='{$value}'/>";
  279. }
  280. $html .= "<input type='submit' value='ok' style='display:none;'></form>";
  281. return "{$html}<script>document.forms['alipaysubmit'].submit();</script>";
  282. }
  283. /**
  284. * 获取应用私钥内容
  285. * @return string
  286. */
  287. private function getAppPrivateKey()
  288. {
  289. $content = wordwrap($this->trimCert($this->config->get('private_key')), 64, "\n", true);
  290. return "-----BEGIN RSA PRIVATE KEY-----\n{$content}\n-----END RSA PRIVATE KEY-----";
  291. }
  292. /**
  293. * 获取支付公钥内容
  294. * @return string
  295. */
  296. public function getAliPublicKey()
  297. {
  298. $cert = $this->config->get('public_key');
  299. if (strpos(trim($cert), '-----BEGIN CERTIFICATE-----') !== false) {
  300. $pkey = openssl_pkey_get_public($cert);
  301. $keyData = openssl_pkey_get_details($pkey);
  302. return trim($keyData['key']);
  303. } else {
  304. $content = wordwrap($this->trimCert($cert), 64, "\n", true);
  305. return "-----BEGIN PUBLIC KEY-----\n{$content}\n-----END PUBLIC KEY-----";
  306. }
  307. }
  308. /**
  309. * 新版 从证书中提取序列号
  310. * @param string $sign
  311. * @return string
  312. */
  313. private function getAppCertSN($sign)
  314. {
  315. $ssl = openssl_x509_parse($sign, true);
  316. return md5($this->_arr2str(array_reverse($ssl['issuer'])) . $ssl['serialNumber']);
  317. }
  318. /**
  319. * 新版 提取根证书序列号
  320. * @param string $sign
  321. * @return string|null
  322. */
  323. private function getRootCertSN($sign)
  324. {
  325. $sn = null;
  326. $array = explode('-----END CERTIFICATE-----', $sign);
  327. for ($i = 0; $i < count($array) - 1; $i++) {
  328. $ssl[$i] = openssl_x509_parse($array[$i] . '-----END CERTIFICATE-----', true);
  329. if (strpos($ssl[$i]['serialNumber'], '0x') === 0) {
  330. $ssl[$i]['serialNumber'] = $this->_hex2dec($ssl[$i]['serialNumberHex']);
  331. }
  332. if ($ssl[$i]['signatureTypeLN'] == 'sha1WithRSAEncryption' || $ssl[$i]['signatureTypeLN'] == 'sha256WithRSAEncryption') {
  333. if ($sn == null) {
  334. $sn = md5($this->_arr2str(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']);
  335. } else {
  336. $sn = $sn . '_' . md5($this->_arr2str(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']);
  337. }
  338. }
  339. }
  340. return $sn;
  341. }
  342. /**
  343. * 新版 设置网关应用公钥证书SN、支付宝根证书SN
  344. */
  345. protected function setAppCertSnAndRootCertSn()
  346. {
  347. if (!($appCert = $this->config->get('app_cert'))) {
  348. throw new InvalidArgumentException('Missing Config -- [app_cert|app_cert_path]');
  349. }
  350. if (!($rootCert = $this->config->get('root_cert'))) {
  351. throw new InvalidArgumentException('Missing Config -- [root_cert|alipay_root_path]');
  352. }
  353. $this->options->set('app_cert_sn', $this->getAppCertSN($appCert));
  354. $this->options->set('alipay_root_cert_sn', $this->getRootCertSN($rootCert));
  355. if (!$this->options->get('app_cert_sn')) {
  356. throw new InvalidArgumentException('Missing options -- [app_cert_sn]');
  357. }
  358. if (!$this->options->get('alipay_root_cert_sn')) {
  359. throw new InvalidArgumentException('Missing options -- [alipay_root_cert_sn]');
  360. }
  361. }
  362. /**
  363. * 新版 数组转字符串
  364. * @param array $array
  365. * @return string
  366. */
  367. private function _arr2str($array)
  368. {
  369. $string = [];
  370. if ($array && is_array($array)) {
  371. foreach ($array as $key => $value) {
  372. $string[] = $key . '=' . $value;
  373. }
  374. }
  375. return join(',', $string);
  376. }
  377. /**
  378. * 新版 0x转高精度数字
  379. * @param string $hex
  380. * @return int|string
  381. */
  382. private function _hex2dec($hex)
  383. {
  384. list($dec, $len) = [0, strlen($hex)];
  385. for ($i = 1; $i <= $len; $i++) {
  386. $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
  387. }
  388. return $dec;
  389. }
  390. /**
  391. * 应用数据操作
  392. * @param array $options
  393. * @return mixed
  394. */
  395. abstract public function apply($options);
  396. }