开发主要分为以下几个步骤:
- 申请测试公众号
- 配置服务器地址
- 验证服务器地址的有效性
- 接收消息,回复消息
申请测试公众号
记录:不知道如何使用微信公众号进行调试,百度了很多资料,发现微信公众平台提供了相当好的测试工具,这是一个模拟的公众号,我们可以用自己的微信关注该公众号,然后进行接口调试。
第一步首先要申请一个测试的微信公众号,便于调试,地址: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参数内容,则接入生效,成为开发者成功,否则接入失败。
验证步骤如下:
- 将token、timestamp、nonce三个参数进行字典序排序。
- 将三个参数字符串拼接成一个字符串进行sha1加密。
- 获得加密后的字符串与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版]