消息接收与回复
当普通微信用户向公众账号发消息时,微信服务器将POST
消息的XML
数据包到开发者填写的URL
上。
请注意:
- 关于重试的消息排重,推荐使用
msgid
排重。 - 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。详情请见“发送消息-被动回复消息”。
- 如果开发者需要对用户消息在5秒内立即做出回应,即使用“发送消息-被动回复消息”接口向用户被动回复消息时,可以在
公众平台官网的开发者中心处设置消息加密。开启加密后,用户发来的消息和开发者回复的消息都会被加密(但开发者通过客服接口等API调用形式向用户发送消息,则不受影响)。关于消息加解密的详细说明,请见“发送消息-被动回复消息加解密说明”。 各消息类型的推送XML数据包结构如下:
由于微信服务器是将消息推送到了开发者填写URL
上,所以,消息接收和回复都将在接入文件simple.php
中处理。
接收普通消息
补充接收消息的代码,并对之前的代码做一点修改:
// simple.php
define('TOKEN', 'weixin');
new Weixin();
class Weixin
{
function __construct()
{
// 有echostr是接入校验操作
if (isset($_GET['echostr'])) {
$this->validate();
} else {
// 接收消息和回复消息
$this->response();
}
}
/**
* 接入校验
* @throws Exception
*/
private function validate()
{
$echoStr = $_GET["echostr"];
if ($this->checkSignature()) {
echo $echoStr;
exit;
}
}
// 接收消息和回复消息
private function response()
{
// 接收消息
$postData = file_get_contents('php://input');
libxml_disable_entity_loader(true);
file_put_contents('log', $postData);
}
/**
* 接入校验
* @return bool
* @throws Exception
*/
private function checkSignature()
{
if (!defined('TOKEN')) {
throw new Exception('TOKEN 没有定义');
}
$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$token = TOKEN;
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
if ($tmpStr === $signature) {
return true;
} else {
return false;
}
}
}
在公众号的聊天界面输入消息:
我们可以看到,在文件log
中,接收到的消息格式如下:
<xml>
<ToUserName><![CDATA[gh_a20954d36923]]></ToUserName>
<FromUserName><![CDATA[oTSbLwcg8E8e4sG-nesJosSN29_M]]></FromUserName>
<CreateTime>1597826407</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[hello]]></Content>
<MsgId>22875375674389500</MsgId>
</xml>
这是一条普通的文本消息,微信公众号将普通消息分成了以下几类:
- 文本消息
- 图片消息
- 语音消息
- 视频消息
- 小视频消息
- 地理位置消息
- 链接消息
每一种消息对应的xml
中的参数都有区别,详情可查阅微信开发文档-消息管理-接收普通消息。
接收到消息后,下一步就是针对不同的消息形式和内容,根据实际的业务需求做出不同的响应即可。
被动回复用户消息
该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次,如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime 排
重。
假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示。详见下面说明:
- 直接回复success(推荐方式)
- 直接回复空串(指字节长度为0的空字符串,而不是XML结构体中content字段的内容为空)
一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”:
- 开发者在5秒内未回复任何内容
- 开发者回复了异常数据,比如JSON数据等
另外,请注意,回复图片(不支持gif动图)等多媒体消息时需要预先通过素材管理接口上传临时素材到微信服务器,可以使用素材管理中的临时素材,也可以使用永久素材。
可回复的消息类型如下:
- 文本消息
- 图片消息
- 语音消息
- 视频消息
- 音乐消息
- 图文消息
我们以回复文本消息为例,根据用户发送的内容实现自动回复。
在simple.php
的response
方法中,添加代码:
// simple.php
// ... 省略代码
libxml_disable_entity_loader(true);
// 获取消息模板
$template = require_once 'template.php';
// 将接收的xml数据转化为对象的形式
$postData = simplexml_load_string($postData,'SimpleXMLElement',LIBXML_NOCDATA);
// 获取消息类型
$msgtype = $postData->MsgType;
// 获取消息发送方
$fomUserName = $postData->FromUserName;
// 获取消息的接收方
$toUserName = $postData->ToUserName;
if((string)$msgtype==='text'){
// 接收发送的消息
$msg = $postData->Content;
// 回复消息
$reMsg = '默认消息';
$createTime = time();
if((string)$msg==='你好'){
// 根据关键字设定回复消息
$reMsg = '你好,这是回复的消息';
}
$textTmp = $template['text'];
$textTmp = sprintf($textTmp,$fomUserName,$toUserName,$createTime,$reMsg);
file_put_contents('r', $textTmp);
echo $textTmp;
}else{
echo '';
}
// template.php
return [
'text'=>'<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%s</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[%s]]></Content>
</xml>'
];
回复消息的时候需要注意:
- 选择正确的
xml
模板; - 接收的消息中的
ToUserName
和FromUserName
,在回复消息的时候,需要放在回复xml
的FromUserName
和ToUserName
标签中。
更多被动回复消息,请查阅微信开发文档-消息管理-被动回复用户消息
接收事件推送
在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许,详细内容如下:
关注/取消关注事件
扫描带参数二维码事件
上报地理位置事件
自定义菜单事件
点击菜单拉取消息时的事件推送
点击菜单跳转链接时的事件推送
事件消息和普通消息唯一的不同是,在事件消息中多了:
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[具体事件类型]]></Event>
参数 | 描述 |
---|---|
MsgType | 消息类型,event |
Event | 事件类型 |
另外,不同的事件消息,在xml
结构上有些许的不同,具体请查阅微信开发文档-消息管理-接收事件推送
我们以用户关注公众号后,自动回复谢谢关注
为例,说明对事件消息的处理,仍然是在simple.php
的response
方法中:
if ((string)$msgtype === 'text') {
// 接收发送的消息
// ... 省略代码
} else if ((string)$msgtype === 'event') {
// 获取具体的事件类型
$event = $postData->Event;
if ((string)$event === 'subscribe') {
$reMsg = '谢谢关注^_^';
$textTmp = $template['text'];
$textTmp = sprintf($textTmp, $fomUserName, $toUserName, $createTime, $reMsg);
echo $textTmp;
}else {
echo '';
}
} else {
echo '';
}
在关注公众号后,看到如下结果:
了解更多事件推送内容,请查阅微信开发文档-消息管理-接收事件推送。
不管是接收的普通消息也好,事件推送也好,需要做的无非是根据具体的消息类型或者事件类型,做出不同的响应即可。
消息加解密说明
启用加解密功能(即选择兼容模式或安全模式)后,公众平台服务器在向公众账号服务器配置地址(可在“开发者中心”修改)推送消息时,URL将新增加两个参数(加密类型和消息体签名)。
公众平台提供了3种加解密的模式供开发者选择,即:
- 明文模式;
- 兼容模式;
- 安全模式;
(可在“开发者中心”选择相应模式),选择兼容模式和安全模式前,需在开发者中心填写消息加解密密钥EncodingAESKey
。
- 明文模式:维持现有模式,没有适配加解密新特性,消息体明文收发,默认设置为明文模式 ;
- 兼容模式:公众平台发送消息内容将同时包括明文和密文,消息包长度增加到原来的3倍左右;公众号回复明文或密文均可,不影响现有消息收发;开发者可在此模式下进行调试 ;
- 安全模式(推荐):公众平台发送消息体的内容只含有密文,公众账号回复的消息体也为密文,建议开发者在调试成功后使用此模式收发消息。
什么是EncodingAESKey
微信公众平台采用AES
对称加密算法对推送给公众帐号的消息体对行加密,EncodingAESKey
则是加密所用的秘钥。公众帐号用此秘钥对收到的密文消息体进行解密,回复消息体也用此秘钥加密。
具体实现
首先在微信官方文档-消息管理-消息加解密说明页面,下载微信官方提供的sdk
示例。
使用压缩包中的PHP示例:
该示例中的文件说明如下:
WXBizMsgCrypt.php
文件提供了WXBizMsgCrypt
类的实现,是用户接入企业微信的接口类。demo.php
提供了示例以供开发者参考。errorCode.php, pkcs7Encoder.php, sha1.php, xmlparse.php
文件是实现这个类的辅助类,开发者无须关心其具体实现。WXBizMsgCrypt
类封装了DecryptMsg
,EncryptMsg
两个接口,分别用于开发者解密以及开发者回复消息的加密。使用方法可以参考demo.php
文件。- 加解密协议请参考微信公众平台官方文档。
这里官方提供的SDK存在问题,由于官方提供的代码中,使用了mcrypt
扩展,但是,该扩展在7.1.x以后已经移除,所以不能使用,需要使用openssl
代替
需要将pkcs7Encoder.php
中使用的mcrypt
扩展替代为oepnssl
,代码如下:
// pkcs7Encoder.php
// ... 省略代码
/**
* 对明文进行加密
* @param string $text 需要加密的明文
* @return string 加密后的密文
*/
public function encrypt($text, $appid)
{
try {
//获得16位随机字符串,填充到明文之前
$random = $this->getRandomStr();
$text = $random . pack("N", strlen($text)) . $text . $appid;
// 网络字节序
// $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
// $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = substr($this->key, 0, 16);
//使用自定义的填充方式对明文进行补位填充
$pkc_encoder = new PKCS7Encoder;
$text = $pkc_encoder->encode($text);
// mcrypt_generic_init($module, $this->key, $iv);
//加密
// $encrypted = mcrypt_generic($module, $text);
// mcrypt_generic_deinit($module);
// mcrypt_module_close($module);
//print(base64_encode($encrypted));
$encrypted = openssl_encrypt($text,'AES-256-CBC',$this->key,OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING,$iv);
//使用BASE64对加密后的字符串进行编码
return array(ErrorCode::$OK, base64_encode($encrypted));
} catch (Exception $e) {
//print $e;
return array(ErrorCode::$EncryptAESError, null);
}
}
/**
* 对密文进行解密
* @param string $encrypted 需要解密的密文
* @return string 解密得到的明文
*/
public function decrypt($encrypted, $appid)
{
try {
//使用BASE64对需要解密的字符串进行解码
$ciphertext_dec = base64_decode($encrypted);
// $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = substr($this->key, 0, 16);
// mcrypt_generic_init($module, $this->key, $iv);
//解密
// $decrypted = mdecrypt_generic($module, $ciphertext_dec);
// mcrypt_generic_deinit($module);
// mcrypt_module_close($module);
$decrypted = openssl_decrypt($ciphertext_dec,'AES-256-CBC',$this->key,OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING,$iv);
} catch (Exception $e) {
return array(ErrorCode::$DecryptAESError, null);
}
// ... 省略代码
我们将核心的内文件放在我们的项目中:
在开发者中心
设置消息加解密方式:
我们发送消息给公众号后,可以看到接收的消息如下:
<xml>
<ToUserName><![CDATA[ToUserName]]></ToUserName>
<FromUserName><![CDATA[FromUserName]]></FromUserName>
<CreateTime>1597890997</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
<MsgId>22876298016865586</MsgId>
<Encrypt><![CDATA[ylnfrabqVpC9VD2M0Ky6fPgz6Sm3O9GMT8C/lSCAgLdbrb+mviO7CRwNM5JaXyYSiqIaQH5Alx4CzfJW3zZZeFKAtFcGLGtYVCVSV2GqwuG4+lqdGAcT9pNPwt4Kxew0hkUnn2kywNXU7JnyjweII+Y6l2Ht5406fuXBNF+Qz+1VuWNf+3Em5B9vzwDFfL72KDnHjOhTA6JlrjxYV/ac8rDICHSKnTPV3VwRt6k5OaaRzwuVVg/oGXT7QcTjPksB4F9BWlcNe6d+1olFY8hLqvbsxCvuy/yivdw5ERgr3RgL8JHjfNgFGhuRNVgpfIXrigdegPiPMCPVHc3Y/Ib2F9hBm2QZ+rLbFO2C70NABbuqB+FPGMifqnLarbsskleylIJGP+VPyzgM4VCDw/geKwW745jw2NRMrFyUcBwXRoo=]]></Encrypt>
</xml>
其中Encrypt
就是对消息加密后的密文;由于选择的是兼容模式,所以,还能看到未加密的明文消息你好
;如果选择【安全模式】,那么看到的仅仅是密文:
<xml>
<ToUserName><![CDATA[ToUserName]]></ToUserName>
<Encrypt><![CDATA[TBhpyaIxh1QtXHfXQ3cMEfAi8KKzzfT1W+kEK5csBL64LJkBvY9mYghg2tJ4qXsFgOATy90v1JUhuM8G8+t9zKaOfLmb6VQIaxvra9iFc6Xr/XPs1QEA5t1EY3RJ8KFE5M1ZTwZGPwhtQiyBOLw+gXxkT1OUQfnFXGArXr6Td5uTli39wE4GOfPEkJmd0XcARhDerqeai2tQIZK/eyBpNhN7TmR/G+3EBaAhw3p9XLRe4KnVeFw02OzqYGQkOD4z3nRN++l8bY7iBqxU+oz0MJSJwv0z2T26zcgc8XztDA3FeL2HeMo3mxpLbDs2CeXL9aTm/Snb61KI2n5eSsIS5U9h3MKYAbH1W+X33HeFsVAEQMvdoKkauJ8M2UWV7vWyDgJk96jZMV7EiNvH6FSBcO1Dsg/b9lpZ5vgP4WMOJvw=]]></Encrypt>
</xml>
接下来,我们对消息进行加解密操作。
仍然在之前的simple.php
文件中添加代码:
define('TOKEN', 'weixin');
// 导入sdk
require_once 'libs/wxBizMsgCrypt.php';
new Weixin();
class Weixin
{
private $encodingAesKey = '5Wn6TwLqOymq0jpRfO1U5sNqUkadWhCGKoUY22f3fTw';
private $appId = 'wxb81715728bdd3b35';
private $pc = null;
function __construct()
{
$this->pc = new WXBizMsgCrypt(TOKEN, $this->encodingAesKey, $this->appId);
// ... 省略代码
}
// ... 省略代码
// 接收消息和回复消息
private function response()
{
// 接收消息
$msg = $postData = file_get_contents('php://input');
libxml_disable_entity_loader(true);
// 判断是否使用了兼容模式或者安全模式
if(isset($_GET['encrypt_type']) && !empty($_GET['encrypt_type'])){
// 解密
$msg_sign = $_GET['msg_signature'];
$nonce = $_GET['nonce'];
$timeStamp = $_GET['timestamp'];
$errCode = $this->pc->decryptMsg($msg_sign, $timeStamp, $nonce, $postData, $msg);
if ($errCode == 0) {
file_put_contents('decryptMsg', $msg.PHP_EOL,FILE_APPEND);
} else {
file_put_contents('decryptMsg', $errCode.PHP_EOL,FILE_APPEND);
}
}
file_put_contents('log', $postData.PHP_EOL,FILE_APPEND);
file_put_contents('url', json_encode($_GET,JSON_UNESCAPED_UNICODE).PHP_EOL,FILE_APPEND);
// 获取消息模板
$template = require_once 'template.php';
// 将接收的xml数据转化为对象的形式
$postData = simplexml_load_string($msg, 'SimpleXMLElement', LIBXML_NOCDATA);
// 获取消息类型
$msgtype = $postData->MsgType;
// 获取消息发送方
$fomUserName = $postData->FromUserName;
// 获取消息的接收方
$toUserName = $postData->ToUserName;
$createTime = time();
// 回复消息模板
$textTmp = '';
if ((string)$msgtype === 'text') {
// 接收发送的消息
$msg = $postData->Content;
// 回复消息
$reMsg = '默认消息';
if ((string)$msg === '你好') {
// 根据关键字设定回复消息
$reMsg = '你好,这是回复的消息';
}
$textTmp = $template['text'];
$textTmp = sprintf($textTmp, $fomUserName, $toUserName, $createTime, $reMsg);
} else if ((string)$msgtype === 'event') {
// 获取具体的事件类型
$event = $postData->Event;
if ((string)$event === 'subscribe') {
$reMsg = '谢谢关注^_^';
$textTmp = $template['text'];
$textTmp = sprintf($textTmp, $fomUserName, $toUserName, $createTime, $reMsg);
}
}
// 判断是否使用了兼容模式或者安全模式
if(isset($_GET['encrypt_type']) && !empty($_GET['encrypt_type'])){
// 加密
$nonce = $_GET['nonce'];
$timeStamp = $_GET['timestamp'];
$encryptMsg = '';
$errCode = $this->pc->encryptMsg($textTmp, $timeStamp, $nonce, $encryptMsg);
if ($errCode == 0) {
file_put_contents('encryptMsg', $encryptMsg.PHP_EOL,FILE_APPEND);
$textTmp = $encryptMsg;
} else {
file_put_contents('encryptMsg', $errCode.PHP_EOL,FILE_APPEND);
}
}
file_put_contents('reply', $textTmp.PHP_EOL,FILE_APPEND);
echo $textTmp;
}
// ... 省略代码
}
注意:使用了安全模式后,回复的消息内容也必须经过加密处理。