微信公众号开发之接收与回复消息(安全模式与明文模式)

专栏收录该内容

Hi I'm Shendi




微信公众号接收消息接口

微信文档


在第一篇的时候配置了服务器,并实现了get接口,当有消息时,微信将通过POST发送到接口

配置参考 微信公众号开发入门,配置与获取AccessToken


当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。


因为数据是 XML 格式的,且数据在请求体,如果使用的 node.js 可以查阅下面这两篇文章

Node.js的Express参数获取及获取POST请求的请求体

Nodejs解析XML - xmlreader


通过查阅文档,在 xml 中 MsgType 代表消息类型,通过这个参数做不同操作

例如文本消息的xml结构如下

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[this is a test]]></Content>
  <MsgId>1234567890123456</MsgId>
  <MsgDataId>xxxx</MsgDataId>
  <Idx>xxxx</Idx>
</xml>
参数 描述
ToUserName 开发者微信号
FromUserName 发送方账号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,文本为text
Content 文本消息内容
MsgId 消息id,64位整型
MsgDataId 消息的数据ID(消息如果来自文章时才有)
Idx 多图文时第几篇文章,从1开始(消息如果来自文章时才有)

本地测试可以使用花生壳之类的做个穿透,当发送消息,用户关注/取消关注,扫带参数二维码等,微信会发送消息到接口,这样接收就做好了,剩下的就是处理了

明文模式

nodejs简单的示例代码如下

var xmlreader = require('xmlreader');

router.post('/服务器配置的地址', (req, res) => {
    let readData = req.read().toString();
    console.log(readData);
    
    // 解析xml字符串 这是同步的
    xmlreader.read(readData, function (err, xmlData) {
        if (err) {
            console.error(err);
            return;
        }
        
        // 是否响应数据,如果没有,在最后响应空字符串
        let isResp = false;
        
        // 根节点为 xml
        let xml = xmlData.xml;
        
        // 根据 MsgType 做不同操作
        switch (xml.MsgType.text()) {
            // 文本消息
            case "text":
               	console.log("发送了文本");
                break;
            // 事件,事件通过Event做不同操作
            case "event":
                switch (xml.Event.text()) {
                    // 关注
                    case "subscribe":
                        console.log("关注了");
                    break;
                    // 取消关注
                    case "unsubscribe":
                        console.log("取消关注了");
                    break;
                }
            break;
        }
    });
    
    // 没有响应则返回 success 或者空字符串,这样微信不会重复发送消息
    if (!isResp) res.send("");
});

安全模式

数据将是加密的,消息的加解密参考:微信公众号开发之消息加解密

上面的代码将稍作更改,在 let xml = xmlData.xml; 后增加以下代码

let encData = xml.Encrypt.text(),
    timestamp = req.query.timestamp,
    nonce = req.query.nonce,
    msgSignature = req.query.msg_signature;
res.statusCode = 404;
if (encData != null && timestamp != null && nonce != null && msgSignature != null
    && msgSignature == WxEncrypt.signature([encData, timestamp, nonce, TOKEN_VAL])) {
    // 校验成功,开始解密
    let dec = WxEncrypt.decode(encData);
    console.log(dec);
    xmlreader.read(dec, function (err, xmlData) {
        if (err) {
            console.error(err);
            return;
        }
        xml = xmlData.xml;
    });
    res.statusCode = 200;
}

if (res.statusCode == 404) {
    res.send();
    return;
}

其中 TOKEN_VAL 是服务器配置的 Token



被动回复消息

微信文档

这个地方有很多的坑...


当微信发消息给我们的接口,我们可以响应数据回去,例如用户发送消息,我们回复消息给用户,但需要确保在五秒内完成,如果不能保证,那就返回空字符串,后面使用客服接口去发送消息

微信服务器在将用户的消息发给公众号的开发者服务器地址后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次

假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示。

1、直接回复success(推荐方式)

2、直接回复空串(指字节长度为0的空字符串,而不是XML结构体中content字段的内容为空)


明文模式

这里以回复文本消息为例,需要返回的数据格式如下

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>12345678</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[你好]]></Content>
</xml>
参数 是否必须 描述
ToUserName 接收方账号(收到的OpenID)
FromUserName 开发者微信号
CreateTime 消息创建时间 (整型)
MsgType 消息类型,文本为text
Content 回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示)

首先建议先以明文方式尝试,可以使用测试号,测试号只支持明文

第一个坑,微信发送的ToUserName是开发者微信号,FromUserName是发送方账号

而返回的数据中ToUserName是接收方账号,FromUserName是开发者微信号,是相反的

回复

吃了没仔细看的亏,这个地方卡了我很久,以至于我发送消息给公众号,公众号只显示

发送消息

当时可纳闷了,仔细对比数据结构、内容,用postman尝试,包括响应头都仔细检查了...


代码如下

// 文本消息,回复有坑-ToUserName与FromUserName是相反的
case "text":
    let data = `<xml>
        <ToUserName><![CDATA[${xml.FromUserName.text()}]]></ToUserName>
        <FromUserName><![CDATA[${xml.ToUserName.text()}]]></FromUserName>
        <CreateTime>${new Date().getTime()}</CreateTime>
        <MsgType><![CDATA[text]]></MsgType>
        <Content><![CDATA[你好呀]]></Content></xml>`;
    res.send(data);
    isResp = true;
break;

需要注意的是,其中的xml数据内容不能包含空格,这是官方上说的,这个坑我没有踩

常见错误举例


安全模式

安全模式需要线上的公众号来进行测试,加解密参考上面那篇消息加解密的文章

加密消息的格式

<xml>
<Encrypt></Encrypt>
<MsgSignature></MsgSignature>
<TimeStamp></TimeStamp>
<Nonce></Nonce>
</xml>

其中,msg_encrypt=Base64_Encode(AES_Encrypt [random(16B)+ msg_len(4B) + msg + ])

random(16B)为16字节的随机字符串;msg_len为msg长度,占4个字节(网络字节序);

AESKey =Base64_Decode(EncodingAESKey + “=”),32个字节msg_signature=sha1(sort(Token、timestamp、nonce, msg_encrypt))timestamp、nonce回填请求中的值即可。

上面的引用是微信官方的原话,其中msg_encrypt代表 Encrypt 标签的内容,msg_signature代表 MsgSignature,剩下两个参数是从url拿到

我这里也踩了个坑,以为MsgSignature就是url上的msg_signature,仔细看才发现所用的 msg_encrypt 为自己加密后的内容,而不是微信发送的加密消息


示例代码如下

// 文本消息,回复有坑-ToUserName与FromUserName是相反的
case "text":
    let enc = `<xml>
        <ToUserName><![CDATA[${xml.FromUserName.text()}]]></ToUserName>
        <FromUserName><![CDATA[${xml.ToUserName.text()}]]></FromUserName>
        <CreateTime>${new Date().getTime()}</CreateTime>
        <MsgType><![CDATA[text]]></MsgType>
        <Content><![CDATA[你好呀]]></Content></xml>`;

    enc = WxEncrypt.encode(enc);
    let data = `<xml>
        <Encrypt>${enc}</Encrypt>
        <MsgSignature>${WxEncrypt.signature([enc, req.query.timestamp, req.query.nonce, TOKEN_VAL])}</MsgSignature>
        <TimeStamp>${req.query.timestamp}</TimeStamp>
        <Nonce>${req.query.nonce}</Nonce>
        </xml>`
    
    res.send(data);
    isResp = true;
break;

效果图

效果图


最后,别忘了将用户OpenID存起来,后面调用客服发送消息也需要使用到openid




END

本文链接:https://sdpro.top/blog/html/article/1101.html

♥ 赞助 ♥

尽管去做,或许最终的结果不尽人意,但你不付出,他不付出,那怎会进步呢?