Support.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <?php
  2. namespace Yansongda\Pay\Gateways\Alipay;
  3. use Exception;
  4. use Yansongda\Pay\Events;
  5. use Yansongda\Pay\Exceptions\GatewayException;
  6. use Yansongda\Pay\Exceptions\InvalidArgumentException;
  7. use Yansongda\Pay\Exceptions\InvalidConfigException;
  8. use Yansongda\Pay\Exceptions\InvalidSignException;
  9. use Yansongda\Pay\Gateways\Alipay;
  10. use Yansongda\Pay\Log;
  11. use Yansongda\Supports\Arr;
  12. use Yansongda\Supports\Collection;
  13. use Yansongda\Supports\Config;
  14. use Yansongda\Supports\Str;
  15. use Yansongda\Supports\Traits\HasHttpRequest;
  16. /**
  17. * @author yansongda <me@yansongda.cn>
  18. *
  19. * @property string app_id alipay app_id
  20. * @property string ali_public_key
  21. * @property string private_key
  22. * @property array http http options
  23. * @property string mode current mode
  24. * @property array log log options
  25. * @property string pid ali pid
  26. */
  27. class Support
  28. {
  29. use HasHttpRequest;
  30. /**
  31. * Alipay gateway.
  32. *
  33. * @var string
  34. */
  35. protected $baseUri;
  36. /**
  37. * Config.
  38. *
  39. * @var Config
  40. */
  41. protected $config;
  42. /**
  43. * Instance.
  44. *
  45. * @var Support
  46. */
  47. private static $instance;
  48. /**
  49. * Bootstrap.
  50. *
  51. * @author yansongda <me@yansongda.cn>
  52. */
  53. private function __construct(Config $config)
  54. {
  55. $this->baseUri = Alipay::URL[$config->get('mode', Alipay::MODE_NORMAL)];
  56. $this->config = $config;
  57. $this->setHttpOptions();
  58. }
  59. /**
  60. * __get.
  61. *
  62. * @author yansongda <me@yansongda.cn>
  63. *
  64. * @param $key
  65. *
  66. * @return mixed|Config|null
  67. */
  68. public function __get($key)
  69. {
  70. return $this->getConfig($key);
  71. }
  72. /**
  73. * create.
  74. *
  75. * @author yansongda <me@yansongda.cn>
  76. *
  77. * @return Support
  78. */
  79. public static function create(Config $config)
  80. {
  81. if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
  82. self::$instance = new self($config);
  83. }
  84. return self::$instance;
  85. }
  86. /**
  87. * getInstance.
  88. *
  89. * @author yansongda <me@yansongda.cn>
  90. *
  91. * @throws InvalidArgumentException
  92. *
  93. * @return Support
  94. */
  95. public static function getInstance()
  96. {
  97. if (is_null(self::$instance)) {
  98. throw new InvalidArgumentException('You Should [Create] First Before Using');
  99. }
  100. return self::$instance;
  101. }
  102. /**
  103. * clear.
  104. *
  105. * @author yansongda <me@yansongda.cn>
  106. */
  107. public function clear()
  108. {
  109. self::$instance = null;
  110. }
  111. /**
  112. * Get Alipay API result.
  113. *
  114. * @author yansongda <me@yansongda.cn>
  115. *
  116. * @throws GatewayException
  117. * @throws InvalidConfigException
  118. * @throws InvalidSignException
  119. */
  120. public static function requestApi(array $data): Collection
  121. {
  122. Events::dispatch(new Events\ApiRequesting('Alipay', '', self::$instance->getBaseUri(), $data));
  123. $data = array_filter($data, function ($value) {
  124. return ('' == $value || is_null($value)) ? false : true;
  125. });
  126. $result = json_decode(self::$instance->post('', $data), true);
  127. Events::dispatch(new Events\ApiRequested('Alipay', '', self::$instance->getBaseUri(), $result));
  128. return self::processingApiResult($data, $result);
  129. }
  130. /**
  131. * Generate sign.
  132. *
  133. * @author yansongda <me@yansongda.cn>
  134. *
  135. * @throws InvalidConfigException
  136. */
  137. public static function generateSign(array $params): string
  138. {
  139. $privateKey = self::$instance->private_key;
  140. if (is_null($privateKey)) {
  141. throw new InvalidConfigException('Missing Alipay Config -- [private_key]');
  142. }
  143. if (Str::endsWith($privateKey, '.pem')) {
  144. $privateKey = openssl_pkey_get_private(
  145. Str::startsWith($privateKey, 'file://') ? $privateKey : 'file://'.$privateKey
  146. );
  147. } else {
  148. $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n".
  149. wordwrap($privateKey, 64, "\n", true).
  150. "\n-----END RSA PRIVATE KEY-----";
  151. }
  152. openssl_sign(self::getSignContent($params), $sign, $privateKey, OPENSSL_ALGO_SHA256);
  153. $sign = base64_encode($sign);
  154. Log::debug('Alipay Generate Sign', [$params, $sign]);
  155. if (is_resource($privateKey)) {
  156. openssl_free_key($privateKey);
  157. }
  158. return $sign;
  159. }
  160. /**
  161. * Verify sign.
  162. *
  163. * @author yansongda <me@yansonga.cn>
  164. *
  165. * @param bool $sync
  166. * @param string|null $sign
  167. *
  168. * @throws InvalidConfigException
  169. */
  170. public static function verifySign(array $data, $sync = false, $sign = null): bool
  171. {
  172. $publicKey = self::$instance->ali_public_key;
  173. if (is_null($publicKey)) {
  174. throw new InvalidConfigException('Missing Alipay Config -- [ali_public_key]');
  175. }
  176. if (Str::endsWith($publicKey, '.crt')) {
  177. $publicKey = file_get_contents($publicKey);
  178. } elseif (Str::endsWith($publicKey, '.pem')) {
  179. $publicKey = openssl_pkey_get_public(
  180. Str::startsWith($publicKey, 'file://') ? $publicKey : 'file://'.$publicKey
  181. );
  182. } else {
  183. $publicKey = "-----BEGIN PUBLIC KEY-----\n".
  184. wordwrap($publicKey, 64, "\n", true).
  185. "\n-----END PUBLIC KEY-----";
  186. }
  187. $sign = $sign ?? $data['sign'];
  188. $toVerify = $sync ? json_encode($data, JSON_UNESCAPED_UNICODE) : self::getSignContent($data, true);
  189. $isVerify = 1 === openssl_verify($toVerify, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
  190. if (is_resource($publicKey)) {
  191. openssl_free_key($publicKey);
  192. }
  193. return $isVerify;
  194. }
  195. /**
  196. * Get signContent that is to be signed.
  197. *
  198. * @author yansongda <me@yansongda.cn>
  199. *
  200. * @param bool $verify
  201. */
  202. public static function getSignContent(array $data, $verify = false): string
  203. {
  204. ksort($data);
  205. $stringToBeSigned = '';
  206. foreach ($data as $k => $v) {
  207. if ($verify && 'sign' != $k && 'sign_type' != $k) {
  208. $stringToBeSigned .= $k.'='.$v.'&';
  209. }
  210. if (!$verify && '' !== $v && !is_null($v) && 'sign' != $k && '@' != substr($v, 0, 1)) {
  211. $stringToBeSigned .= $k.'='.$v.'&';
  212. }
  213. }
  214. Log::debug('Alipay Generate Sign Content Before Trim', [$data, $stringToBeSigned]);
  215. return trim($stringToBeSigned, '&');
  216. }
  217. /**
  218. * Convert encoding.
  219. *
  220. * @author yansongda <me@yansonga.cn>
  221. *
  222. * @param string|array $data
  223. * @param string $to
  224. * @param string $from
  225. */
  226. public static function encoding($data, $to = 'utf-8', $from = 'gb2312'): array
  227. {
  228. return Arr::encoding((array) $data, $to, $from);
  229. }
  230. /**
  231. * Get service config.
  232. *
  233. * @author yansongda <me@yansongda.cn>
  234. *
  235. * @param string|null $key
  236. * @param mixed|null $default
  237. *
  238. * @return mixed|null
  239. */
  240. public function getConfig($key = null, $default = null)
  241. {
  242. if (is_null($key)) {
  243. return $this->config->all();
  244. }
  245. if ($this->config->has($key)) {
  246. return $this->config[$key];
  247. }
  248. return $default;
  249. }
  250. /**
  251. * Get Base Uri.
  252. *
  253. * @author yansongda <me@yansongda.cn>
  254. *
  255. * @return string
  256. */
  257. public function getBaseUri()
  258. {
  259. return $this->baseUri;
  260. }
  261. /**
  262. * 生成应用证书SN.
  263. *
  264. * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
  265. *
  266. * @param $certPath
  267. *
  268. * @throws /Exception
  269. */
  270. public static function getCertSN($certPath): string
  271. {
  272. if (!is_file($certPath)) {
  273. throw new Exception('unknown certPath -- [getCertSN]');
  274. }
  275. $x509data = file_get_contents($certPath);
  276. if (false === $x509data) {
  277. throw new Exception('Alipay CertSN Error -- [getCertSN]');
  278. }
  279. openssl_x509_read($x509data);
  280. $certdata = openssl_x509_parse($x509data);
  281. if (empty($certdata)) {
  282. throw new Exception('Alipay openssl_x509_parse Error -- [getCertSN]');
  283. }
  284. $issuer_arr = [];
  285. foreach ($certdata['issuer'] as $key => $val) {
  286. $issuer_arr[] = $key.'='.$val;
  287. }
  288. $issuer = implode(',', array_reverse($issuer_arr));
  289. Log::debug('getCertSN:', [$certPath, $issuer, $certdata['serialNumber']]);
  290. return md5($issuer.$certdata['serialNumber']);
  291. }
  292. /**
  293. * 生成支付宝根证书SN.
  294. *
  295. * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
  296. *
  297. * @param $certPath
  298. *
  299. * @return string
  300. *
  301. * @throws /Exception
  302. */
  303. public static function getRootCertSN($certPath)
  304. {
  305. if (!is_file($certPath)) {
  306. throw new Exception('unknown certPath -- [getRootCertSN]');
  307. }
  308. $x509data = file_get_contents($certPath);
  309. if (false === $x509data) {
  310. throw new Exception('Alipay CertSN Error -- [getRootCertSN]');
  311. }
  312. $kCertificateEnd = '-----END CERTIFICATE-----';
  313. $certStrList = explode($kCertificateEnd, $x509data);
  314. $md5_arr = [];
  315. foreach ($certStrList as $one) {
  316. if (!empty(trim($one))) {
  317. $_x509data = $one.$kCertificateEnd;
  318. openssl_x509_read($_x509data);
  319. $_certdata = openssl_x509_parse($_x509data);
  320. if (in_array($_certdata['signatureTypeSN'], ['RSA-SHA256', 'RSA-SHA1'])) {
  321. $issuer_arr = [];
  322. foreach ($_certdata['issuer'] as $key => $val) {
  323. $issuer_arr[] = $key.'='.$val;
  324. }
  325. $_issuer = implode(',', array_reverse($issuer_arr));
  326. if (0 === strpos($_certdata['serialNumber'], '0x')) {
  327. $serialNumber = self::bchexdec($_certdata['serialNumber']);
  328. } else {
  329. $serialNumber = $_certdata['serialNumber'];
  330. }
  331. $md5_arr[] = md5($_issuer.$serialNumber);
  332. Log::debug('getRootCertSN Sub:', [$certPath, $_issuer, $serialNumber]);
  333. }
  334. }
  335. }
  336. return implode('_', $md5_arr);
  337. }
  338. /**
  339. * processingApiResult.
  340. *
  341. * @author yansongda <me@yansongda.cn>
  342. *
  343. * @param $data
  344. * @param $result
  345. *
  346. * @throws GatewayException
  347. * @throws InvalidConfigException
  348. * @throws InvalidSignException
  349. */
  350. protected static function processingApiResult($data, $result): Collection
  351. {
  352. $method = str_replace('.', '_', $data['method']).'_response';
  353. if (!isset($result['sign']) || '10000' != $result[$method]['code']) {
  354. throw new GatewayException('Get Alipay API Error:'.$result[$method]['msg'].(isset($result[$method]['sub_code']) ? (' - '.$result[$method]['sub_code']) : ''), $result);
  355. }
  356. if (self::verifySign($result[$method], true, $result['sign'])) {
  357. return new Collection($result[$method]);
  358. }
  359. Events::dispatch(new Events\SignFailed('Alipay', '', $result));
  360. throw new InvalidSignException('Alipay Sign Verify FAILED', $result);
  361. }
  362. /**
  363. * Set Http options.
  364. *
  365. * @author yansongda <me@yansongda.cn>
  366. */
  367. protected function setHttpOptions(): self
  368. {
  369. if ($this->config->has('http') && is_array($this->config->get('http'))) {
  370. $this->config->forget('http.base_uri');
  371. $this->httpOptions = $this->config->get('http');
  372. }
  373. return $this;
  374. }
  375. /**
  376. * 0x转高精度数字.
  377. *
  378. * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
  379. *
  380. * @param $hex
  381. *
  382. * @return int|string
  383. */
  384. private static function bchexdec($hex)
  385. {
  386. $dec = 0;
  387. $len = strlen($hex);
  388. for ($i = 1; $i <= $len; ++$i) {
  389. if (ctype_xdigit($hex[$i - 1])) {
  390. $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
  391. }
  392. }
  393. return str_replace('.00', '', $dec);
  394. }
  395. }