WeChat.php 6.3 KB

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