WxApi.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <?php
  2. /**
  3. * 封装微信api签名安全类
  4. */
  5. namespace common\components\wxapi;
  6. use phpseclib\Crypt\RSA;
  7. use yii\base\ErrorException;
  8. class WxApi
  9. {
  10. private $appId;
  11. private $aes;
  12. private $rsa;
  13. private $cert;
  14. private $url;
  15. private $accessToken;
  16. public function __construct($appId, $accessToken)
  17. {
  18. $this->appId = $appId;
  19. $this->accessToken = $accessToken;
  20. $this->aes['sn'] = getenv('AES_SN');
  21. $this->aes['key'] = getenv('AES_KEY');
  22. $this->rsa['sn'] = getenv('RSA_SN');
  23. $env = getenv('YII_ENV') == 'production' ? 'production' : 'dev';
  24. $this->rsa['rsa-public-key'] = file_get_contents(__DIR__ . '/' . $env . '/rsa-public-key.txt');
  25. $this->rsa['rsa-private-key'] = file_get_contents(__DIR__. '/' . $env . '/rsa-private-key.txt');
  26. $this->cert['sn'] = getenv('CERT_SN');
  27. $this->cert['cert-key'] = file_get_contents(__DIR__ . '/' . $env . '/api.cer');
  28. }
  29. /**
  30. * Name:对外方法用于所有微信api的请求方法
  31. * User: zcw
  32. * Date: 2023/7/14
  33. * Time: 9:51
  34. * @param $url
  35. * @param $reqData
  36. * @throws ErrorException
  37. * @throws \Exception
  38. */
  39. public function request($url, $reqData)
  40. {
  41. $this->url = $url;
  42. $urls = $url . "?access_token=" . $this->accessToken;
  43. //1.数据加密
  44. $newReData = $this->getRequestParam($url, $reqData);
  45. //2.获取签名
  46. $signature = $this->getSignature($newReData);
  47. //本地验签 非必需
  48. $checkLocalSig = $this->checkLocalSignature($newReData, $signature);
  49. if (!$checkLocalSig) {
  50. throw new ErrorException('本地验签错误');
  51. }
  52. $appId = $this->appId;
  53. $headerArray = ['Wechatmp-Appid:' . $appId, 'Wechatmp-TimeStamp:' . $newReData['ts'], 'Wechatmp-Signature:' . $signature];
  54. \Yii::info("请求头: " . json_encode($headerArray), 'wxapi_debug');
  55. $data = $this->curlPost($urls, $newReData['reqData'], $headerArray);
  56. $headers = $this->httpParseHeaders($data['header']);
  57. $body = json_decode($data['body'], true);
  58. \Yii::info("响应: " . json_encode($body), 'wxapi_debug');
  59. //请求平台报错
  60. if (isset($body['errcode'])) {
  61. \Yii::error("微信API错误: " . json_encode($body), 'wxapi_debug');
  62. throw new ErrorException($body['errmsg']);
  63. }
  64. // 3.响应参数验签
  65. // $vertify = $this->vertifyResponse($data);
  66. // if (!$vertify) {
  67. // throw new ErrorException('响应参数验签失败');
  68. // }
  69. //4.参数解密
  70. return $this->jM($headers['Wechatmp-TimeStamp'], $body);
  71. }
  72. /**
  73. * Name:post请求
  74. * User: zcw
  75. * Date: 2023/7/14
  76. * Time: 9:19
  77. * @param $url
  78. * @param $field
  79. * @param $header
  80. * @return array
  81. */
  82. public function curlPost($url, $field, $header)
  83. {
  84. $headerArray = array("Content-type:application/json;charset=utf-8", "Accept:application/json");
  85. $headerArray = array_merge($headerArray, $header);
  86. $curl = curl_init();
  87. curl_setopt($curl, CURLOPT_HTTPHEADER, $headerArray);
  88. curl_setopt($curl, CURLOPT_URL, $url);
  89. curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
  90. curl_setopt($curl, CURLOPT_POST, 1);
  91. curl_setopt($curl, CURLOPT_POSTFIELDS, $field);
  92. //输出响应头部
  93. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  94. curl_setopt($curl, CURLOPT_HEADER, true);
  95. $str = curl_exec($curl);
  96. $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  97. $headers = substr($str, 0, $headerSize);
  98. $body = substr($str, $headerSize);
  99. curl_close($curl);
  100. return ['body' => $body, 'header' => $headers];
  101. }
  102. /**
  103. * Name:对外方法用于所有微信api的请求通道
  104. * User: zcw
  105. * Date: 2023/7/14
  106. * Time: 9:21
  107. * @param $url
  108. * @param $reqData
  109. * @return array
  110. * @throws \Exception
  111. */
  112. public function getRequestParam($url, $reqData)
  113. {
  114. $key = base64_decode($this->aes['key']);
  115. $sn = $this->aes['sn'];
  116. $appId = $this->appId;
  117. $time = time();
  118. //16位随机字符
  119. $nonce = rtrim(base64_encode(random_bytes(16)), '=');
  120. $addReq = ["_n" => $nonce, "_appid" => $appId, "_timestamp" => $time];
  121. $realReq = array_merge($addReq, $reqData);
  122. $realReq = json_encode($realReq);
  123. //额外参数
  124. $aad = $url . "|" . $appId . "|" . $time . "|" . $sn;
  125. //12位随机字符
  126. $iv = random_bytes(12);
  127. $cipher = openssl_encrypt($realReq, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $tag, $aad);
  128. $iv = base64_encode($iv);
  129. $data = base64_encode($cipher);
  130. $authTag = base64_encode($tag);
  131. $reqData = ["iv" => $iv, "data" => $data, "authtag" => $authTag];
  132. //校验本地加密是否正确 非必须
  133. // $checkParam = $this->checkParam($key, $authTag, $iv, $data, $aad);
  134. return ['ts' => $time, 'reqData' => json_encode($reqData)];
  135. }
  136. /**
  137. * Name:请求前本地验签
  138. * User: zcw
  139. * Date: 2023/7/14
  140. * Time: 9:57
  141. * @param $key
  142. * @param $authTag
  143. * @param $iv
  144. * @param $data
  145. * @param $aad
  146. */
  147. private function checkParam($key, $authTag, $iv, $data, $aad)
  148. {
  149. $iv = base64_decode($iv);
  150. $data = base64_decode($data);
  151. $authTag = base64_decode($authTag);
  152. return openssl_decrypt($data, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad);
  153. }
  154. /**
  155. * Name:获取签名
  156. * User: zcw
  157. * Date: 2023/7/14
  158. * Time: 10:03
  159. * @param array $newRe
  160. */
  161. private function getSignature(array $newRe)
  162. {
  163. $time = $newRe['ts'];
  164. $key = $this->rsa['rsa-private-key'];
  165. $url = $this->url;
  166. $appId = $this->appId;
  167. $reqData = $newRe['reqData'];
  168. $payload = "$url\n$appId\n$time\n$reqData";
  169. $rsa = new RSA();
  170. $rsa->loadKey($key);
  171. $rsa->setHash("sha256");
  172. $rsa->setMGFHash("sha256");
  173. $signature = $rsa->sign($payload);
  174. return base64_encode($signature);
  175. }
  176. /**
  177. * Name:请求前本地验签
  178. * User: zcw
  179. * Date: 2023/7/14
  180. * Time: 10:11
  181. * @param array $newRe
  182. * @param string $signature
  183. */
  184. private function checkLocalSignature(array $newRe, string $signature)
  185. {
  186. $signature = base64_decode($signature);
  187. $rsaPubKey = $this->rsa['rsa-public-key'];
  188. $appId = $this->appId;
  189. $url = $this->url;
  190. $time = $newRe['ts'];
  191. $reqData = $newRe['reqData'];
  192. $payload = "$url\n$appId\n$time\n$reqData";
  193. $payload = utf8_encode($payload);
  194. $rsa = new RSA();
  195. $rsa->loadKey($rsaPubKey);
  196. $rsa->setHash("sha256");
  197. $rsa->setMGFHash("sha256");
  198. return $rsa->verify($payload, $signature);
  199. }
  200. /**
  201. * Name:解析头部信息
  202. * User: zcw
  203. * Date: 2023/7/14
  204. * Time: 10:28
  205. * @param $headerString
  206. * @return array
  207. */
  208. private function httpParseHeaders($headerString)
  209. {
  210. $headers = [];
  211. $lines = explode("\r\n", $headerString);
  212. foreach ($lines as $line) {
  213. $line = trim($line);
  214. if (!empty($line)) {
  215. $parts = explode(':', $line, 2);
  216. $key = trim($parts[0]);
  217. $value = isset($parts[1]) ? trim($parts[1]) : '';
  218. $headers[$key] = $value;
  219. }
  220. }
  221. return $headers;
  222. }
  223. /**
  224. * Name:解密参数
  225. * User: zcw
  226. * Date: 2023/7/14
  227. * Time: 10:31
  228. * @param $ts
  229. * @param $body
  230. * @return mixed|null
  231. * @throws ErrorException
  232. */
  233. private function jM($ts, $body)
  234. {
  235. $url = $this->url;
  236. $appId = $this->appId;
  237. $sn = $this->aes['sn'];
  238. $aad = $url . '|' . $appId . '|' . $ts . '|' . $sn;
  239. $key = $this->aes['key'];
  240. $key = base64_decode($key);
  241. $iv = base64_decode($body['iv']);
  242. $data = base64_decode($body['data']);
  243. $authTag = base64_decode($body['authtag']);
  244. $result = openssl_decrypt($data, "aes-256-gcm", $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad);
  245. if (!$result) {
  246. throw new ErrorException();
  247. }
  248. $result = json_decode($result, true);
  249. return $result;
  250. }
  251. /**
  252. * Name:验证响应值
  253. * User: zcw
  254. * Date: 2023/7/14
  255. * Time: 11:16
  256. * @param $data
  257. */
  258. private function vertifyResponse($data)
  259. {
  260. $headers = $this->httpParseHeaders($data['header']);
  261. $nowTime = time();
  262. $reTime = $headers['Wechatmp-TimeStamp'];
  263. $appId = $this->appId;
  264. $cert = $this->cert;
  265. $sn = $cert['sn'];
  266. $key = $cert['cert-key'];
  267. $url = $this->url;
  268. if ($appId != $headers['Wechatmp-Appid'] || $nowTime - $reTime > 300){
  269. throw new \ErrorException('返回值安全字段校验失败');
  270. }
  271. if ($sn == $headers['Wechatmp-Serial']) {
  272. $signature = $headers['Wechatmp-Signature'];
  273. } elseif ($sn == $headers['Wechatmp-Serial-Deprecated']) {
  274. $signature = $headers['Wechatmp-Signature-Deprecated'];
  275. } else {
  276. throw new \ErrorException('返回值sn不匹配');
  277. }
  278. $reData = $data['body'];
  279. $payload = "$url\n$appId\n$reTime\n$reData";
  280. $payload = utf8_encode($payload);
  281. $signature = base64_decode($signature);
  282. $rsa = new RSA();
  283. $rsa->loadKey($key);
  284. $rsa->setHash("sha256");
  285. $rsa->setMGFHash("sha256");
  286. return $rsa->verify($payload, $signature);
  287. }
  288. }