BaseClient.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. <?php
  2. namespace ln\services\easywechat;
  3. use EasyWeChat\Core\AbstractAPI;
  4. use EasyWeChat\Core\AccessToken;
  5. use EasyWeChat\Core\Exceptions\HttpException;
  6. use EasyWeChat\Core\Exceptions\InvalidConfigException;
  7. use EasyWeChat\Core\Http;
  8. use EasyWeChat\Encryption\EncryptionException;
  9. use think\exception\InvalidArgumentException;
  10. class BaseClient extends AbstractAPI
  11. {
  12. protected $app;
  13. const KEY_LENGTH_BYTE = 32;
  14. const AUTH_TAG_LENGTH_BYTE = 16;
  15. public function __construct(AccessToken $accessToken, $app)
  16. {
  17. parent::__construct($accessToken);
  18. $this->app = $app;
  19. }
  20. /**
  21. * @param $api
  22. * @param $params
  23. * @return \EasyWeChat\Support\Collection|null
  24. * @throws \EasyWeChat\Core\Exceptions\HttpException
  25. */
  26. protected function httpPostJson($api, $params)
  27. {
  28. try {
  29. return $this->parseJSON('json', [$api, $params]);
  30. } catch (HttpException $e) {
  31. $code = $e->getCode();
  32. throw new HttpException("接口异常[$code]" . ($e->getMessage()), $code);
  33. }
  34. }
  35. /**
  36. * @param $api
  37. * @param $params
  38. * @return \EasyWeChat\Support\Collection|null
  39. * @throws \EasyWeChat\Core\Exceptions\HttpException
  40. */
  41. protected function httpPost($api, $params)
  42. {
  43. try {
  44. return $this->parseJSON('post', [$api, $params]);
  45. } catch (HttpException $e) {
  46. $code = $e->getCode();
  47. throw new HttpException("接口异常[$code]" . ($e->getMessage()), $code);
  48. }
  49. }
  50. /**
  51. * @param $api
  52. * @param $params
  53. * @return \EasyWeChat\Support\Collection|null
  54. * @throws \EasyWeChat\Core\Exceptions\HttpException
  55. */
  56. protected function httpGet($api, $params)
  57. {
  58. try {
  59. return $this->parseJSON('get', [$api, $params]);
  60. } catch (HttpException $e) {
  61. $code = $e->getCode();
  62. throw new HttpException("接口异常[$code]" . ($e->getMessage()), $code);
  63. }
  64. }
  65. /**
  66. * request.
  67. *
  68. * @param string $endpoint
  69. * @param string $method
  70. * @param array $options
  71. * @param bool $returnResponse
  72. */
  73. public function request(string $endpoint, string $method = 'POST', array $options = [], $serial = true)
  74. {
  75. $sign_body = $options['sign_body'] ?? '';
  76. $headers = [
  77. 'Content-Type' => 'application/json',
  78. 'User-Agent' => 'curl',
  79. 'Accept' => 'application/json',
  80. 'Authorization' => $this->getAuthorization($endpoint, $method, $sign_body),
  81. // 'Wechatpay-Serial' => $this->app['config']['payment']['serial_no']
  82. ];
  83. $options['headers'] = array_merge($headers, ($options['headers'] ?? []));
  84. if ($serial) $options['headers']['Wechatpay-Serial'] = $this->app->certficates->get()['serial_no'];
  85. Http::setDefaultOptions($options);
  86. return $this->_doRequestCurl($method, 'https://api.mch.weixin.qq.com' . $endpoint, $options);
  87. }
  88. private function _doRequestCurl($method, $location, $options = [])
  89. {
  90. $curl = curl_init();
  91. // POST数据设置
  92. if (strtolower($method) === 'post') {
  93. curl_setopt($curl, CURLOPT_POST, true);
  94. curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data'] ?? $options['sign_body'] ?? '');
  95. }
  96. // CURL头信息设置
  97. if (!empty($options['headers'])) {
  98. $headers = [];
  99. foreach ($options['headers'] as $k => $v) {
  100. $headers[] = "$k: $v";
  101. }
  102. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  103. }
  104. curl_setopt($curl, CURLOPT_URL, $location);
  105. curl_setopt($curl, CURLOPT_HEADER, true);
  106. curl_setopt($curl, CURLOPT_TIMEOUT, 60);
  107. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  108. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  109. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  110. $content = curl_exec($curl);
  111. $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  112. curl_close($curl);
  113. return json_decode(substr($content, $headerSize), true);
  114. }
  115. /**
  116. * get sensitive fields name.
  117. *
  118. * @return array
  119. */
  120. protected function getSensitiveFieldsName()
  121. {
  122. return [
  123. 'contact_name',
  124. 'contact_id_number',
  125. 'mobile_phone',
  126. 'contact_email',
  127. 'id_card_name',
  128. 'id_card_number',
  129. 'id_doc_name',
  130. 'id_doc_number',
  131. 'name',
  132. 'id_number',
  133. 'account_name',
  134. 'account_number',
  135. 'id_card_name',
  136. 'id_card_number',
  137. 'account_name',
  138. 'contact_name',
  139. 'contact_id_card_number',
  140. 'mobile_phone',
  141. 'contact_email'
  142. ];
  143. }
  144. /**
  145. * To id card, mobile phone number and other fields sensitive information encryption.
  146. *
  147. * @param string $string
  148. *
  149. * @return string
  150. */
  151. protected function encryptSensitiveInformation(string $string)
  152. {
  153. $certificates = $this->app->certficates->get()['certificates'];
  154. if (null === $certificates) {
  155. throw new InvalidConfigException('config certificate connot be empty.');
  156. }
  157. $encrypted = '';
  158. if (openssl_public_encrypt($string, $encrypted, $certificates, OPENSSL_PKCS1_OAEP_PADDING)) {
  159. //base64编码
  160. $sign = base64_encode($encrypted);
  161. } else {
  162. throw new EncryptionException('Encryption of sensitive information failed');
  163. }
  164. return $sign;
  165. }
  166. /**
  167. * processing parameters contain fields that require sensitive information encryption.
  168. *
  169. * @param array $params
  170. *
  171. * @return array
  172. */
  173. protected function processParams(array $params)
  174. {
  175. $sensitive_fields = $this->getSensitiveFieldsName();
  176. foreach ($params as $k => $v) {
  177. if (is_array($v)) {
  178. $params[$k] = $this->processParams($v);
  179. } else {
  180. if (in_array($k, $sensitive_fields, true)) {
  181. $params[$k] = $this->encryptSensitiveInformation($v);
  182. }
  183. }
  184. }
  185. return $params;
  186. }
  187. /**
  188. * @param string $url
  189. * @param string $method
  190. * @param string $body
  191. * @return string
  192. */
  193. protected function getAuthorization(string $url, string $method, string $body)
  194. {
  195. $nonce_str = uniqid();
  196. $timestamp = time();
  197. $message = $method . "\n" .
  198. $url . "\n" .
  199. $timestamp . "\n" .
  200. $nonce_str . "\n" .
  201. $body . "\n";
  202. openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
  203. $sign = base64_encode($raw_sign);
  204. $schema = 'WECHATPAY2-SHA256-RSA2048 ';
  205. $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
  206. $this->app['config']['service_payment']['merchant_id'], $nonce_str, $timestamp, $this->app['config']['service_payment']['serial_no'], $sign);
  207. return $schema . $token;
  208. }
  209. /**
  210. * 获取商户私钥
  211. * @return bool|resource
  212. */
  213. protected function getPrivateKey()
  214. {
  215. $key_path = $this->app['config']['service_payment']['key_path'];
  216. if (!file_exists($key_path)) {
  217. throw new \InvalidArgumentException(
  218. "SSL certificate not found: {$key_path}"
  219. );
  220. }
  221. return openssl_pkey_get_private(file_get_contents($key_path));
  222. }
  223. /**
  224. * decrypt ciphertext.
  225. *
  226. * @param array $encryptCertificate
  227. *
  228. * @return string
  229. */
  230. public function decrypt(array $encryptCertificate)
  231. {
  232. $ciphertext = base64_decode($encryptCertificate['ciphertext'], true);
  233. $associatedData = $encryptCertificate['associated_data'];
  234. $nonceStr = $encryptCertificate['nonce'];
  235. $aesKey = $this->app['config']['service_payment']['apiv3_key'];
  236. try {
  237. // ext-sodium (default installed on >= PHP 7.2)
  238. if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
  239. return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
  240. }
  241. // ext-libsodium (need install libsodium-php 1.x via pecl)
  242. if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
  243. return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
  244. }
  245. // openssl (PHP >= 7.1 support AEAD)
  246. if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
  247. $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
  248. $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
  249. return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
  250. }
  251. } catch (\Exception $exception) {
  252. throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
  253. } catch (\SodiumException $exception) {
  254. throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
  255. }
  256. throw new InvalidArgumentException('AEAD_AES_256_GCM 需要 PHP 7.1 以上或者安装 libsodium-php');
  257. }
  258. }