微信公众号开发者模式介绍及接入

举报
技术火炬手 发表于 2018/07/02 11:17:28 2018/07/02
【摘要】 Java公众号开发环境搭建需要准备的东西:一个微信公众号,参考:微信公众号申请及介绍内网穿透工具,参考:使用natapp开启内网穿透之旅数据交互编辑模式和开发模式的关系:编辑模式和开发模式是互斥的关系,也就是说,当我们使用开发模式时,编辑模式下的操作就会失效。反之,使用编辑模式时,开发模式下的操作就会失效,所以只能使用其中一个模式进行公众号的开发。开发模式下,公众号数据的交互流程:注:图中的...

Java公众号开发环境搭建

需要准备的东西:


数据交互

编辑模式和开发模式的关系:

image.png

编辑模式和开发模式是互斥的关系,也就是说,当我们使用开发模式时,编辑模式下的操作就会失效。反之,使用编辑模式时,开发模式下的操作就会失效,所以只能使用其中一个模式进行公众号的开发。

开发模式下,公众号数据的交互流程:

image.png

注:图中的微信公众号服务器,就是我们开发者所要开发的部分


开发者模式接入

微信公众平台相关技术文档地址如下:

我们根据 “接入指南” 中的说明来完成公众平台的接入,但是我们跳过文档中的第一步,先来完成第二步的操作,即验证消息的确来自微信服务器。因为提交服务器配置信息时微信会对配置的URL发起调用,验证该服务器是否正常可用,所以我们得先把第二步完成,才能去完成第一步。既然是开发就得建工程了,所以在IDEA中创建一个SpringBoot工程,工程结构如下:

先说明一点:当我们提交服务器配置信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数分别为signature、timestamp、nonce、echostr。开发者通过检验signature对请求进行校验,若确认此次GET请求来自微信服务器,则原样返回echostr参数内容,表示接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

1)将token、timestamp、nonce三个参数进行字典序排序 
2)将三个参数字符串拼接成一个字符串进行SHA1加密 
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

可以看到,第二步中,我们需要将三个参数字符串拼接成一个字符串进行SHA1加密,这就涉及到SHA1加密算法。那么就需要一个专门的工具类来完成SHA1加密,所以需要在util包中,新建一个 SHA1Util 类,用于进行SHA1加密,代码如下:

package org.zero01.weixin.mqdemo.util;import java.security.MessageDigest;/**
 * @program: mq-demo
 * @description: SHA1加密
 * @author: 01
 * @create: 2018-06-23 18:06
 **/public final class SHA1Util {    private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5',            '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};    /**
     * Takes the raw bytes from the digest and formats them correct.
     *
     * @param bytes the raw bytes from the digest.
     * @return the formatted bytes.
     */
    private static String getFormattedText(byte[] bytes) {        int len = bytes.length;
        StringBuilder buf = new StringBuilder(len * 2);        // 把密文转换成十六进制的字符串形式
        for (byte aByte : bytes) {
            buf.append(HEX_DIGITS[(aByte >> 4) & 0x0f]);
            buf.append(HEX_DIGITS[aByte & 0x0f]);
        }        return buf.toString();
    }    public static String encode(String str) {        if (str == null) {            return null;
        }        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
            messageDigest.update(str.getBytes());            return getFormattedText(messageDigest.digest());
        } catch (Exception e) {            throw new RuntimeException(e);
        }
    }
}

在util包中,再新建一个 WechatMqCheckedUtil 工具类,用于校验微信发起调用时所传递的参数,代码如下:

package org.zero01.weixin.mqdemo.util;import java.util.Arrays;/**
 * @program: mq-demo
 * @description: 校验微信发起调用时所传递的参数
 * @author: 01
 * @create: 2018-06-23 17:57
 **/public class WechatMqCheckedUtil {    // 在公众平台上配置的自定义token
    private static final String token = "zeroJun";    /**
     * 校验微信加密签名
     *
     * @param signature 微信加密签名
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @return
     */
    public static boolean checkedSignature(String signature, String timestamp, String nonce) {        // 1.加入token进行排序
        String[] paramArr = new String[]{token, timestamp, nonce};
        Arrays.sort(paramArr);        // 2.拼接成字符串,进行sha1加密
        StringBuilder content = new StringBuilder();        for (String aParamArr : paramArr) {
            content.append(aParamArr);
        }

        String temp = SHA1Util.encode(content.toString());        // 3.与signature参数进行对比,并返回对比结果
        return temp.equals(signature);
    }
}

在controller包中,新建一个 WeChatMqController 控制器类,提供给微信调用的接口,代码如下:

package org.zero01.weixin.mqdemo.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.zero01.weixin.mqdemo.util.WechatMqCheckedUtil;/**
 * @program: mq-demo
 * @description: 接入微信公众平台
 * @author: 01
 * @create: 2018-06-23 17:51
 **/@RestController@RequestMapping("/wechat/mq")public class WeChatMqController {    /**
     * 验证消息的确来自微信服务器
     *
     * @param signature 微信加密签名
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @param echostr   随机字符串
     * @return
     */
    @GetMapping("/common")    public String token(@RequestParam("signature") String signature,
                        @RequestParam("timestamp") String timestamp,
                        @RequestParam("nonce") String nonce,
                        @RequestParam("echostr") String echostr) {        // 验证成功则返回echostr
        if (WechatMqCheckedUtil.checkedSignature(signature, timestamp, nonce)) {
            System.out.println(echostr);            return echostr;
        }        return null;
    }
}

完成代码的编写,并运行了工程及natapp客户端后,就可以到公众平台上填写服务器的配置信息了。进入“基本配置” 的页面,点击 “修改配置” ,如下:
微信公众号开发者模式介绍及接入

填写好基本的配置:

image.png

提交配置:

image.png

提交成功后,启用服务器配置:

image.png

image.png

到此为止,我们的开发者模式就接入完成了。此时,在编辑模式的界面中,可以看到编辑模式下的操作都已失效:

image.png

消息的接收与响应

消息管理相关的文档:

我们先来完成文本消息的接收及回复,由于微信传递的数据是xml格式的,所以我们需要添加一些用于解析xml的包,在pom.xml中添加如下依赖:

image.png

在工程中,新建一个vo包,在该包下新建一个 AllMessage 类,用于封装所有普通消息的字段。关于不同类型的普通消息所包含的具体字段及描述信息,请参考:接收普通消息。代码如下:


package org.zero01.weixin.mqdemo.vo;import lombok.Data;/**
 * @program: mq-demo
 * @description: 所有类型的消息封装对象
 * @author: 01
 * @create: 2018-06-23 21:16
 **/@Data // lombok注解public class AllMessage {    /**
     * 属性名首字母大写的原因是因为返回的xml中标签的名称是需要大写的,否则微信解析不了
     */
    private String ToUserName;  // 接收方账号
    private String FromUserName;  // 发送方账号
    private long CreateTime;  // 消息创建时间 (整型)
    private String MsgType;  // 消息类型
    private String PicUrl;  // 消息内容
    private String Content;  // 消息内容
    private String MediaId;  // 消息媒体id,可以调用多媒体文件下载接口拉取数据。
    private String Format;  // 语音格式,如amr,speex等
    private String Recognition;  // 语音识别结果,UTF8编码
    private String MsgId;  // 消息id,64位整型
    private String ThumbMediaId;  // 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。
    private String Location_X;  // 地理位置维度
    private String Location_Y;  // 地理位置经度
    private String Scale;  // 地图缩放大小
    private String Label;  // 地理位置信息
    private String Title;  // 消息标题
    private String Description;  // 消息描述
    private String Url;  // 消息链接
    private String Event;  // 事件类型
    private String EventKey; // 事件KEY值
    private String Ticket;  // 二维码的ticket}

新建一个common包,并在该包中,新建一个 MessageTypeEnum 枚举类,用于存放普通消息的类型。代码如下:


package org.zero01.weixin.mqdemo.common;import lombok.AllArgsConstructor;import lombok.Getter;/**
 * @program: mq-demo
 * @description: 普通消息类型
 * @author: 01
 * @create: 2018-06-24 14:00
 **/@Getter@AllArgsConstructorpublic enum MessageTypeEnum {

    MSG_TEXT("text"),  // 文本消息类型
    MSG_IMAGE("image"), // 图片消息类型
    MSG_VOICE("voice"), // 语音消息类型
    MSG_VIDEO("video"), // 视频消息类型
    MSG_SHORTVIDEO("shortvideo"), // 小视频消息类型
    MSG_LOCATION("location"), // 地理位置消息类型
    MSG_LINK("link"), // 链接消息类型
    MSG_EVENT("event"), // 事件消息类型
    ;    private String msgType;
}

事件消息类型中包含订阅/取消订阅两种事件类型,所以我们也需要增加一个枚举来存放这两种事件类型。代码如下:

package org.zero01.weixin.mqdemo.common;import lombok.AllArgsConstructor;import lombok.Getter;/**
 * @program: mq-demo
 * @description: 事件推送类型
 * @author: 01
 * @create: 2018-06-24 14:09
 **/@Getter@AllArgsConstructorpublic enum EventType {

    EVENT_SUBSCRIBE("subscribe"),  // 订阅事件类型
    EVENT_UNSUBSCRIBE("unsubscribe"),  // 取消订阅事件类型
    ;    private String eventType;
}

我们希望有一个专门的地方,来配置我们的自动回复内容,所以再次新建一个枚举类,用于存放自动回复的内容。代码如下:

package org.zero01.weixin.mqdemo.common;import lombok.AllArgsConstructor;import lombok.Getter;/**
 * @program: mq-demo
 * @description: 回复的内容
 * @author: 01
 * @create: 2018-06-24 14:09
 **/@AllArgsConstructor@Getterpublic enum ContentEnum {
    CONTENT_SUBSCRIBE("你好,欢迎关注zero菌~"),
    CONTENT_NONSUPPORT("暂不支持文本以外的消息回复!"),
    CONTENT_PREFIX("你发送的消息是:"),
    ;    private String content;
}

在util包下,新建一个 MessageUtil 工具类,用于转换消息数据类型,代码如下:

package org.zero01.weixin.mqdemo.util;import com.thoughtworks.xstream.XStream;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.Element;import org.dom4j.io.SAXReader;import org.zero01.weixin.mqdemo.vo.AllMessage;import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.io.InputStream;import java.util.HashMap;import java.util.List;import java.util.Map;/**
 * @program: mq-demo
 * @description: 转换消息数据类型的工具类
 * @author: 01
 * @create: 2018-06-23 21:04
 **/public class MessageUtil {    private final static String XML = "xml";    /**
     * xml转换为map集合
     *
     * @param request
     * @return
     * @throws IOException
     * @throws DocumentException
     */
    public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
        Map<String, String> map = new HashMap<>();
        SAXReader reader = new SAXReader();

        InputStream inputStream = request.getInputStream();
        Document document = reader.read(inputStream);

        Element root = document.getRootElement();

        List<Element> elementList = root.elements();        for (Element element : elementList) {
            map.put(element.getName(), element.getText());
        }
        inputStream.close();        return map;
    }    /**
     * 将 AllMessage 消息对象,转换为xml
     *
     * @param allMessage
     * @return
     */
    public static String allMessageToXml(AllMessage allMessage) {
        XStream xStream = new XStream();
        xStream.alias(XML, allMessage.getClass());        return xStream.toXML(allMessage);
    }    /**
     * 将 AllMessage 消息对象,转换为xml,并指定content的内容
     *
     * @param allMessage
     * @return
     */
    public static String allMessageToXml(AllMessage allMessage, String content) {
        allMessage.setContent(content);        return allMessageToXml(allMessage);
    }    /**
     * 将xml转换为 AllMessage消息对象
     *
     * @param xmlStr
     * @return
     */
    public static AllMessage xmlToAllMessage(String xmlStr) {
        XStream xStream = new XStream();
        AllMessage allMessage = new AllMessage();
        xStream.aliasType(XML, allMessage.getClass());
        allMessage = (AllMessage) xStream.fromXML(xmlStr);        return allMessage;
    }    /**
     * 将xml转换为 AllMessage 消息对象,并指定content的内容
     *
     * @param xmlStr
     * @param content
     * @return
     */
    public static AllMessage xmlToAllMessage(String xmlStr, String content) {
        AllMessage allMessage = xmlToAllMessage(xmlStr);
        allMessage.setContent(content);        return allMessage;
    }    /**
     * 设置并获取文本消息类型的 AllMessage 对象
     * @param fromUserName
     * @param toUserName
     * @param content
     * @return
     */
    public static AllMessage setGetTextMsg(String fromUserName, String toUserName, String content) {
        AllMessage allMessage = new AllMessage();
        allMessage.setFromUserName(toUserName);
        allMessage.setToUserName(fromUserName);
        allMessage.setMsgType(MessageTypeEnum.MSG_TEXT.getMsgType());
        allMessage.setCreateTime(new Date().getTime());
        allMessage.setContent(content);        return allMessage;
    }    /**
     * 自动回复
     * @param allMessage
     * @param content
     * @return
     */
    public static String autoReply(AllMessage allMessage,String content) {
        allMessage = setGetTextMsg(allMessage.getFromUserName(), allMessage.getToUserName(), content);        return allMessageToXml(allMessage);
    }
}

最后在 WeChatMqController 控制器类中,新增接收微信公众号消息的接口。注意,接口映射的uri也是/wechat/mq/common,但请求方式是post。代码如下:

/**
 * 接收微信公众号消息的接口
 *
 * @param xmlStr
 * @return
 */@PostMapping("/common")public String text(@RequestBody String xmlStr) {    // 将xml格式的数据,转换为 AllMessage 对象
    AllMessage allMessage = MessageUtil.xmlToAllMessage(xmlStr);    // 是否是文本消息类型
    if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_TEXT.getMsgType())) {        // 自动回复用户所发送的文本消息
        return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_PREFIX.getContent() + allMessage.getContent());
    }    // 是否是事件推送类型
    else if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_EVENT.getMsgType())) {        // 是否为订阅事件,即公众号被关注时所触发的事件
        if (EventType.EVENT_SUBSCRIBE.getEventType().equals(allMessage.getEvent())) {            // 自动回复欢迎语
            return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_SUBSCRIBE.getContent());
        }
    } else {        // 暂不支持文本以外的消息回复
        return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
    }    return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
}

编写完以上代码后,运行SpringBoot工程以及natapp客户端,接着向公众号发送各种类型的普通消息,自动回复结果如下:

image.png

如上图,可以看到,当公众号被关注时,回复了欢迎语。并成功接收了所有类型的普通消息,进行了相应的自动回复。到此为止,我们就完成了公众号开发模式的接入。


本文转自ZeroOne01博客51CTO博客,如需转载,请自行联系原作者。
原文链接 

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。