BasicAliPay.php 12 KB

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