WxApi.php 9.9 KB

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