WeChat.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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 function EncryptMsg(string $sReplyMsg, int|null $sTimeStamp, string $sNonce, string &$sEncryptMsg)
  10. { //将公众平台回复用户的消息加密打包.
  11. //加密
  12. $array = (new Prpcrypt())->encrypt($sReplyMsg);
  13. $ret = $array[0];
  14. if ($ret !== 0) {
  15. return $ret;
  16. }
  17. if ($sTimeStamp === null) {
  18. $sTimeStamp = time();
  19. }
  20. $encrypt = $array[1];
  21. //生成安全签名
  22. $array = $this->getSHA1($sTimeStamp, $sNonce, $encrypt);
  23. $ret = $array[0];
  24. if ($ret !== 0) {
  25. return $ret;
  26. }
  27. $signature = $array[1];
  28. //生成发送的xml
  29. $sEncryptMsg = $this->generate($encrypt, $signature, $sTimeStamp, $sNonce);
  30. return 0;
  31. }
  32. public function getSHA1(string $timestamp, string $nonce, string $encrypt_msg): array
  33. {
  34. //排序
  35. try {
  36. $array = [$encrypt_msg, sysConfig('wechat_token'), $timestamp, $nonce];
  37. sort($array, SORT_STRING);
  38. return [0, sha1(implode($array))];
  39. } catch (Exception $e) {
  40. Log::critical('企业微信消息推送异常:'.var_export($e->getMessage(), true));
  41. return [-40003, null]; // ComputeSignatureError
  42. }
  43. }
  44. /**
  45. * 生成xml消息.
  46. *
  47. * @param string $encrypt 加密后的消息密文
  48. * @param string $signature 安全签名
  49. * @param string $timestamp 时间戳
  50. * @param string $nonce 随机字符串
  51. */
  52. public function generate(string $encrypt, string $signature, string $timestamp, string $nonce): string
  53. {
  54. $format = '<xml>
  55. <Encrypt><![CDATA[%s]]></Encrypt>
  56. <MsgSignature><![CDATA[%s]]></MsgSignature>
  57. <TimeStamp>%s</TimeStamp>
  58. <Nonce><![CDATA[%s]]></Nonce>
  59. </xml>';
  60. return sprintf($format, $encrypt, $signature, $timestamp, $nonce);
  61. }
  62. public function DecryptMsg(string $sMsgSignature, int|null $sTimeStamp, string $sNonce, string $sPostData, string &$sMsg)
  63. { // 检验消息的真实性,并且获取解密后的明文.
  64. //提取密文
  65. $array = $this->extract($sPostData);
  66. $ret = $array[0];
  67. if ($ret !== 0) {
  68. return $ret;
  69. }
  70. if ($sTimeStamp === null) {
  71. $sTimeStamp = time();
  72. }
  73. $encrypt = $array[1];
  74. //验证安全签名
  75. $this->verifyURL($sMsgSignature, $sTimeStamp, $sNonce, $encrypt, $sMsg);
  76. }
  77. /**
  78. * 提取出xml数据包中的加密消息.
  79. *
  80. * @param string $xmlText 待提取的xml字符串
  81. * @return array 提取出的加密消息字符串
  82. */
  83. public function extract(string $xmlText): array
  84. {
  85. try {
  86. $xml = new DOMDocument();
  87. $xml->loadXML($xmlText);
  88. $array_e = $xml->getElementsByTagName('Encrypt');
  89. $encrypt = $array_e->item(0)->nodeValue;
  90. return [0, $encrypt];
  91. } catch (Exception $e) {
  92. Log::critical('企业微信消息推送异常:'.var_export($e->getMessage(), true));
  93. return [-40002, null]; // ParseXmlError
  94. }
  95. }
  96. public function verifyURL(string $sMsgSignature, string $sTimeStamp, string $sNonce, string $sEchoStr, string &$sReplyEchoStr)
  97. { // 验证URL
  98. //verify msg_signature
  99. $array = $this->getSHA1($sTimeStamp, $sNonce, $sEchoStr);
  100. $ret = $array[0];
  101. if ($ret !== 0) {
  102. return $ret;
  103. }
  104. $signature = $array[1];
  105. if ($signature !== $sMsgSignature) {
  106. return -40001; // ValidateSignatureError
  107. }
  108. $result = (new Prpcrypt())->decrypt($sEchoStr);
  109. if ($result[0] !== 0) {
  110. return $result[0];
  111. }
  112. $sReplyEchoStr = $result[1];
  113. return 0;
  114. }
  115. }
  116. /**
  117. * PKCS7Encoder class.
  118. *
  119. * 提供基于PKCS7算法的加解密接口.
  120. */
  121. class PKCS7Encoder
  122. {
  123. public static int $block_size = 32;
  124. public function encode(string $text): string
  125. { // 对需要加密的明文进行填充补位
  126. //计算需要填充的位数
  127. $amount_to_pad = self::$block_size - (strlen($text) % self::$block_size);
  128. if ($amount_to_pad === 0) {
  129. $amount_to_pad = self::$block_size;
  130. }
  131. return $text.str_repeat(chr($amount_to_pad), $amount_to_pad); // 获得补位所用的字符
  132. }
  133. public function decode(string $text): string
  134. { // 对解密后的明文进行补位删除
  135. $pad = ord(substr($text, -1));
  136. if ($pad < 1 || $pad > self::$block_size) {
  137. $pad = 0;
  138. }
  139. return substr($text, 0, strlen($text) - $pad);
  140. }
  141. }
  142. /**
  143. * Prpcrypt class.
  144. *
  145. * 提供接收和推送给公众平台消息的加解密接口.
  146. */
  147. class Prpcrypt
  148. {
  149. public string $key;
  150. public string $iv;
  151. public function __construct()
  152. {
  153. $this->key = base64_decode(sysConfig('wechat_encodingAESKey').'=');
  154. $this->iv = substr($this->key, 0, 16);
  155. }
  156. /**
  157. * 加密.
  158. */
  159. public function encrypt(string $text): array
  160. {
  161. try {
  162. //拼接
  163. $text = Str::random().pack('N', strlen($text)).$text.sysConfig('wechat_cid');
  164. //添加PKCS#7填充
  165. $text = (new PKCS7Encoder)->encode($text);
  166. //加密
  167. $encrypted = openssl_encrypt($text, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
  168. return [0, $encrypted];
  169. } catch (Exception $e) {
  170. Log::critical('企业微信消息推送异常:'.var_export($e->getMessage(), true));
  171. return [-40006, null]; // EncryptAESError
  172. }
  173. }
  174. public function decrypt(string $encrypted): array
  175. { // 解密
  176. try {
  177. //解密
  178. $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $this->key, OPENSSL_ZERO_PADDING, $this->iv);
  179. } catch (Exception $e) {
  180. Log::critical('企业微信消息推送异常:'.var_export($e->getMessage(), true));
  181. return [-40007, null]; // DecryptAESError
  182. }
  183. try {
  184. //删除PKCS#7填充
  185. $result = (new PKCS7Encoder)->decode($decrypted);
  186. if (strlen($result) < 16) {
  187. return [];
  188. }
  189. //拆分
  190. $content = substr($result, 16, strlen($result));
  191. $len_list = unpack('N', substr($content, 0, 4));
  192. $xml_len = $len_list[1];
  193. $xml_content = substr($content, 4, $xml_len);
  194. $from_receiveId = substr($content, $xml_len + 4);
  195. } catch (Exception $e) {
  196. // 发送错误
  197. Log::critical('企业微信消息推送异常:'.var_export($e->getMessage(), true));
  198. return [-40008, null]; // IllegalBuffer
  199. }
  200. if ($from_receiveId !== sysConfig('wechat_cid')) {
  201. return [-40005, null]; // ValidateCorpidError
  202. }
  203. return [0, $xml_content];
  204. }
  205. }