100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > PHP 微信公众号消息加解密

PHP 微信公众号消息加解密

时间:2023-12-03 09:23:43

相关推荐

PHP 微信公众号消息加解密

公众号配置

根据提示设置即可:【图中信息均为无意义数据,仅供参考。注意服务器地址需可接收 GET/POST 两种请求】

AESKey 直接点一下随机生成即可,Token 可以生成一个 UUID 再把 UUID 进行 MD5 一次即可。

接收关注事件消息示例

请求参数校验

这一步根据项目情况,可供参考:(Lumen 框架)

$validateData = Validator::validate($request->all(), ['signature' => 'required|string|size:40','timestamp' => 'required|string|size:10','nonce' => 'required|numeric','echostr' => 'filled|string','openid' => 'filled|string','encrypt_type' => 'filled|string|in:aes','msg_signature' => 'filled|string|size:40',]);

消息签名校验

/*** 消息签名验证** @param string$signature 签名* @param string$timestamp 10 位时间戳* @param string$nonce 随机数* @param string|null $encrypt_msg 加密消息** @return bool*/public function checkSignature(string $signature, string $timestamp, string $nonce, ?string $encrypt_msg = null): bool {$array = [$this->serverToken, $timestamp, $nonce];if ($encrypt_msg) {$array[] = $encrypt_msg;}sort($array, SORT_STRING);return sha1(implode($array)) === $signature;}

通过公众号配置

在公众号后台配置服务器地址时,需要进行一次 Token 响应的校验,所以我们应该在checkSignature之上再添加一层用以通过验证并保存配置。

/*** @param array $data 请求参数,传入通过校验的请求参数 $validateData** @return bool|int|string*/public function checkSign(array $data) {if ($this->checkSignature($data['signature'], $data['timestamp'], $data['nonce'])) {return (isset($data['echostr']) && !isset($data['msg_signature'])) ? $data['echostr'] : true;}return -40001;}

随后在控制器中,只要请求$request->method()是个GET就可以直接返回echostr字符串了。

错误码返回值参考下方附录。

消息解密

加密消息中,有 5 个参数通过query的形式请求,而密文则为XML格式通过POST请求。

5 个参数分别为:

signature 消息请求签名timestamp 时间戳nonce 随机数encrypt_type(加密类型固定aes)msg_signature 消息签名(不能和 signature 搞混)

解密消息需要 4 个参数,分别是:XML(密文)、msg_signaturetimestampnonce

解码函数

/*** @param string $text** @return string*/public function decode(string $text): string {$pad = ord(substr($text, -1));if ($pad < 1 || $pad > 32) {$pad = 0;}return substr($text, 0, (strlen($text) - $pad));}

解密

AESKey 处理

此处是重点,必须提前处理 AESKey,否则将影响解密结果。

$this->aesKey = base64_decode('U2FsdGVkX18lt9IhqeRHnImsi6D3Q+8Xo0YYZGmQZSa' . '=');

解密函数

/*** 密文解密* $this->aesKey 以及 $this->appId 自行调整配置** @param string $encrypted** @return int|string*/public function decrypt(string $encrypted) {$iv = substr($this->aesKey, 0, 16);// decrypt$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $this->aesKey, OPENSSL_ZERO_PADDING, $iv);if (!$decrypted) {return -40007;}$result = $this->decode($decrypted);if (strlen($result) < 16) {return '';}$content = substr($result, 16, strlen($result));$lenList = unpack('N', substr($content, 0, 4));$lenXML = $lenList[1];$fromAppId = substr($content, $lenXML + 4);if ($fromAppId !== $this->appId) {return -40001;}return substr($content, 4, $lenXML);}

消息解密处理

/*** 消息解密** @param string $message* @param string $msg_signature* @param string $timestamp* @param string $nonce** @return SimpleXMLElement|int*/public function decryptMessage(string $message, string $msg_signature, string $timestamp, string $nonce) {// get messagetry {$message = simplexml_load_string($message, 'SimpleXMLElement', LIBXML_COMPACT + LIBXML_NOCDATA);} catch (Exception $e) {return -40002;}// get encrypt text$encrypt = $message->Encrypt->__toString();if (!$encrypt) {return -40002;}// check signif (!$this->checkSignature($msg_signature, $timestamp, $nonce, $encrypt)) {return -40001;}$decrypted = $this->decrypt($encrypt);if (is_int($decrypted)) {return $decrypted;}try {return simplexml_load_string($decrypted, 'SimpleXMLElement', LIBXML_COMPACT + LIBXML_NOCDATA);} catch (Exception $e) {return -40002;}}

至此消息解密已经成功,例如微信用户 OpenID 可通过decryptMessage方法的返回值获取:

$openId = $decrypted_msg->FromUserName->__toString();

对于解密后消息内所含属性,参阅:基础消息能力 | 微信开放文档

消息加密

当微信用户首次关注公众号时,微信会发送“订阅事件”消息到服务端,我们可以使用和公众号后台内相同的“自动回复”功能响应回复内容给微信,从而实现自动回复,这时我们需要对响应的消息进行加密。

根据文档所述,除了消息加密后的响应,如果无需响应任何操作可返回字符串success或长度为0的空内容,但微信推荐的是success。所以我们应当保证服务端仅会响应两种结果:一是success字符串;二是XML格式内容。

以下内容使用了 Laravel/Lumen 的View功能,供参考。

构建响应内容

文件路径:Project/resources/views/wechat/subscribe/default.blade.php

嗨,终于等到你啦!🌹关注 XX 公众号~了解更多请点击下方分类菜单吧!

注意:底部如有空行,会在响应给微信用户时显示。文本消息内容是支持 Emoji、超链接的。

构建消息模板

明文消息模板

末尾不能存在空行。

<!-- 文件路径 Project/resources/xml/WeChatReplyMsg.xml --><xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime><MsgType><![CDATA[$s]]></MsgType><Content><![CDATA[%s]]></Content></xml>

加密消息模板

末尾不能存在空行。

<!-- 文件路径 Project/resources/xml/WeChatReplyMsgCrypt.xml --><xml><Encrypt><![CDATA[%s]]></Encrypt><MsgSignature><![CDATA[%s]]></MsgSignature><TimeStamp>%s</TimeStamp><Nonce><![CDATA[%s]]></Nonce></xml>

填充响应消息

$retMsg = view('wechat.subscribe.default')->render();// $openId 和 $toUser 可以通过已解密的消息获得,$timestamp 可以自己生成或直接取微信请求中的 timestamp// 此处的 text 根据需要进行影响的消息进行调整,内容同理$replyMessage = sprintf(file_get_contents(resource_path('xml/WeChatReplyMsg.xml')), $openId, $toUser, $timestamp, 'text', $retMsg);

加密

编码函数

// 固定值$this->blockSize = 32;/*** @param string $text** @return string*/public function encode(string $text): string {$text_length = strlen($text);$amount_to_pad = $this->blockSize - ($text_length % $this->blockSize);if ($amount_to_pad == 0) {$amount_to_pad = $this->blockSize;}$pad_chr = chr($amount_to_pad);$tmp = '';for ($index = 0; $index < $amount_to_pad; $index++) {$tmp .= $pad_chr;}return $text . $tmp;}

加密函数

/*** @param string $text** @return int|string*/public function encrypt(string $text) {// Laravel/Lumen 中可直接生成 16 位随机字符串// 如非该框架请参考附录$random = Illuminate\Support\Str::random();$text = $random . pack('N', strlen($text)) . $text . $this->appId;$text = $this->encode($text);$iv = substr($this->aesKey, 0, 16);// encrypt$encrypted = openssl_encrypt($text, 'AES-256-CBC', $this->aesKey, OPENSSL_ZERO_PADDING, $iv);return $encrypted ?: -40006;}

生成签名

/*** @param string $encrypt_msg* @param string $timestamp* @param string $nonce** @return string*/public function generateSignature(string $encrypt_msg, string $timestamp, string $nonce): string {$array = [$encrypt_msg, $this->serverToken, $timestamp, $nonce];sort($array, SORT_STRING);return sha1(implode($array));}

消息加密处理

/*** @param string $reply_message* @param string $timestamp* @param string $nonce** @return int|string*/public function encryptMessage(string $reply_message, string $timestamp, string $nonce) {// encrypt$encrypted = $this->encrypt($reply_message);if (is_int($encrypted)) {return $encrypted;}// $nonce 同 $timestamp 可以自己生成或直接取微信请求中的 nonce$signature = $this->generateSignature($encrypted, $timestamp, $nonce);if (!$signature) {return -40001;}return sprintf(file_get_contents(resource_path('xml/WeChatReplyMsgCrypt.xml')),$encrypted, $signature, $timestamp, $nonce);}

最后将加密结果响应即可,注意响应头需加上Content-Type: application/xml

多说两句

在接收到微信发来的请求后,根据场景进行业务逻辑处理,在无需响应任何消息(被动回复)时,应直接在方法里返回success或空字符串、null之类。上层根据返回情况判断是否加密消息并返回,尽可能满足5秒内响应微信。

附录

错误码

const RET_ERRCODE = [-40001 => '签名验证错误',-40002 => 'XML 解析失败',-40003 => '生成签名失败',-40004 => 'EncodingAESKey 错误',-40005 => 'AppID 校验错误',-40006 => 'AES 加密失败',-40007 => 'AES 解密失败',-40008 => 'Buffer 非法',-40009 => 'Base64 编码失败',-40010 => 'Base64 解码失败',-40011 => '生成 XML 失败'];// 可以通过 self::RET_ERRCODE[-40001] 的形式返回字符串

随机字符串

/*** 随机生成 16 位字符串** @return string 生成的字符串*/function getRandomStr(): string {$str = '';$str_pol = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';$max = strlen($str_pol) - 1;for ($i = 0; $i < 16; $i++) {$str .= $str_pol[mt_rand(0, $max)];}return $str;}

消息加解密测试

<?phpnamespace WeChatTest;use App\Services\WeChat\WeChatService;use SimpleXMLElement;use TestCase;class TestWeChatMessage extends TestCase {/** @var WeChatService */private $wechatService;public function setUp(): void {parent::setUp();$this->wechatService = app(WeChatService::class);}public function testEncryptMessage() {$plainMsg = '<xml><ToUserName><![CDATA[o_zzzZZZ111234567-0123456789]]></ToUserName><FromUserName><![CDATA[gh_zZ0123456789]]></FromUserName><CreateTime>1616760794</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[被动回复消息示例]]></Content></xml>';$timestamp = '1616755616';$nonce = '00000000';$encrypted = $this->wechatService->encryptMessage($plainMsg, $timestamp, $nonce);$encryptedMsg = simplexml_load_string($encrypted);$msgSign = $encryptedMsg->MsgSignature->__toString();$decrypted = $this->wechatService->decryptMessage($encrypted, $msgSign, $timestamp, $nonce);$this->assertTrue($decrypted instanceof SimpleXMLElement);}public function testDecryptMessage() {$encryptedMsg = '<xml><Encrypt><![CDATA[dkKTw8EWS+kVDfm5v7v01epbQoV5acC0FWzSXSUaA2D2MCcIFQHorCv5vDxr2CoV60rBIENCEmryK441W4tDD94oWcYYJJfrE2QXX55ecP8G3BHYMZLE2XoC5cfpLin/PfIHWVzTyGH3CO2iFR2Hp9kDtJLAK49frsokXn5c4umyrUtl5Y5gY9qMKT5OvzqPS2QpeOHUNJ4dOgnb6hnWcZ/F4Yum0/c/2XijSkJg3wyBvx8C/TyEztnJ5+a3COGwSHPUSUPfCZ6uEuAtfy6RVh7TjiS4kBbcN15SJeHzvenWZcsMAMgUHkTaYbgN7RxPVSZN7NAP9b5pfiIQJ1rSGU2qRm1Asan4dTuBsYt1b7p1PEUT0g9kwJjAbfwnuhFe1zIwRLXn6AhnePTRRicBIP2c5DU4rwbZyHVkkuYPZ+1fQZjEX7GJO/x0z5gX/Cf8Wrl924x4+xrcUCwkpSYYvQ==]]></Encrypt></xml>';$msgSign = '64fca2f2f18f663339b468c85be733d548cc57bb';$timestamp = '1616755616';$nonce = '00000000';$result = $this->wechatService->decryptMessage($encryptedMsg, $msgSign, $timestamp, $nonce);$this->assertTrue($result instanceof SimpleXMLElement);}}

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。