WeChat.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. namespace App\Channels\Library;
  3. use DOMDocument;
  4. use Exception;
  5. use Log;
  6. use Str;
  7. class WeChat
  8. {
  9. public string $key;
  10. public string $iv;
  11. public function __construct()
  12. {
  13. $this->key = base64_decode(sysConfig('wechat_encodingAESKey').'=');
  14. $this->iv = substr($this->key, 0, 16);
  15. }
  16. public function encryptMsg(string $sReplyMsg, ?int $sTimeStamp, string $sNonce, string &$sEncryptMsg): int
  17. { // 将公众平台回复用户的消息加密打包.
  18. $array = $this->prpcrypt_encrypt($sReplyMsg); // 加密
  19. if ($array[0] !== 0) {
  20. return $array[0];
  21. }
  22. $encrypt = $array[1];
  23. $sTimeStamp = $sTimeStamp ?? time();
  24. $array = $this->getSHA1($sTimeStamp, $sNonce, $encrypt);
  25. if ($array[0] !== 0) {
  26. return $array[0];
  27. }
  28. $signature = $array[1];
  29. $sEncryptMsg = $this->generate($encrypt, $signature, $sTimeStamp, $sNonce);
  30. return 0;
  31. }
  32. public function prpcrypt_encrypt(string $data): array
  33. {
  34. try {
  35. // 拼接
  36. $data = Str::random().pack('N', strlen($data)).$data.sysConfig('wechat_cid');
  37. // 添加PKCS#7填充
  38. $data = $this->pkcs7_encode($data);
  39. // 加密
  40. $encrypted = openssl_encrypt($data, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
  41. return [0, $encrypted];
  42. } catch (Exception $e) {
  43. Log::critical(trans('notification.error', ['channel' => trans('admin.system.notification.channel.wechat'), 'reason' => var_export($e->getMessage(), true)]));
  44. return [-40006, null]; // EncryptAESError
  45. }
  46. }
  47. public function pkcs7_encode(string $data): string
  48. {// 对需要加密的明文进行填充补位
  49. // 计算需要填充的位数
  50. $padding = 32 - (strlen($data) % 32);
  51. $padding = ($padding === 0) ? 32 : $padding;
  52. $pattern = chr($padding);
  53. return $data.str_repeat($pattern, $padding); // 获得补位所用的字符
  54. }
  55. public function getSHA1(string $timestamp, string $nonce, string $encryptMsg): array
  56. {
  57. $data = [$encryptMsg, sysConfig('wechat_token'), $timestamp, $nonce];
  58. sort($data, SORT_STRING);
  59. $signature = sha1(implode($data));
  60. return [0, $signature];
  61. }
  62. /**
  63. * 生成xml消息.
  64. *
  65. * @param string $encrypt 加密后的消息密文
  66. * @param string $signature 安全签名
  67. * @param string $timestamp 时间戳
  68. * @param string $nonce 随机字符串
  69. */
  70. public function generate(string $encrypt, string $signature, string $timestamp, string $nonce): string
  71. {
  72. $format = <<<'XML'
  73. <xml>
  74. <Encrypt><![CDATA[%s]]></Encrypt>
  75. <MsgSignature><![CDATA[%s]]></MsgSignature>
  76. <TimeStamp>%s</TimeStamp>
  77. <Nonce><![CDATA[%s]]></Nonce>
  78. </xml>
  79. XML;
  80. return sprintf($format, $encrypt, $signature, $timestamp, $nonce);
  81. }
  82. public function decryptMsg(string $sMsgSignature, ?int $sTimeStamp, string $sNonce, string $sPostData, string &$sMsg)
  83. { // 检验消息的真实性,并且获取解密后的明文.
  84. // 提取密文
  85. [$code, $encrypt] = $this->extract($sPostData);
  86. if ($code !== 0) {
  87. return $code;
  88. }
  89. $sTimeStamp = $sTimeStamp ?? time();
  90. $this->verifySignature($sMsgSignature, $sTimeStamp, $sNonce, $encrypt, $sMsg); // 验证安全签名
  91. }
  92. /**
  93. * 提取出xml数据包中的加密消息.
  94. *
  95. * @param string $xmlText 待提取的xml字符串
  96. * @return array 提取出的加密消息字符串
  97. */
  98. public function extract(string $xmlText): array
  99. {
  100. try {
  101. $xml = new DOMDocument;
  102. $xml->loadXML($xmlText);
  103. $encrypt = $xml->getElementsByTagName('Encrypt')->item(0)->nodeValue;
  104. return [0, $encrypt];
  105. } catch (Exception $e) {
  106. Log::critical(trans('notification.error', ['channel' => trans('admin.system.notification.channel.wechat'), 'reason' => var_export($e->getMessage(), true)]));
  107. return [-40002, null]; // ParseXmlError
  108. }
  109. }
  110. public function verifySignature(string $sMsgSignature, string $sTimeStamp, string $sNonce, string $sEcho, string &$sMsg): int
  111. { // 验证URL
  112. // verify msg_signature
  113. [$code, $encrypt] = $this->extract($sEcho);
  114. if ($code !== 0) {
  115. return $code;
  116. }
  117. [$code, $signature] = $this->getSHA1($sTimeStamp, $sNonce, $encrypt);
  118. if ($code !== 0) {
  119. return $code;
  120. }
  121. if ($sMsgSignature !== $signature) {
  122. Log::critical(trans('notification.error', ['channel' => trans('admin.system.notification.channel.wechat'), 'reason' => trans('notification.sign_failed')]));
  123. return -40004; // ValidateSignatureError
  124. }
  125. $sMsg = $encrypt;
  126. return 0;
  127. }
  128. public function prpcrypt_decrypt(string $encrypted): array
  129. {
  130. try {
  131. // 解密
  132. $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
  133. } catch (Exception $e) {
  134. Log::critical(trans('notification.error', ['channel' => trans('admin.system.notification.channel.wechat'), 'reason' => var_export($e->getMessage(), true)]));
  135. return [-40007, null]; // DecryptAESError
  136. }
  137. try {
  138. // 删除PKCS#7填充
  139. $result = $this->pkcs7_decode($decrypted);
  140. if (strlen($result) < 16) {
  141. return [];
  142. }
  143. // 拆分
  144. $content = substr($result, 16, strlen($result));
  145. $len_list = unpack('N', substr($content, 0, 4));
  146. $xml_len = $len_list[1];
  147. $xml_content = substr($content, 4, $xml_len);
  148. $from_receiveId = substr($content, $xml_len + 4);
  149. } catch (Exception $e) {
  150. // 发送错误
  151. Log::critical(trans('notification.error', ['channel' => trans('admin.system.notification.channel.wechat'), 'reason' => var_export($e->getMessage(), true)]));
  152. return [-40008, null]; // IllegalBuffer
  153. }
  154. if ($from_receiveId !== sysConfig('wechat_cid')) {
  155. return [-40005, null]; // ValidateCorpidError
  156. }
  157. return [0, $xml_content];
  158. }
  159. public function pkcs7_decode(string $encrypted): string
  160. {// 对解密后的明文进行补位删除
  161. $length = strlen($encrypted);
  162. $padding = ord($encrypted[$length - 1]);
  163. if ($padding < 1 || $padding > 32) {
  164. return $encrypted;
  165. }
  166. return substr($encrypted, 0, $length - $padding);
  167. }
  168. }