Home 微信公众号自动应答的实现
Post
Cancel

微信公众号自动应答的实现

开发主要分为以下几个步骤:

  1. 申请测试公众号
  2. 配置服务器地址
  3. 验证服务器地址的有效性
  4. 接收消息,回复消息

申请测试公众号

记录:不知道如何使用微信公众号进行调试,百度了很多资料,发现微信公众平台提供了相当好的测试工具,这是一个模拟的公众号,我们可以用自己的微信关注该公众号,然后进行接口调试。

第一步首先要申请一个测试的微信公众号,便于调试,地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login

配置服务器地址

在测试号页面上有接口配置信息选项,在这个选项下面进行配置:
有两个配置项:服务器地址(URL),Token,在正式的公众号上还有一个选项EncodingAESKey。

  • URL:开发者用来接收微信消息和事件的接口URL。
  • Token:可以任意填写,用作生成签名(该Token和接口URL中包含的Token进行比对,从而验证安全性)。
  • EncodingAESKey:由开发者手动填写或随机生成,将用作消息体加解密密钥。

当输入URL和Token点击保存的时候,需要后台启动并且验证Token通过之后才能保存,不然会保存失败,所以先把后台代码启动起来。

记录:验证方式在这里可以找到。在验证的时候始终调不到后台,后来发现需要将本地的服务器映射到外网,这里使用了nat123,但是这个软件用了两天就不能用了,需要收费,于是换成了Sunny-Ngrok,这里推荐使用Sunny-Ngrok,用起来相当顺手。这些工作做完之后就可以保存配置了。

验证服务器地址的有效性

当填写URL,Token,点击保存时,微信会通过GET的方式把微信加密签名(signature),时间戳(timestamp),随机数(nonce)和随机字符串(echostr)传到后台,通过检验signature对请求进行校验。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。

验证步骤如下:

  1. 将token、timestamp、nonce三个参数进行字典序排序。
  2. 将三个参数字符串拼接成一个字符串进行sha1加密。
  3. 获得加密后的字符串与signature对比,如果相等,返回echostr,表示配置成功,否则返回null,配置失败。

签名校检工具类:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.weixin.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class WeixinCheckoutUtil {
    
    private static String token = "you_token";

    public static boolean checkSignature(String signature, String timestamp, 
    String nonce) {
        String[] arr = { 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");
            byte[] digest = md.digest(content.toString().getBytes());
            tmpStr = byteToStr(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        content = null;
        return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
    }

    private static String byteToStr(byte[] byteArray) {
        String strDigest = "";
        for (int i = 0; i < byteArray.length; i++) {
            strDigest = strDigest + byteToHexStr(byteArray[i]);
        }
        return strDigest;
    }

    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 & 0xF)];
        tempArr[1] = Digit[(mByte & 0xF)];
        String s = new String(tempArr);
        return s;
    }

    public 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;
                }
            }
        }
    }
    
}

签名校验请求控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.weixin.controller;

import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.weixin.util.WeixinCheckoutUtil;

@RestController
public class WeixinCheckController {
    
    @RequestMapping(value = "/wx", method = { RequestMethod.GET })
    public String doGet(String signature, String timestamp, 
    String nonce, String echostr) {
        // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功
        if (signature != null && WeixinCheckoutUtil.checkSignature(signature, 
         timestamp, nonce)) {
            return echostr;
        }
        return null;
    }

}

接收消息,回复消息

记录:微信服务器设置如果使用了安全模式,后台需要对报文进行解密,而且响应微信服务器的报文也需要加密。调试可以使用微信公众平台接口调试工具

jar包依赖

微信服务器发送过来的是XML格式的消息,所以我们可以采用开源框架dom4j去解析XML。而weixin-popular则是微信消息体加密及解密需要的。

1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<dependency>
    <groupId>com.github.liyiorg</groupId>
    <artifactId>weixin-popular</artifactId>
    <version>2.8.24</version>
</dependency>

Message实体

1
2
3
4
5
6
7
8
9
10
public class Message {

    private String signature;
    private String timestamp;
    private String nonce;
    private String openid;
    private String msg_signature;
    private String encrypt_type;

}

回调接口

消息、事件回调跟校验回调是同一个接口地址,但是请求方式为POST。消息、事件会以xml格式的传输到后台,后台解析xml进行处理。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package com.weixin.service.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import com.qq.weixin.mp.aes.WXBizMsgCrypt;

public class WechatCallbackServiceImpl implements WechatCallbackService {
    
    private static String TOKEN = "";
    private static String ENCODINGAES_KEY = "";
    private static String APPID = "";

    public void doPost(HttpServletRequest request, HttpServletResponse response) 
                            throws Exception {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        String content = ""; // 应答消息
        String msg = ""; // 用户发来的消息
        String wechatId = ""; // 发送方帐号
        String openid = ""; // 开发者微信号
        
        try {
            /** 获取回调入参 **/
            String encryptType = request.getParameter("encrypt_type"); // 加密类型
            String timestamp = request.getParameter("timestamp"); // 时间戳
            String nonce = request.getParameter("nonce"); // 随机数
            String msgSignature = request.getParameter("msg_signature"); // 加密签名

            /** 对不同模式下的报文进行处理 **/
            WXBizMsgCrypt pc = new WXBizMsgCrypt(TOKEN, ENCODINGAES_KEY, APPID);
            String requestXml = streamToString(request);
            String result = "";
            if ((encryptType != null) 
                    && (!"".equals(encryptType)) 
                    && ("aes".equals(encryptType))) {
                // 安全模式,解密
                result = pc.decryptMsg(msgSignature, timestamp, nonce, requestXml);
            } else {
                // 明文模式
                result = requestXml;
            }
            
            /** 解析XML报文 **/
            Document doc = DocumentHelper.parseText(result);
            Element root = doc.getRootElement();
            List<Element> elelist = root.elements();
            Map<String, String> map = new HashMap();
            for (Element e : elelist) {
                map.put(e.getName(), e.getText());
            }
            wechatId = (String) map.get("ToUserName");
            openid = (String) map.get("FromUserName");
            msg = (String) map.get("Content");

            /** 对消息进行响应 **/
            if (msg.contains("消息")) {
                content = "后台接收到了消息!";
            } else {
                content = "";
            }
            
            /** 拼装响应报文 **/
            String responseXml = "" 
                + "<xml>" 
                    + "<ToUserName><![CDATA[" + openid + "]]></ToUserName>" 
                    + "<FromUserName><![CDATA[" + wechatId + "]]></FromUserName>" 
                    + "<CreateTime>" + System.currentTimeMillis() + "</CreateTime>"
                    + "<MsgType><![CDATA[text]]></MsgType>" 
                    + "<Content><![CDATA[" + content + "]]></Content>"
                + " </xml>";
            
            /** 安全模式下对响应报文进行加密 **/
            if ((encryptType != null) 
                    && (!"".equals(encryptType)) 
                    && ("aes".equals(encryptType))) {
                responseXml = pc.encryptMsg(responseXml, timestamp, nonce);
            }
            
            /** 响应 **/
            if ("".equals(content)) {
                out.println("");
            } else {
                out.println(responseXml);
            }
        } catch (Exception e) {
            throw new Exception("回调接口发生错误,错误信息:" + e.toString());
        }
    }

    private String streamToString(HttpServletRequest request) throws IOException {
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(request.getInputStream()));
        StringBuilder sb = new StringBuilder();
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }

}

回调接口控制器

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
package com.weixin.controller;

import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.weixin.util.WeixinCheckoutUtil;
import com.weixin.service.WechatCallbackService;

@RestController
public class WeixinCheckController {

    @Autowired
    private WechatCallbackService wechatCallbackService;
    
    @RequestMapping(value = "/wx", method = { RequestMethod.GET })
    public String doGet(String signature, String timestamp, 
    String nonce, String echostr) {
        if (signature != null && WeixinCheckoutUtil.checkSignature(signature, 
         timestamp, nonce)) {
            return echostr;
        }
        return null;
    }

    @RequestMapping(value = "/wx", method = { RequestMethod.POST })
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        wechatCallbackService.doPost(request, response);
    }

}

不同消息体报文的封装

可以参考微信官方文档或者微信公众平台开发入门教程[2020版]

参考:
1、java实现微信公众号token验证
2、微信官方文档
3、微信公众平台开发入门教程[2020版]

This post is licensed under CC BY 4.0 by the author.