BaseClient.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <?php
  2. /**
  3. * +----------------------------------------------------------------------
  4. * | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  5. * +----------------------------------------------------------------------
  6. * | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
  7. * +----------------------------------------------------------------------
  8. * | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  9. * +----------------------------------------------------------------------
  10. * | Author: CRMEB Team <admin@crmeb.com>
  11. * +----------------------------------------------------------------------
  12. */
  13. namespace crmeb\services\easywechat\v3pay;
  14. use crmeb\exceptions\PayException;
  15. use crmeb\services\easywechat\Application;
  16. use EasyWeChat\Core\AbstractAPI;
  17. use EasyWeChat\Core\AccessToken;
  18. use EasyWeChat\Core\Exceptions\HttpException;
  19. use EasyWeChat\Core\Exceptions\InvalidConfigException;
  20. use EasyWeChat\Core\Http;
  21. use EasyWeChat\Encryption\EncryptionException;
  22. use think\exception\InvalidArgumentException;
  23. class BaseClient extends AbstractAPI
  24. {
  25. use Certficates;
  26. /**
  27. * @var Application
  28. */
  29. protected $app;
  30. const BASE_URL = 'https://api.mch.weixin.qq.com/';
  31. const KEY_LENGTH_BYTE = 32;
  32. const AUTH_TAG_LENGTH_BYTE = 16;
  33. /**
  34. * BaseClient constructor.
  35. * @param AccessToken $accessToken
  36. * @param $app
  37. */
  38. public function __construct(AccessToken $accessToken, $app)
  39. {
  40. parent::__construct($accessToken);
  41. $this->app = $app;
  42. }
  43. /**
  44. * request.
  45. *
  46. * @param string $endpoint
  47. * @param string $method
  48. * @param array $options
  49. * @param bool $returnResponse
  50. */
  51. public function request(string $endpoint, string $method = 'POST', array $options = [], $serial = true)
  52. {
  53. $body = $options['body'] ?? '';
  54. if (isset($options['json'])) {
  55. $body = json_encode($options['json']);
  56. $options['body'] = $body;
  57. unset($options['json']);
  58. }
  59. $headers = [
  60. 'Content-Type' => 'application/json',
  61. 'User-Agent' => 'curl',
  62. 'Accept' => 'application/json',
  63. 'Authorization' => $this->getAuthorization($endpoint, $method, $body),
  64. ];
  65. $options['headers'] = array_merge($headers, ($options['headers'] ?? []));
  66. if ($serial) {
  67. $options['headers']['Wechatpay-Serial'] = $this->getCertficatescAttr('serial_no');
  68. }
  69. return $this->_doRequestCurl($method, self::BASE_URL . $endpoint, $options);
  70. }
  71. /**
  72. * @param $method
  73. * @param $location
  74. * @param array $options
  75. * @return mixed
  76. */
  77. private function _doRequestCurl($method, $location, $options = [])
  78. {
  79. $curl = curl_init();
  80. // POST数据设置
  81. if (strtolower($method) === 'post') {
  82. curl_setopt($curl, CURLOPT_POST, true);
  83. curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data'] ?? $options['body'] ?? '');
  84. }
  85. // CURL头信息设置
  86. if (!empty($options['headers'])) {
  87. $headers = [];
  88. foreach ($options['headers'] as $k => $v) {
  89. $headers[] = "$k: $v";
  90. }
  91. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  92. }
  93. curl_setopt($curl, CURLOPT_URL, $location);
  94. curl_setopt($curl, CURLOPT_HEADER, true);
  95. curl_setopt($curl, CURLOPT_TIMEOUT, 60);
  96. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  97. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  98. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  99. $content = curl_exec($curl);
  100. $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  101. curl_close($curl);
  102. return json_decode(substr($content, $headerSize), true);
  103. }
  104. /**
  105. * To id card, mobile phone number and other fields sensitive information encryption.
  106. *
  107. * @param string $string
  108. *
  109. * @return string
  110. */
  111. protected function encryptSensitiveInformation(string $string)
  112. {
  113. $certificates = $this->app->certficates->get()['certificates'];
  114. if (null === $certificates) {
  115. throw new InvalidConfigException('config certificate connot be empty.');
  116. }
  117. $encrypted = '';
  118. if (openssl_public_encrypt($string, $encrypted, $certificates, OPENSSL_PKCS1_OAEP_PADDING)) {
  119. //base64编码
  120. $sign = base64_encode($encrypted);
  121. } else {
  122. throw new EncryptionException('Encryption of sensitive information failed');
  123. }
  124. return $sign;
  125. }
  126. /**
  127. * @param string $url
  128. * @param string $method
  129. * @param string $body
  130. * @return string
  131. */
  132. protected function getAuthorization(string $url, string $method, string $body)
  133. {
  134. $nonceStr = uniqid();
  135. $timestamp = time();
  136. $message = $method . "\n" .
  137. '/' . $url . "\n" .
  138. $timestamp . "\n" .
  139. $nonceStr . "\n" .
  140. $body . "\n";
  141. openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
  142. $sign = base64_encode($raw_sign);
  143. $schema = 'WECHATPAY2-SHA256-RSA2048 ';
  144. $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
  145. $this->app['config']['v3_payment']['mchid'], $nonceStr, $timestamp, $this->app['config']['v3_payment']['serial_no'], $sign);
  146. return $schema . $token;
  147. }
  148. /**
  149. * 获取商户私钥
  150. * @return bool|resource
  151. */
  152. protected function getPrivateKey()
  153. {
  154. $key_path = $this->app['config']['v3_payment']['key_path'];
  155. if (!file_exists($key_path)) {
  156. throw new \InvalidArgumentException(
  157. "SSL certificate not found: {$key_path}"
  158. );
  159. }
  160. return openssl_pkey_get_private(file_get_contents($key_path));
  161. }
  162. /**
  163. * 获取商户公钥
  164. * @return bool|resource
  165. */
  166. protected function getPublicKey()
  167. {
  168. $key_path = $this->app['config']['v3_payment']['cert_path'];
  169. if (!file_exists($key_path)) {
  170. throw new \InvalidArgumentException(
  171. "SSL certificate not found: {$key_path}"
  172. );
  173. }
  174. return openssl_pkey_get_public(file_get_contents($key_path));
  175. }
  176. /**
  177. * 替换url
  178. * @param string $url
  179. * @param $search
  180. * @param $replace
  181. * @return array|string|string[]
  182. */
  183. public function getApiUrl(string $url, $search, $replace)
  184. {
  185. $newSearch = [];
  186. foreach ($search as $key) {
  187. $newSearch[] = '{' . $key . '}';
  188. }
  189. return str_replace($newSearch, $replace, $url);
  190. }
  191. /**
  192. * @param int $padding
  193. */
  194. private static function paddingModeLimitedCheck(int $padding): void
  195. {
  196. if (!($padding === OPENSSL_PKCS1_OAEP_PADDING || $padding === OPENSSL_PKCS1_PADDING)) {
  197. throw new PayException(sprintf("Doesn't supported padding mode(%d), here only support OPENSSL_PKCS1_OAEP_PADDING or OPENSSL_PKCS1_PADDING.", $padding));
  198. }
  199. }
  200. /**
  201. * 加密数据
  202. * @param string $plaintext
  203. * @param int $padding
  204. * @return string
  205. */
  206. public function encryptor(string $plaintext, int $padding = OPENSSL_PKCS1_OAEP_PADDING)
  207. {
  208. self::paddingModeLimitedCheck($padding);
  209. if (!openssl_public_encrypt($plaintext, $encrypted, $this->getPublicKey(), $padding)) {
  210. throw new PayException('Encrypting the input $plaintext failed, please checking your $publicKey whether or nor correct.');
  211. }
  212. return base64_encode($encrypted);
  213. }
  214. /**
  215. * decrypt ciphertext.
  216. *
  217. * @param array $encryptCertificate
  218. *
  219. * @return string
  220. */
  221. public function decrypt(array $encryptCertificate)
  222. {
  223. $ciphertext = base64_decode($encryptCertificate['ciphertext'], true);
  224. $associatedData = $encryptCertificate['associated_data'];
  225. $nonceStr = $encryptCertificate['nonce'];
  226. $aesKey = $this->app['config']['v3_payment']['key'];
  227. try {
  228. // ext-sodium (default installed on >= PHP 7.2)
  229. if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
  230. return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
  231. }
  232. // ext-libsodium (need install libsodium-php 1.x via pecl)
  233. if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
  234. return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
  235. }
  236. // openssl (PHP >= 7.1 support AEAD)
  237. if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
  238. $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
  239. $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
  240. return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
  241. }
  242. } catch (\Exception $exception) {
  243. throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
  244. } catch (\SodiumException $exception) {
  245. throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
  246. }
  247. throw new InvalidArgumentException('AEAD_AES_256_GCM 需要 PHP 7.1 以上或者安装 libsodium-php');
  248. }
  249. }