微信公众号的消息自动回复是微信公众平台给公众账号提供的一种基础能力。在微信公众号的管理平台,微信开放了三种简单基础的消息自动回复规则,用Spring mvc实现消息服务器还是比较简单高效。
当我们需要强大的消息回复逻辑时,微信公众平台自带的回复规则设定就无法满足我们的需求,这时就需要我们自己实现回复规则。微信开放这种能力,接下来几步实现自定义消息回复。
在微信公众号的开发基本设置中有一个“服务器配置”的区域,很多做公众号的都不知道其配置有何作用,接下来一一解答。
微信基础的功能“自动回复”和“自定义菜单”,在未做任何设置的之前,我们都可以通过微信公众平台做出预期设置。但当我们设置了“服务器配置”后,至少这两大功能,微信公众平台就不提供了,需要我们自己调用微信的接口实现。会有以下提示:
这样, 你大概就明白配置的作用了。既然服务器配置是必经之路,那该配置到底该如果配置呢?先看一下我的配置吧
注意:如果服务器收不到微信的请求,可能是你的服务器配置虽然配置了,但并没有启用,请看右上角。
服务器地址URL必须是一个可以通过http正常访问的链接,当用户关注或回复公众号时,微信会将消息以HTTP GET转发到该服务器地址中。令牌(Token)是配置服务器地址验证的关键,微信为防止他人非法冒充该公众号服务器,引入Token该拂去必须正确响应微信发送的Token验证。Token自定义设置。
以下是服务器验证代码:
@RequestMapping(value="/wechat", method=RequestMethod.GET)
public void wechatGet(HttpServletRequest request, HttpServletResponse response) {
logger.info("wechat get request start...");
PrintWriter print;
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
logger.info("\n[signature=" + signature
+ "][timestamp=" + timestamp
+ "][nonce=" + nonce
+ "][echostr=" + echostr
+ "][token=" + token + "]");
// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if (signature != null && WeChatUtil.checkSignature(signature, token, timestamp, nonce)) {
try {
print = response.getWriter();
print.write(echostr);
print.flush();
logger.info("wechat auth success...");
} catch (IOException e) {
e.printStackTrace();
}
}else{
logger.info("wechat auth failure...");
}
}
把我的工具类也贴进来
/**
* WeChat token validate tools.
* @author
* @date 2018-09-01
*
* 1、获取get请求参数:echostr, timestamp, nonce, signature
* 2、对token, timestamp, nonce进行排序
* 3、对排序后的数据进行加密,得到hashCode
* 4、hashCode与signature比较进行验签
* 5、验签成功返回:echostr, 失败返回:""
*/
public class WeChatUtil {
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 (int j = 0; j < len; j++) {
buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
}
return buf.toString();
}
/**
*
* @param str 待加密数据
* @return
*/
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);
}
}
/**
* @param token 自定义验证token
* @param timestamp 时间戳
* @param nonce 随机数
* @return
*
*/
public static String sort(String token, String timestamp, String nonce){
String[] arrTemp = {token, timestamp, nonce};
Arrays.sort(arrTemp);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < arrTemp.length; i++) {
sb.append(arrTemp[i]);
}
return sb.toString();
}
/**
*
* @param signature 签名
* @param token 自定义验证token
* @param timestamp 时间戳
* @param nonce 随机字符串
* @return
*/
public static boolean checkSignature(String signature, String token, String timestamp, String nonce) {
String sortedStr = sort(token, timestamp, nonce);
String encodeStr = encode(sortedStr);
if(signature.equals(encodeStr)){
return true;
}
return false;
}
}
因为我的服务配置以.do结尾,所以服务器地址配置成https://xxx.xxx.xxx/projectname/wechat.do
用户给公众号发送的消息,微信都会以HTTP POST 数据类型为xml的形式发送到服务器地址。上面的方法处理GET请求,接下来要处理POST请求。由于xml不好操作,我们需要两个方法,分别将xml字符串转为Map和Map转为xml字符串。
/**
* XML转Map
* @param strXML
* @return
*/
@SuppressWarnings("unchecked")
public Map<String, String> xmlToMap(String strXML) {
SAXReader reader = new SAXReader();
Document doc;
Map<String, String> map = new HashMap<String, String>();
try {
InputStream in = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
doc = reader.read(in);
Element root = doc.getRootElement();
List<Element> list = (List<Element>) root.elements();
for (Element element : list) {
map.put(element.getName(), element.getText());
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
}
return map;
}
/**
* XML转Map
* @param strXML
* @return
*/
public String mapToXml2(Map<String, String> map) {
SAXReader reader = new SAXReader();
Document doc = null;
String xml = " ";
try {
InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8"));
doc = reader.read(in);
Element root = doc.getRootElement();
for (String key: map.keySet()) {
Element item = root.addElement(key);
item.addCDATA(map.get(key));
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return doc.asXML().toString();
}
处理POST请求:
@RequestMapping(value="/wechat", method=RequestMethod.POST, produces="application/xml;charset=UTF-8")
public void wechatPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
logger.info("wechat post request start...");
PrintWriter print = response.getWriter();
String reqStr = IOUtils.toString(request.getInputStream());
logger.info("解密请求消息:");
try {
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, "cX8goXy3hpewENeykcU3wOjxqbDpQcaw2gLbb7DWXsP", "wxe43240c5a6750ef8");
String timeStamp = String.valueOf(System.currentTimeMillis()); // 时间戳
String nonce = DateUtil.format(new Date(), "yyyyMMddHHmmss"); // 随机字符串
print = response.getWriter();
String msgSignature = request.getParameter("msg_signature");
String timestamp = request.getParameter("timestamp");
String urlnonce = request.getParameter("nonce");
String openid = request.getParameter("openid");
String encrypt_type = request.getParameter("encrypt_type");
String decryptStr= pc.decryptMsg(msgSignature, timestamp, urlnonce, reqStr);
logger.info("解密后的请求消息:" + decryptStr);
Map<String, String> req = xmlToMap(decryptStr);
Map<String, String> rsp = new HashMap<String, String>();
rsp.put("ToUserName", req.get("FromUserName"));
rsp.put("FromUserName", req.get("ToUserName"));
rsp.put("CreateTime", req.get("CreateTime"));
rsp.put("MsgType", "text");
Map replyMap = new HashMap();
replyMap.put("你好","你好啊");
replyMap.put("开玩笑","我认真的");
String replymsg = "";
if(replymsg != null && replyMap.containsKey(req.get("Content"))) {
replymsg = (String) replyMap.get(req.get("Content"));
}
rsp.put("Content", replymsg);
logger.info("加密前响应报文:\n" + mapToXml2(rsp));
String miwen = pc.encryptMsg(mapToXml2(rsp), timestamp, urlnonce);
print.write(miwen);
logger.info("加密后响应报文:\n" + miwen);
print.flush();
} catch (IOException e) {
e.printStackTrace();
} catch (AesException e) {
e.printStackTrace();
}
}
微信公众平台采用AES对称加密算法对推送给公众帐号的消息体对行加密,EncodingAESKey则是加密所用的秘钥。公众帐号用此秘钥对收到的密文消息体进行解密,回复消息体也用此秘钥加密。
消息报文的三种模式:
微信公众平台为开发者提供了5种语言的示例代码(包括C++、php、Java、Python和C#版本),请自行下载。
解密后的普通文本消息
<xml>
<ToUserName>ToUserName>
<FromUserName>FromUserName>
<CreateTime>1545013410CreateTime>
<MsgType>MsgType>
<Content>Content>
<MsgId>6635782068263705931MsgId>
xml>
当用户关注公众号时,微信会发送什么给服务器呢?嗯,这是一个事件消息。
{CreateTime=1545007942, EventKey=, Event=subscribe, ToUserName=gh_1fc3764cbbdb, FromUserName=oFcn2jkbrUcqgt18Xe4jOZNHA8c0, MsgType=event}