微信公众号开发文档介绍了消息的多种类型,微信开发文档–>公众号–>基础消息能力–>「被动回复用户消息」 如下
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次,如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime 排重。
如果开发者希望增强安全性,可以在开发者中心处开启消息加密,这样,用户发给公众号的消息以及公众号被动回复用户消息都会继续加密,详见被动回复消息加解密说明。
假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示。详见下面说明:
1、直接回复success(推荐方式) 2、直接回复空串(指字节长度为0的空字符串,而不是XML结构体中content字段的内容为空)
一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”:
1、开发者在5秒内未回复任何内容 2、开发者回复了异常数据,比如JSON数据等
另外,请注意,回复图片(不支持gif动图)等多媒体消息时需要预先通过素材管理接口上传临时素材到微信服务器,可以使用素材管理中的临时素材,也可以使用永久素材。
各消息类型需要的XML数据包结构如下:
1 回复文本消息
2 回复图片消息
3 回复语音消息
4 回复视频消息
5 回复音乐消息
6 回复图文消息
点击关注或者取消关注,微信那边会自动触发关注/取消关注事件,并请求事先配置好的地址
/**
* 接收微信推送事件
*
* @param request
* @param response
*/
@RequestMapping(value = "/check/signature", method = RequestMethod.POST, produces = {"application/xml; charset=UTF-8"})
@ResponseBody
public void wechatEvent(HttpServletRequest request, HttpServletResponse response) {
log.error("--------------------接收微信推送事件-----------------------");
try {
// 为了防止消息乱码
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
wxService.handleEvent(request, response);
}
下面是处理被动回复消息逻辑代码,需要根据具体的需求选择具体的消息类型,我这里以文本消息为例。
触发事件的时候会返回微信openId等其他信息。
public void handleEvent(HttpServletRequest request, HttpServletResponse response) {
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
Map<String, Object> map = XmlUtil.parseXML(inputStream);
// openId
String userOpenId = (String) map.get("FromUserName");
// 微信账号
String userName = (String) map.get("ToUserName");
// 事件
String event = (String) map.get("Event");
// 区分消息类型
String msgType = (String) map.get("MsgType");
// 普通消息
if ("text".equals(msgType)) {
// todo 处理文本消息
}
// else if ("image".equals(msgType)) {
// } else if ("voice".equals(msgType)) {
// } else if ("video".equals(msgType)) {
// }
// 事件推送消息
else if ("event".equals(msgType)) {
if ("subscribe".equals(event)) {
logger.info("用户扫码|关注|openId:{},userName:{}", userOpenId, userName);
String ticket = (String) map.get("Ticket");
if (StringUtils.isNotBlank(ticket)) {
redisCacheManager.set(ConstantsRedisKey.ADV_WX_LOGIN_TICKET.replace("ticketId", ticket), userOpenId, 10 * 60);
}
String mapToXml = handleEventSubscribe(map, userOpenId);
response.getWriter().print(mapToXml);
return;
} else if ("SCAN".equals(event)) {
logger.info("用户扫码|登录|openId:{},userName:{}", userOpenId, userName);
String ticket = (String) map.get("Ticket");
if (StringUtils.isNotBlank(ticket)) {
redisCacheManager.set(ConstantsRedisKey.ADV_WX_LOGIN_TICKET.replace("ticketId", ticket), userOpenId, 10 * 60);
}
// todo 业务处理
} else if ("unsubscribe".equals(event)) {
logger.info("用户取消关注,拜拜~,openId:{}", userOpenId);
// todo 取消关注 业务处理
}
}
logger.info("接收参数:{}", map);
} catch (IOException e) {
logger.error("处理微信公众号请求异常:", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ioe) {
logger.error("关闭inputStream异常:", ioe);
}
}
}
}
/**
* 处理 subscribe 类型的event
*
* @param map
* @param userOpenId
* @return
*/
private String handleEventSubscribe(Map<String, Object> map, String userOpenId) {
String resXmlStr = getReturnMsgSubscribe(map);
logger.info("用户扫码关注返回的xml:{}", resXmlStr);
return resXmlStr;
}
public String getReturnMsgSubscribe(Map<String, Object> decryptMap) {
logger.info("---开始封装xml---decryptMap:" + decryptMap.toString());
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(decryptMap.get("FromUserName").toString());
textMessage.setFromUserName(decryptMap.get("ToUserName").toString());
textMessage.setCreateTime(System.currentTimeMillis());
textMessage.setMsgType("text");
textMessage.setContent("你好,欢迎关注XXX!\n" +
"\n" +
"关注XXX。立即登录PC端网址 \n" + domainname +
" 即可完成注册!\n" +
"\n" +
"或," +
"点击这里立即完成注册");
return getXmlString(textMessage);
}
public String getXmlString(TextMessage textMessage) {
String xml = "";
if (textMessage != null) {
xml = "" ;
xml += ";
xml += textMessage.getToUserName();
xml += "]]> ";
xml += ";
xml += textMessage.getFromUserName();
xml += "]]> ";
xml += "" ;
xml += textMessage.getCreateTime();
xml += "";
xml += ";
xml += textMessage.getMsgType();
xml += "]]> ";
xml += ";
xml += textMessage.getContent();
xml += "]]> ";
xml += "";
}
return xml;
}
登录微信公众号平台–>设置开发–>「基础配置」–>服务器配置
注意⚠️:
服务器地址 必须是外网可以访问的,否则微信调不通。
令牌 需要和代码里的token一致,因为提交服务器配置的时候,会触发配置好的那个外网地址,发送一个Get请求,进行验证签名。
消息加密密钥 自动生成即可。
消息加密方式 为了方便开发调试,可以选择兼容模式。
填好信息别忘记 提交。
服务器配置以后还可以修改的,每次修改也都会发Get请求验证签名。
/***
* 填写服务器URL点击提交时,微信服务器触发get请求用于检测签名
* @return echoStr
*/
@GetMapping("/check/signature")
@ResponseBody
public String wechatCheckSignature(HttpServletRequest request) {
log.error("-------------------服务器配置|进行签名检测------------------------");
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echoStr = request.getParameter("echostr");
boolean checkSignature = WechatPublicUtils.checkSignature(signature, timestamp, nonce, Constants.WX_SERVER_CONFIG_TOKEN);
if (checkSignature) {
return echoStr;
}
return null;
}
校验签名工具类
public class WechatPublicUtils {
/**
* 校验签名
*
* @param signature 微信签名
* @param timestamp 时间戳
* @param nonce 随机字符串
* @param token 我们在公众号平台「基本配置」里定义的token
* @return 验证结果
*/
public static boolean checkSignature(String signature, String timestamp, String nonce, String token) {
// 将token、timestamp、nonce 进行字典序排序
String[] arr = new String[]{token, timestamp, nonce};
Arrays.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;
}
private static String byteToStr(byte[] byteArray) {
StringBuilder strDigest = new StringBuilder();
for (byte b : byteArray) {
strDigest.append(byteToHexStr(b));
}
return strDigest.toString();
}
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];
return new String(tempArr);
}
}