微信公众账号开发part2——用户消息接收

上一篇写了如何通过微信开发者认证,今天来讲下如何接收用户的消息,我们以接收用户的订阅消息为例。

微信用户消息格式

在开发者文档的接收事件推送文档中,说明了用户订阅消息的请求实体,内容如下:

1
2
3
4
5
6
7
<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[FromUser]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[subscribe]]></Event>
</xml>
  • ToUserName: 开发者微信号
  • FromUserName: 用户微信账号的OpenID
  • CreateTime: 消息发送时间,秒数
  • MsgType: 消息类型,事件消息为event
  • Event: 事件类型,订阅消息为subscribe

消息真实性验证

每次开发者接收用户消息的时候,微信也都会带上前面三个参数(signature、timestamp、nonce)访问开发者设置的URL,开发者依然通过对签名的效验判断此条消息的真实性。效验方式与首次提交验证申请一致。

所以每个订阅消息的http请求都会带有(signature、timestamp、nonce)这3个参数和上面的xml请求实体,服务端可以选择是否校验消息的真实性,建议校验,这样会比较安全。

接收消息后的响应内容

了解了消息请求的入参后,还需要知道我们处理请求后,需要返回什么样的内容给用户,这个在开发者文档里面好像没有提及,参考各方资料后知道需要返回一段xml内容,格式如下:

1
2
3
4
5
6
7
8
<xml>
    <Content>感谢您关注我的公众账号[愉快]</Content>
    <CreateTime>1423022113</CreateTime>
    <FromUserName>zzm</FromUserName>
    <FuncFlag>0</FuncFlag>
    <MsgType>text</MsgType>
    <ToUserName>zzm</ToUserName>
</xml>
  • ToUserName: 用户微信账号的OpenID
  • FromUserName: 开发者微信号
  • CreateTime: 消息发送时间,秒数
  • FuncFlag: 这个暂时不知道是什么,默认值为0
  • MsgType: 消息类型,文档消息可以为text和其他,这里我们以最简单的text文本消息为例
  • Content: 返回给订阅用户的消息内容,可以加表情

PS: ToUserName和FromUserName这2个参数和请求的xml实体要相反,这个也比较好理解,用户发了条消息过来,你要发个消息回去,ToUserName就变成了用户,FromUserName变成了你自己的公众账号了。

服务端开发

  • 了解了http请求的入参和出参,我们可以来开发我们的API了,talk is cheap, show me code
MainController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 //这里我们定义跟之前认证api相同的url,但方法是POST
  @RequestMapping(value = "/index", method = RequestMethod.POST)
    public
    @ResponseBody
    //3个校验消息真实性的参数,还有一个request实体body,里面是xml文本
    ResponseEntity<String> receive(@RequestParam("signature") String signature,
                                   @RequestParam("timestamp") String timestamp,
                                   @RequestParam("nonce") String nonce,
                                   @RequestBody String body) throws Exception {
        log.info("receive message start");
        log.info(String.format("signature:%s, timestamp:%s, nonce:%s", signature, timestamp, nonce));

        //先校验消息的真实性,如果校验失败,则返回400
        if (!wechatAuth(signature, timestamp, nonce)) {
            log.info("wechat auth failed");
            return new ResponseEntity<String>("wechat auth failed.", HttpStatus.BAD_REQUEST);
        }

        log.info(String.format("body:%s", body));
        //我们定义了一个util来解析xml,将其转换为一个object
        TextMessage requestMessage = XmlUtil.toTextMessage(body);
        log.info(String.format("requestMessage:%s", requestMessage));

        TextMessage textMessage = null;
        String msgType = requestMessage.getMsgType();
        String toUserName = requestMessage.getToUserName();
        String fromUserName = requestMessage.getFromUserName();
        //判断消息类型,如果是event,且事件类型为subscribe,则新建一个文本消息
        if (MessageType.event.name().equals(msgType)) {
            if (EventType.subscribe.name().equals(requestMessage.getEvent())) {
                String message = "感谢您关注我的公众账号[愉快]";
                textMessage = new TextMessage(toUserName, fromUserName,
                        MessageType.text.name(), message, TimeUtil.currentSeconds());
            }
        }

        //将文本消息转换为xml文本
        String responseMessage = XmlUtil.toXml(textMessage);
        HttpHeaders responseHeaders = new HttpHeaders();
        //设置返回实体的编码,不设置的话可能会变成乱码
        responseHeaders.add("Content-Type", "text/html; charset=utf-8");
        log.info(String.format("response message: %s", responseMessage));
        log.info("receive message finish");
        return new ResponseEntity<String>(responseMessage, responseHeaders, HttpStatus.OK);
    }
  • 这里使用java原生的JAXB来解析xml。
XmlUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import com.zzm.wechat.model.TextMessage;
import org.apache.commons.io.IOUtils;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;
import java.io.StringWriter;

public class XmlUtil {

    public static String toXml(TextMessage textMessage) throws Exception {
        if (textMessage == null) return "";

        JAXBContext context = JAXBContext.newInstance(TextMessage.class);
        Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        m.setProperty(Marshaller.JAXB_FRAGMENT, true);

        StringWriter sw = new StringWriter();
        m.marshal(textMessage, sw);
        return sw.toString();
    }

    public static TextMessage toTextMessage(String xml) throws Exception {
        JAXBContext jaxbContext = JAXBContext.newInstance(TextMessage.class);
        Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
        StringReader reader = new StringReader(xml);
        TextMessage textMessage = (TextMessage) jaxbUnmarshaller.unmarshal(reader);
        IOUtils.closeQuietly(reader);
        return textMessage;
    }
}
  • 定义消息的model类,这里需要用到xml的一些annotation。
XmlUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

//定义命名空间,如果不写的话,xml会以类名开头: <TextMessage>...</TextMessage>,写了就会以xml开头: <xml>...</xml>
@XmlRootElement(name = "xml")
public class TextMessage {
    private String fromUserName;
    private String toUserName;
    private String msgType;
    private int funcFlag = 0;
    private String content;
    private String event;
    private long createTime;

    public TextMessage() {
    }

    public TextMessage(String fromUserName, String toUserName, String msgType, String content, long createTime) {
        this.fromUserName = fromUserName;
        this.toUserName = toUserName;
        this.msgType = msgType;
        this.content = content;
        this.createTime = createTime;
    }

    public String getToUserName() {
        return toUserName;
    }

    //定义xml子项的名称,不写这个annotation的话,转换后的xml是: <toUserName>xxx</toUserName>,首字母变小写了,会导致消息传输错误
    @XmlElement(name = "ToUserName")
    public void setToUserName(String toUserName) {
        this.toUserName = toUserName;
    }

    //other setter and getter

    @Override
    public String toString() {
        //...
    }
}
  • 方法写完以后,同样的打包,部署SAE。

  • 打开手机,关注你的公众账号后,就可以看到服务端传过来的消息内容了。

我的公众账号是赵芝明的公账号,有兴趣的也可以加一下,以后这个公共账号的功能肯定会慢慢丰富的。

Comments