本编文章手把手教大家如何快速搭建微信个人订阅号被动回复功能

未被认证的微信个人订阅号可调用的接口很少,大多都没有权限,如果想使用微信更多的官方接口,需要进行认证。

先来看看最终实现的功能截图:

1. 关注后被动的回复:

2. 输入消息被动回复:

准备工作

  • 微信个人订阅号(未认证的即可)
  • 对公服务器Ecs(阿里云或腾讯云)
  • SpringBoot
  • docker

搭建步骤

1. 配置个人订阅号

第一需要我们先拥有一个微信订阅号,可以去微信公众号官网进行注册,并关联自己的微信号。这里注册的内容就不再详述了,自行注册即可。

配置好所有信息之后,需要开启开发者模式,然后点击基本配置。

在基本配置页面配置属于自己的服务器以及设定好的Token等内容

上述内容中我们只需要关注服务器配置

服务器地址URL: 这里需要填写上述准备好的公网ECS地址,注意必须是公网地址,否则微信服务器接受无法找到地址。
令牌(Token): 这是自己约定好的与服务器链接的密钥,自己可以随意设置,但是在服务器上的程序必须要通过设置好的Token验证。
消息加解密密钥(EncodingAESKey): 微信生成的加解密密钥。
消息加解密方式: 加解密方式,这里可以选择明文方式。

2. 编写SpringBoot程序

既然要配置服务器URL,那么要让微信服务器找到我们,就必须先配置一个WechatController。


@Resource private WeChatService weChatService; /** * 处理微信服务器发来的get请求,进行签名的验证 * * @param signature 微信端发来的签名 * @param timestamp 微信端发来的时间戳 * @param nonce 微信端发来的随机字符串 * @param echostr 微信端发来的验证字符串 */ @GetMapping("/wechat") public String validate(@RequestParam(value = "signature") String signature, @RequestParam(value = "timestamp") String timestamp, @RequestParam(value = "nonce") String nonce, @RequestParam(value = "echostr") String echostr) { log.info("wechat request,start check it !"); return WeChatHelper.checkSignature(signature, timestamp, nonce) ? echostr : null; } /** * 此处是处理微信服务器的消息转发的 */ @PostMapping("/wechat") public String processMsg(HttpServletRequest request) { log.info("message received, start process message!"); // 调用核心服务类接收处理请求 return weChatService.processRequest(request); }

第一步是微信服务器的校验,配置了一个接口/wechat,注意这里的接口名称要和上述微信订阅号中配置的<服务器地址URL>中的接口名称一致,我们测试配置的是/wechat,上面的参数大家可以对照我们订阅号的基本配置看一下,signature是设定好的Token

第二个方法就是接受微信服务器发来的请求,可以通过具体的请求在weChatService中实现自身的业务逻辑。

校验代码在WechatHelper类中,除了校验的工具方法外,还有一些常用的消息、图文处理方法。

如下:

/**
 * @Description 微信帮助类
 * @Author lmx.zh
 * @date 2020-04-30 17:26
 * @Copyright 2019 Alibaba.com All right reserved.
 */
public class WeChatHelper {

    /**
     * 验证签名
     *
     * @param signature
     * @param timestamp
     * @param nonce
     * @return
     */
    public static boolean checkSignature(String signature, String timestamp, String nonce) {
        String[] arr = new String[] {WeChatConstant.TOKEN, timestamp, nonce};
        // 将token、timestamp、nonce三个参数进行字典序排序
        sort(arr);
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            content.append(arr[i]);
        }
        MessageDigest md = null;
        String tmpStr = null;

        try {
            md = MessageDigest.getInstance("SHA-1");
            // 将三个参数字符串拼接成一个字符串进行sha1加密
            byte[] digest = md.digest(content.toString().getBytes());
            tmpStr = byteToStr(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        content = null;
        // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
        return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
    }

    /**
     * 将字节数组转换为十六进制字符串
     *
     * @param byteArray
     * @return
     */
    private static String byteToStr(byte[] byteArray) {
        String strDigest = "";
        for (int i = 0; i < byteArray.length; i++) {
            strDigest += byteToHexStr(byteArray[i]);
        }
        return strDigest;
    }

    /**
     * 将字节转换为十六进制字符串
     *
     * @param mByte
     * @return
     */
    private static String byteToHexStr(byte mByte) {
        char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        char[] tempArr = new char[2];
        tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
        tempArr[1] = Digit[mByte & 0X0F];

        String s = new String(tempArr);
        return s;
    }

    private static void sort(String a[]) {
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = i + 1; j < a.length; j++) {
                if (a[j].compareTo(a[i]) < 0) {
                    String temp = a[i];
                    a[i] = a[j];
                    a[j] = temp;
                }
            }
        }
    }

    /**
     * 解析微信发来的请求(xml)
     *
     * @param request
     * @return
     * @throws Exception
     */
    @SuppressWarnings({"unchecked"})
    public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
        // 将解析结果存储在HashMap中
        Map<String, String> map = new HashMap<>();
        // 从request中取得输入流
        InputStream inputStream = request.getInputStream();
        // 读取输入流
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        // 得到xml根元素
        Element root = document.getRootElement();
        // 得到根元素的所有子节点
        List<Element> elementList = root.elements();
        // 遍历所有子节点
        for (Element e : elementList) { map.put(e.getName(), e.getText()); }
        // 释放资源
        inputStream.close();
        inputStream = null;
        return map;
    }

    public static String mapToXML(Map map) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        mapToXML2(map, sb);
        sb.append("</xml>");
        try {
            return sb.toString();
        } catch (Exception e) {
        }
        return null;
    }

    private static void mapToXML2(Map map, StringBuffer sb) {
        Set set = map.keySet();
        for (Iterator it = set.iterator(); it.hasNext(); ) {
            String key = (String)it.next();
            Object value = map.get(key);
            if (null == value) { value = ""; }
            if (value.getClass().getName().equals("java.util.ArrayList")) {
                ArrayList list = (ArrayList)map.get(key);
                sb.append("<" + key + ">");
                for (int i = 0; i < list.size(); i++) {
                    HashMap hm = (HashMap)list.get(i);
                    mapToXML2(hm, sb);
                }
                sb.append("</" + key + ">");

            } else {
                if (value instanceof HashMap) {
                    sb.append("<" + key + ">");
                    mapToXML2((HashMap)value, sb);
                    sb.append("</" + key + ">");
                } else {
                    sb.append("<" + key + "><![CDATA[" + value + "]]></" + key + ">");
                }

            }

        }
    }

    /**
     * 回复文本消息
     *
     * @param requestMap
     * @param content
     * @return
     */
    public static String sendTextMsg(Map<String, String> requestMap, String content) {

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("ToUserName", requestMap.get(WeChatConstant.FromUserName));
        map.put("FromUserName", requestMap.get(WeChatConstant.ToUserName));
        map.put("MsgType", WeChatConstant.RESP_MESSAGE_TYPE_TEXT);
        map.put("CreateTime", DateHelper.formatDate(new Date()));
        map.put("Content", content);
        return mapToXML(map);
    }

    /**
     * 回复图文消息
     *
     * @param requestMap
     * @param items
     * @return
     */
    public static String sendArticleMsg(Map<String, String> requestMap, List<ArticleItem> items) {
        if (items == null || items.size() < 1) {
            return "";
        }
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("ToUserName", requestMap.get(WeChatConstant.FromUserName));
        map.put("FromUserName", requestMap.get(WeChatConstant.ToUserName));
        map.put("MsgType", "news");
        map.put("CreateTime", DateHelper.formatDate(new Date()));
        List<Map<String, Object>> Articles = new ArrayList<Map<String, Object>>();
        for (ArticleItem item : items) {
            Map<String, Object> itemMap = new HashMap<String, Object>();
            Map<String, Object> itemContent = new HashMap<String, Object>();
            itemContent.put("Title", item.getTitle());
            itemContent.put("Description", item.getDescription());
            itemContent.put("PicUrl", item.getPicUrl());
            itemContent.put("Url", item.getUrl());
            itemMap.put("item", itemContent);
            Articles.add(itemMap);
        }
        map.put("Articles", Articles);
        map.put("ArticleCount", Articles.size());
        return mapToXML(map);
    }

}

接受用户发来的请求,进行具体业务逻辑处理的类是WeChatServiceImpl,我们这里主要看一下processRequest实现方法。

由于接受到用户的消息微信服务器是通过xml的方式发送到服务器上的,所以我们收到消息后,需要先解析xml格式,具体方法已经在工具类给出了,大家可以参考下。

如下图是微信服务器进行加密后发送给服务器中的消息模板:

processRequest具体实现方法

 @Override
    public String processRequest(HttpServletRequest request) {
        long startProcessTime = System.currentTimeMillis();
        //xml格式的消息数据
        String respXml = null;
        //默认返回的文本消息内容
        String respContent;
        try {
            // 调用parseXml方法解析请求消息
            Map<String, String> requestMap = WeChatHelper.parseXml(request);
            //消息类型
            String msgType = requestMap.get(WeChatConstant.MsgType);
            String mes = null;
            //文本消息
            if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_TEXT)) {
                respContent = "您发送的是文本消息!";
                respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
            }
            //图片消息
            else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_IMAGE)) {
                respContent = "您发送的是图片消息!";
                respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
            }
            // 语音消息
            else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_VOICE)) {
                respContent = "您发送的是语音消息!";
                respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
            }
            // 视频消息
            else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_VIDEO)) {
                respContent = "您发送的是视频消息!";
                respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
            }
            // 地理位置消息
            else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_LOCATION)) {
                respContent = "您发送的是地理位置消息!";
                respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
            }
            // 链接消息
            else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_LINK)) {
                respContent = "您发送的是链接消息!";
                respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
            }
            // 事件推送
            else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_EVENT)) {
                // 事件类型
                String eventType = requestMap.get(WeChatConstant.Event);
                // 关注
                if (eventType.equals(WeChatConstant.EVENT_TYPE_SUBSCRIBE)) {
                    respXml = WeChatHelper.sendTextMsg(requestMap, WeChatConstant.SUBSCRIBE_MSG);
                }
                // 取消关注
                else if (eventType.equals(WeChatConstant.EVENT_TYPE_UNSUBSCRIBE)) {
                    // TODO 取消订阅后用户不会再收到公众账号发送的消息,因此不需要回复
                }
                // 扫描带参数二维码
                else if (eventType.equals(WeChatConstant.EVENT_TYPE_SCAN)) {
                    // TODO 处理扫描带参数二维码事件
                }
                // 上报地理位置
                else if (eventType.equals(WeChatConstant.EVENT_TYPE_LOCATION)) {
                    // TODO 处理上报地理位置事件
                }
                // 自定义菜单
                else if (eventType.equals(WeChatConstant.EVENT_TYPE_CLICK)) {
                    // TODO 处理菜单点击事件
                }
            }
            if (respXml == null) { respXml = WeChatHelper.sendTextMsg(requestMap, mes); }
            log.info("wechat reponse user message is " + respXml);
            return respXml;
        } catch (Exception e) {
            log.info("微信公众号消息处理时出现异常 " + e.getMessage());
        }
        long endProcessTime = System.currentTimeMillis();
        log.info("wehcat process request time consuming is " + (endProcessTime - startProcessTime) + "ms");
        return "";
    }

我们可以看到解析xml以后是通过msgType来判断消息类型,然后通过不同的消息类型进行不同的业务逻辑处理,这里大家可以根据自身业务进行处理。

对接微信端的常量WechatConstant类也罗列一下

    //APPID
    public static final String appID = "appid";
    //appsecret
    public static final String appsecret = "appsecret";
    // Token
    public static final String TOKEN = "MyToken";
    public static final String RESP_MESSAGE_TYPE_TEXT = "text";
    public static final Object REQ_MESSAGE_TYPE_TEXT = "text";
    public static final Object REQ_MESSAGE_TYPE_IMAGE = "image";
    public static final Object REQ_MESSAGE_TYPE_VOICE = "voice";
    public static final Object REQ_MESSAGE_TYPE_VIDEO = "video";
    public static final Object REQ_MESSAGE_TYPE_LOCATION = "location";
    public static final Object REQ_MESSAGE_TYPE_LINK = "link";
    public static final Object REQ_MESSAGE_TYPE_EVENT = "event";
    public static final Object EVENT_TYPE_SUBSCRIBE = "SUBSCRIBE";
    public static final Object EVENT_TYPE_UNSUBSCRIBE = "UNSUBSCRIBE";
    public static final Object EVENT_TYPE_SCAN = "SCAN";
    public static final Object EVENT_TYPE_LOCATION = "LOCATION";
    public static final Object EVENT_TYPE_CLICK = "CLICK";
    public static final String FromUserName = "FromUserName";
    public static final String ToUserName = "ToUserName";
    public static final String MsgType = "MsgType";
    public static final String Content = "Content";
    public static final String Event = "Event";

这样我们就配置完对接微信个人订阅号的SpringBoot程序了,是不是很简单。只需要一个Controller、Helper、Service即可。

3. docker部署

程序写完后,我们就需要部署了,将写好的SpringBoot程序打成Dokcer镜像,部署到对接的服务器上。如果有小伙伴不会使用docker打包镜像,可以参考文章如何使用DockerFile构建SpringBoot工程镜像?

镜像上传到服务器之后,进行镜像load以及run,注意这里建议使用如下命令进行run:

//--name 容器名称
//-p 80:8001 注意这里一定要将对外暴露80端口,因为微信服务器验证只认识80端口,8001是springboot工程端口,可以任意设置。
//images 镜像名
docker run --name myFirstWechat -idt -p 80:8001 images

4. 验证

启动容器后,进入基本配置页面,点击启用,进行接入。

启动成功后,我们就可以关注该微信公众号,进行验证了。

截取部分日志信息(这里的日志是根据自身的业务逻辑截取的,大家可各自实现对应的业务逻辑)

16:31:27.034 default [http-nio-8001-exec-8] INFO  c.m.m.a.controller.WeChatController - message received, start process message!
16:31:27.035 default [http-nio-8001-exec-8] INFO  c.m.m.a.s.impl.WeChatServiceImpl - wechat request message is 三体
16:31:27.035 default [http-nio-8001-exec-8] INFO  c.m.m.a.s.impl.WeChatServiceImpl - get into received wechat message processing…