5年了,你知道我这5年是怎么过的吗?! 5年前我做过一次公众号开发,当时写了篇简单的博客:https://blog.csdn.net/User_xiangpeng/article/details/50378806,时隔5年,再次入坑!
一、接入指南
1、当时系先准备公众号,该注册注册,该认证认证;不过现在公众平台为了方便程序员们先开始开发,推出了测试公众号,没有准备好公众号的可以先申请个测试号进行开发。申请测试公众号
2、使用你的开发语言开发好一个接口,用来配置到微信后台做消息通知,且需要有一个外网能访问到的地址(必须80或443端口)能访问该接口。本人在开发时是让运维同学做了个内网穿透,
配置了一个单独的域名穿透到本地方便进行开发和测试。该接口需要同时支持GET和POST请求,GET请求中需要对微信发送的签名进行校验,其中会用到上图中的Token(自己随便定义的一个密钥)
来进行接口鉴权,将接口地址(URL)和Token配置好提交后,微信服务器会立马给该地址发送GET请求鉴权,接口中校验签名通过并返回echostr给微信服务器,接口配置才能成功。官方文档
GET接口代码示例:
/**
* 微信回调接口校验
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @return
*/
@RequestMapping(value = "v1/wx/callback", method = RequestMethod.GET)
public String doGet(@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr) {
LOGGER.info("yun callback from wechat,[signature={}, timestamp={}, nonce={}, echostr={}]", signature,
timestamp, nonce, echostr);
try {
// 请求参数非空判断
if (StrUtil.hasBlank(signature, timestamp, nonce, echostr)) {
LOGGER.error(BaseException.ILLEGAL_OPERATION);
return MessageUtil.message(BaseException.ILLEGAL_OPERATION, messageSource);
}
// 校验签名
if (yunWxAppNoticeService.checkSign(signature, timestamp, nonce)) {
return echostr;
}
return "signature check failed.";
} catch (Exception e) {
LOGGER.error("yun callback error.", e);
return "yun callback error.";
}
}
@Override
public boolean checkSign(String signature, String timestamp, String nonce) {
// 生成签名
List<String> signList = new ArrayList<>();
// 配置文件中读取token(微信后台配置的Token)
signList.add(token);
signList.add(timestamp);
signList.add(nonce);
// 1. 将token、timestamp、nonce三个参数进行字典序排序
signList.sort(Comparator.naturalOrder());
// 2. 将三个参数字符串拼接成一个字符串进行sha1加密
StringBuilder signSb = new StringBuilder();
signList.forEach(signSb::append);
// signStr = CiphertextUtil.passAlgorithmsCiphering(signStr, CiphertextUtil.SHA_1);
String signStr = DigestUtils.sha1Hex(signSb.toString());
// 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
LOGGER.info("signature=[{}], signStr=[{}]", signature, signStr);
return signature.equals(signStr);
}
3、配置ok后,微信服务器就会将公众号的所有消息以POST的请求方式推送到我们配置的URL上,所以接下来我们就开始接收并解析微信服务器给我们的消息。由于微信给的是xml报文,
所以这里需要用到XmlUtils,我这里自己写了个,分享给大家参考和使用。
/**
* 微信消息通知
*
* @param request
* @return
*/
@RequestMapping(value = "v1/wx/callback", method = RequestMethod.POST)
public String doPost(HttpServletRequest request) {
String xmlData = HttpRequestUtil.getDataBodyByRequest(request);
if (StrUtil.isBlank(xmlData)) {
return StringUtils.EMPTY;
}
// 扫码或关注公众号
MessageBean messageBean = XmlUtils.toOejctByXml(xmlData, MessageBean.class);
LOGGER.info("wechat callback message=[{}]", JsonUtil.toJson(messageBean));
if (null == messageBean) {
LOGGER.error("wechat callback message is null. xmlData=[{}]", xmlData);
return StringUtils.EMPTY;
}
//消息类型
MsgTypeEnum msgTypeEnum = MsgTypeEnum.getByName(messageBean.getMsgType());
if (null == msgTypeEnum) {
return StringUtils.EMPTY;
}
//根据不同消息类型做不同处理
switch (msgTypeEnum) {
case EVENT:
return yunWxAppNoticeService.doEvent(request, messageBean);
case TEXT:
MessageBean reply = MessageBean.build(MsgTypeEnum.TEXT).fromUserName(messageBean.getToUserName())
.toUserName(messageBean.getFromUserName()).content("ok, i get " + messageBean.getContent());
LOGGER.debug("reply=[{}]", XmlUtils.toXmlString(reply));
return XmlUtils.toXmlString(reply);
default:
break;
}
return StringUtils.EMPTY;
}
public static String getDataBodyByRequest(HttpServletRequest request) {
// 获取输入流
ServletInputStream in = null;
StringBuilder sb = null;
try {
in = request.getInputStream();
byte[] b = new byte[4096];
sb = new StringBuilder();
for (int n; (n = in.read(b)) != -1;) {
sb.append(new String(b, 0, n, StandardCharsets.UTF_8));
}
} catch (IOException e) {
return null;
}
return sb.toString();
}
package com.lieni.core.util;
import java.io.Writer;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
public class XmlUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(XmlUtils.class);
private static final String TOP = "xml";
private static final String PREFIX_CDATA = "<![CDATA[";
private static final String SUFFIX_CDATA = "]]>";
/**
* xml 格式字符串转java对象
* <p>
* example: <xml> <userName>bryant</userName> </xml>
*
* @param xml
* @param c
* @param <T>
* @return
*/
public static <T> T toOejctByXml(String xml, Class<T> c) {
XStream xs = new XStream(new DomDriver());
xs.alias(TOP, c);
try {
return (T) xs.fromXML(xml);
} catch (Exception e) {
LOGGER.error("toOejctByXml error.", e);
return null;
}
}
/**
* java对象转xml格式字符串
*
* @param t
* @param <T>
* @return example: <xml> <userName>bryant</userName> </xml>
*/
public static <T> String toXmlString(T t) {
XStream xs = new XStream();
xs.alias(TOP, t.getClass());
try {
return xs.toXML(t);
} catch (Exception e) {
LOGGER.error("toXmlString error.", e);
return StringUtils.EMPTY;
}
}
public static <T> String toXmlString(T t, boolean addCDATA) {
XStream xs = addCDATA ? new XStream(new XppDriver() {
@Override
public HierarchicalStreamWriter createWriter(Writer writer) {
return new PrettyPrintWriter(writer) {
protected void writeText(QuickWriter writer, String text) {
writer.write(PREFIX_CDATA + text + SUFFIX_CDATA);
}
};
}
}) : new XStream();
xs.alias(TOP, t.getClass());
try {
return xs.toXML(t);
} catch (Exception e) {
LOGGER.error("toXmlString error.", e);
return StringUtils.EMPTY;
}
}
public static void main(String[] args) {
// String xml = "<xml><ToUserName><![CDATA[gh_2782b51f3718]]></ToUserName>\n"
// + "<FromUserName><![CDATA[oW4qpuETSbxjDBVqylvquR8TxokE]]></FromUserName>\n"
// + "<CreateTime>1607579313</CreateTime>\n" + "<MsgType><![CDATA[event]]></MsgType>\n"
// + "<Event><![CDATA[subscribe]]></Event>\n" + "<EventKey><![CDATA[]]></EventKey>\n" + "</xml>";
// MessageBean messageBean = XmlUtils.toOejctByXml(xml, MessageBean.class);
// System.out.println("解析结果:");
// System.out.println(JsonUtil.toJson(messageBean));
//
// String xmlString = XmlUtils.toXmlString(messageBean);
// System.out.println("xmlString:");
// System.out.println(xmlString);
}
}
具体消息及事件可参考官方接口文档
二、常用的事件及消息处理问题
1、接收消息的POST接口需给出正确的返回,否则会触发微信服务器的重试机制,推荐返回"success"或空字符串。
2、关注/取关事件推送(详细说明),当微信用户关注或取消关注(unsubscribe)微信服务器将会发送POST请求并带上如下报文到我们服务器(URL),我们经常会遇到关注立即给用户
一个反馈这样的业务场景,所以这个时候就要针对事件为subscribe的消息做一些处理。
3、接收消息并自动回复(被动回复),当微信用户向我们的公众号发消息时,微信会发送如下报文到服务器(URL);我们可以根据获取到的消息内容实现关键字回复的功能。
接收到该请求后可以直接给接口返回一个符合微信要求的xml结构报文,实现立即回复用户的效果:
这里在开发时需要注意几个问题:
1)、返回的xml报文标签中不能有空格
2)、ToUserName与FormUserName不能写反或相同
3)、<![CDATA[]]不是必须的
4)、报文中的标签及枚举值严格区分大小写,强烈建议创建javaBean方便做数据解析与包装