ApiV3Request.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <?php
  2. namespace app\common\services\wechatApiV3;
  3. use app\common\services\Utils;
  4. use Ixudra\Curl\Facades\Curl;
  5. class ApiV3Request
  6. {
  7. /**
  8. * @var ApiV3Config
  9. */
  10. public $config;
  11. private $url;
  12. private $method;
  13. private $timestamp;
  14. private $params;
  15. private $nonceStr;
  16. private $certificationType = 'WECHATPAY2-SHA256-RSA2048';
  17. public $accept_language; //应答语种
  18. private $encrypt; //含有加密数据
  19. public function __construct(ApiV3Config $config)
  20. {
  21. $this->config = $config;
  22. }
  23. /**
  24. * @return string[]
  25. * @throws \Exception
  26. */
  27. private function header():array
  28. {
  29. $headers = [
  30. 'Accept-Language' => $this->accept_language,
  31. 'Content-Type' => 'application/json',
  32. 'Accept' => 'application/json',
  33. 'User-Agent' => $_SERVER['HTTP_USER_AGENT'] ? : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36',
  34. ];
  35. if ($this->encrypt) {//加密数据需传递证书序列号
  36. $headers['Wechatpay-Serial'] = $this->config->platformSerialNo();
  37. }
  38. return $headers;
  39. }
  40. private function authorization()
  41. {
  42. $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
  43. $this->config->mchId(),
  44. $this->nonceStr,
  45. $this->timestamp,
  46. $this->config->apiSerialNo(),
  47. $this->signature($this->method,$this->canonicalUrl(),$this->timestamp,$this->nonceStr,$this->getBody())
  48. );
  49. return $this->certificationType . ' ' . $token;
  50. }
  51. private function signature($method,$canonicalUrl,$timestamp,$nonce,$body)
  52. {
  53. $message = $method."\n".
  54. $canonicalUrl."\n".
  55. $timestamp."\n".
  56. $nonce."\n".
  57. $body."\n";;
  58. openssl_sign($message, $raw_sign, $this->config->privateKey(), 'sha256WithRSAEncryption');
  59. $sign = base64_encode($raw_sign);
  60. return $sign;
  61. }
  62. private function canonicalUrl()
  63. {
  64. $url_parts = parse_url($this->url);
  65. return $url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "");
  66. }
  67. private function getBody()
  68. {
  69. return $this->params ? json_encode($this->params) : '';
  70. }
  71. private function createNonceStr($length = 32)
  72. {
  73. $str = '1234567890abcdefghijklmnopqrstuvwxyz';
  74. $result = '';
  75. for ($i = 1;$i <= $length;$i++) {
  76. $result .= substr($str,rand(0,(strlen($str)-1)),1);
  77. }
  78. $this->nonceStr = strtoupper($result);
  79. }
  80. /**
  81. * @param $url //请求url
  82. * @param $params //请求参数(有些GET请求参数需要在url拼上的请勿传到此参数,自行拼接到url)
  83. * @param $request_method //请求方式
  84. * @param $is_encrypt //是否有加密数据
  85. * @param $verify_sign //是否验签,验签可能存在获取平台证书错误,造成记录更新错误,所以自行根据接口重要程度判断要不要对返回数据进行验签
  86. * @param $accept_language //应答语言类型,默认中文
  87. * @return array
  88. * @throws \Exception
  89. */
  90. public function httpRequest($url,$params = [],$request_method = 'GET',$is_encrypt = false,$verify_sign = 0,$accept_language = 'zh-CN')
  91. {
  92. $this->url = $url;
  93. $this->params = $params;
  94. $this->method = $request_method;
  95. $this->encrypt = $is_encrypt;
  96. $this->accept_language = $accept_language;
  97. $this->timestamp = time();
  98. $this->createNonceStr();
  99. $method = "send" . ucfirst(strtolower($this->method));
  100. if (!method_exists($this,$method)) {
  101. throw new \Exception('method not found');
  102. }
  103. $res = $this->$method($this->url,$this->params,$this->authorization(),$this->header());
  104. $res_header = $res->headers;
  105. $res_content = $res->content;
  106. if (ApiV3Status::isCheckSign(($res_content['code']?:'')) && $verify_sign) {
  107. $this->verifySign($res_header['Wechatpay-Signature'],$res_header['Wechatpay-Timestamp'],$res_header['Wechatpay-Nonce'],$res_content,$res_header['Wechatpay-Serial']);
  108. }
  109. //处理统一返回体
  110. return [
  111. 'request_id' => $res_header['Request-ID'], //唯一请求ID
  112. 'http_code' => $res->status, //请求状态码
  113. 'code' => ApiV3Status::returnCode($res->status), //状态
  114. 'message' => $res_content['message'] ? : '', //错误消息
  115. 'data' => $res_content //应答消息体
  116. ];
  117. }
  118. /**
  119. * @param $url
  120. * @param $params
  121. * @param $header
  122. * @return array|mixed|\stdClass
  123. */
  124. private function sendGet($url,$params,$authorization,$header)
  125. {
  126. return Curl::to($url)
  127. ->withHeaders($header)
  128. ->withAuthorization($authorization)
  129. ->withData($params)
  130. ->asJson(true)
  131. ->withResponseHeaders()
  132. ->returnResponseObject()
  133. ->get();
  134. }
  135. /**
  136. * @param $url
  137. * @param $params
  138. * @param $header
  139. * @return array|mixed|\stdClass
  140. */
  141. private function sendPost($url,$params,$authorization,$header)
  142. {
  143. return Curl::to($url)
  144. ->withHeaders($header)
  145. ->withAuthorization($authorization)
  146. ->withData($params)
  147. ->asJson(true)
  148. ->withResponseHeaders()
  149. ->returnResponseObject()
  150. ->post();
  151. }
  152. /**
  153. * @param $path
  154. * @param $file_name
  155. * @return bool
  156. * @throws \Exception
  157. */
  158. public function platformCertApply($path,$file_name)
  159. {
  160. $this->url = 'https://api.mch.weixin.qq.com/v3/certificates';
  161. $this->params = [];
  162. $this->method = 'GET';
  163. $this->timestamp = time();
  164. $this->createNonceStr();
  165. $res = $this->sendGet($this->url,[],$this->authorization(),$this->header());
  166. $res_content = $res->content;
  167. if (!ApiV3Status::returnCode($res->status)) {
  168. throw new \Exception($res_content['message'] ? : '平台证书请求错误!');
  169. }
  170. $content = $res_content['data'];
  171. $data = $this->config->encrypt()->decrypt(
  172. $content[0]['encrypt_certificate']['associated_data'],
  173. $content[0]['encrypt_certificate']['nonce'],
  174. $content[0]['encrypt_certificate']['ciphertext']);
  175. Utils::mkdirs($path);
  176. if (!$data || !file_put_contents($path . $file_name,$data)) {
  177. throw new \Exception('平台证书更新保存失败!');
  178. }
  179. return true;
  180. }
  181. /**
  182. * 回调及应答签名验证
  183. * @param $sign
  184. * @param $timestamp
  185. * @param $nonce
  186. * @param $body
  187. * @param $serial
  188. * @return bool
  189. * @throws \Exception
  190. */
  191. public function verifySign($sign,$timestamp,$nonce,$body,$serial)
  192. {
  193. if ($serial != $this->config->platformSerialNo()) {
  194. //平台证书需更新
  195. $this->config->platformCert(1);
  196. }
  197. $body = $body ? json_encode($body,256) : '';
  198. $message = $timestamp . "\n"
  199. .$nonce . "\n"
  200. .$body . "\n";
  201. if (!openssl_verify($message,base64_decode($sign),$this->config->platformCertKey(), OPENSSL_ALGO_SHA256)) {
  202. \Log::debug('微信支付apiV3--应答签名验证失败',[$sign,$timestamp,$nonce,$body,$serial]);
  203. throw new \Exception('应答签名验证失败');
  204. }
  205. return true;
  206. }
  207. }