- A+
本编文章手把手教大家如何快速搭建微信个人订阅号被动回复功能
未被认证的微信个人订阅号可调用的接口很少,大多都没有权限,如果想使用微信更多的官方接口,需要进行认证。
先来看看最终实现的功能截图:
1. 关注后被动的回复:
2. 输入消息被动回复:
准备工作
- 微信个人订阅号(未认证的即可)
- 对公服务器Ecs(阿里云或腾讯云)
- SpringBoot
- docker
搭建步骤
1. 配置个人订阅号
第一需要我们先拥有一个微信订阅号,可以去微信公众号官网进行注册,并关联自己的微信号。这里注册的内容就不再详述了,自行注册即可。
配置好所有信息之后,需要开启开发者模式,然后点击基本配置。
在基本配置页面配置属于自己的服务器以及设定好的Token等内容
上述内容中我们只需要关注服务器配置
服务器地址URL: 这里需要填写上述准备好的公网ECS地址,注意必须是公网地址,否则微信服务器接受无法找到地址。
令牌(Token): 这是自己约定好的与服务器链接的密钥,自己可以随意设置,但是在服务器上的程序必须要通过设置好的Token验证。
消息加解密密钥(EncodingAESKey): 微信生成的加解密密钥。
消息加解密方式: 加解密方式,这里可以选择明文方式。
2. 编写SpringBoot程序
既然要配置服务器URL,那么要让微信服务器找到我们,就必须先配置一个WechatController。
@Resource
private WeChatService weChatService;
/**
* 处理微信服务器发来的get请求,进行签名的验证
*
* @param signature 微信端发来的签名
* @param timestamp 微信端发来的时间戳
* @param nonce 微信端发来的随机字符串
* @param echostr 微信端发来的验证字符串
*/
@GetMapping("/wechat")
public String validate(@RequestParam(value = "signature") String signature,
@RequestParam(value = "timestamp") String timestamp,
@RequestParam(value = "nonce") String nonce,
@RequestParam(value = "echostr") String echostr) {
log.info("wechat request,start check it !");
return WeChatHelper.checkSignature(signature, timestamp, nonce) ? echostr : null;
}
/**
* 此处是处理微信服务器的消息转发的
*/
@PostMapping("/wechat")
public String processMsg(HttpServletRequest request) {
log.info("message received, start process message!");
// 调用核心服务类接收处理请求
return weChatService.processRequest(request);
}
第一步是微信服务器的校验,配置了一个接口/wechat,注意这里的接口名称要和上述微信订阅号中配置的<服务器地址URL>中的接口名称一致,我们测试配置的是/wechat,上面的参数大家可以对照我们订阅号的基本配置看一下,signature是设定好的Token
第二个方法就是接受微信服务器发来的请求,可以通过具体的请求在weChatService中实现自身的业务逻辑。
校验代码在WechatHelper类中,除了校验的工具方法外,还有一些常用的消息、图文处理方法。
如下:
/**
* @Description 微信帮助类
* @Author lmx.zh
* @date 2020-04-30 17:26
* @Copyright 2019 Alibaba.com All right reserved.
*/
public class WeChatHelper {
/**
* 验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[] {WeChatConstant.TOKEN, timestamp, nonce};
// 将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");
// 将三个参数字符串拼接成一个字符串进行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;
}
/**
* 将字节数组转换为十六进制字符串
*
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为十六进制字符串
*
* @param mByte
* @return
*/
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];
String s = new String(tempArr);
return s;
}
private 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;
}
}
}
}
/**
* 解析微信发来的请求(xml)
*
* @param request
* @return
* @throws Exception
*/
@SuppressWarnings({"unchecked"})
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList) { map.put(e.getName(), e.getText()); }
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
public static String mapToXML(Map map) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
mapToXML2(map, sb);
sb.append("</xml>");
try {
return sb.toString();
} catch (Exception e) {
}
return null;
}
private static void mapToXML2(Map map, StringBuffer sb) {
Set set = map.keySet();
for (Iterator it = set.iterator(); it.hasNext(); ) {
String key = (String)it.next();
Object value = map.get(key);
if (null == value) { value = ""; }
if (value.getClass().getName().equals("java.util.ArrayList")) {
ArrayList list = (ArrayList)map.get(key);
sb.append("<" + key + ">");
for (int i = 0; i < list.size(); i++) {
HashMap hm = (HashMap)list.get(i);
mapToXML2(hm, sb);
}
sb.append("</" + key + ">");
} else {
if (value instanceof HashMap) {
sb.append("<" + key + ">");
mapToXML2((HashMap)value, sb);
sb.append("</" + key + ">");
} else {
sb.append("<" + key + "><![CDATA[" + value + "]]></" + key + ">");
}
}
}
}
/**
* 回复文本消息
*
* @param requestMap
* @param content
* @return
*/
public static String sendTextMsg(Map<String, String> requestMap, String content) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("ToUserName", requestMap.get(WeChatConstant.FromUserName));
map.put("FromUserName", requestMap.get(WeChatConstant.ToUserName));
map.put("MsgType", WeChatConstant.RESP_MESSAGE_TYPE_TEXT);
map.put("CreateTime", DateHelper.formatDate(new Date()));
map.put("Content", content);
return mapToXML(map);
}
/**
* 回复图文消息
*
* @param requestMap
* @param items
* @return
*/
public static String sendArticleMsg(Map<String, String> requestMap, List<ArticleItem> items) {
if (items == null || items.size() < 1) {
return "";
}
Map<String, Object> map = new HashMap<String, Object>();
map.put("ToUserName", requestMap.get(WeChatConstant.FromUserName));
map.put("FromUserName", requestMap.get(WeChatConstant.ToUserName));
map.put("MsgType", "news");
map.put("CreateTime", DateHelper.formatDate(new Date()));
List<Map<String, Object>> Articles = new ArrayList<Map<String, Object>>();
for (ArticleItem item : items) {
Map<String, Object> itemMap = new HashMap<String, Object>();
Map<String, Object> itemContent = new HashMap<String, Object>();
itemContent.put("Title", item.getTitle());
itemContent.put("Description", item.getDescription());
itemContent.put("PicUrl", item.getPicUrl());
itemContent.put("Url", item.getUrl());
itemMap.put("item", itemContent);
Articles.add(itemMap);
}
map.put("Articles", Articles);
map.put("ArticleCount", Articles.size());
return mapToXML(map);
}
}
接受用户发来的请求,进行具体业务逻辑处理的类是WeChatServiceImpl,我们这里主要看一下processRequest实现方法。
由于接受到用户的消息微信服务器是通过xml的方式发送到服务器上的,所以我们收到消息后,需要先解析xml格式,具体方法已经在工具类给出了,大家可以参考下。
如下图是微信服务器进行加密后发送给服务器中的消息模板:
processRequest具体实现方法
@Override
public String processRequest(HttpServletRequest request) {
long startProcessTime = System.currentTimeMillis();
//xml格式的消息数据
String respXml = null;
//默认返回的文本消息内容
String respContent;
try {
// 调用parseXml方法解析请求消息
Map<String, String> requestMap = WeChatHelper.parseXml(request);
//消息类型
String msgType = requestMap.get(WeChatConstant.MsgType);
String mes = null;
//文本消息
if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_TEXT)) {
respContent = "您发送的是文本消息!";
respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
}
//图片消息
else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_IMAGE)) {
respContent = "您发送的是图片消息!";
respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
}
// 语音消息
else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_VOICE)) {
respContent = "您发送的是语音消息!";
respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
}
// 视频消息
else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_VIDEO)) {
respContent = "您发送的是视频消息!";
respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
}
// 地理位置消息
else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_LOCATION)) {
respContent = "您发送的是地理位置消息!";
respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
}
// 链接消息
else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_LINK)) {
respContent = "您发送的是链接消息!";
respXml = WeChatHelper.sendTextMsg(requestMap, respContent);
}
// 事件推送
else if (msgType.equals(WeChatConstant.REQ_MESSAGE_TYPE_EVENT)) {
// 事件类型
String eventType = requestMap.get(WeChatConstant.Event);
// 关注
if (eventType.equals(WeChatConstant.EVENT_TYPE_SUBSCRIBE)) {
respXml = WeChatHelper.sendTextMsg(requestMap, WeChatConstant.SUBSCRIBE_MSG);
}
// 取消关注
else if (eventType.equals(WeChatConstant.EVENT_TYPE_UNSUBSCRIBE)) {
// TODO 取消订阅后用户不会再收到公众账号发送的消息,因此不需要回复
}
// 扫描带参数二维码
else if (eventType.equals(WeChatConstant.EVENT_TYPE_SCAN)) {
// TODO 处理扫描带参数二维码事件
}
// 上报地理位置
else if (eventType.equals(WeChatConstant.EVENT_TYPE_LOCATION)) {
// TODO 处理上报地理位置事件
}
// 自定义菜单
else if (eventType.equals(WeChatConstant.EVENT_TYPE_CLICK)) {
// TODO 处理菜单点击事件
}
}
if (respXml == null) { respXml = WeChatHelper.sendTextMsg(requestMap, mes); }
log.info("wechat reponse user message is " + respXml);
return respXml;
} catch (Exception e) {
log.info("微信公众号消息处理时出现异常 " + e.getMessage());
}
long endProcessTime = System.currentTimeMillis();
log.info("wehcat process request time consuming is " + (endProcessTime - startProcessTime) + "ms");
return "";
}
我们可以看到解析xml以后是通过msgType来判断消息类型,然后通过不同的消息类型进行不同的业务逻辑处理,这里大家可以根据自身业务进行处理。
对接微信端的常量WechatConstant类也罗列一下
//APPID
public static final String appID = "appid";
//appsecret
public static final String appsecret = "appsecret";
// Token
public static final String TOKEN = "MyToken";
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
public static final Object REQ_MESSAGE_TYPE_TEXT = "text";
public static final Object REQ_MESSAGE_TYPE_IMAGE = "image";
public static final Object REQ_MESSAGE_TYPE_VOICE = "voice";
public static final Object REQ_MESSAGE_TYPE_VIDEO = "video";
public static final Object REQ_MESSAGE_TYPE_LOCATION = "location";
public static final Object REQ_MESSAGE_TYPE_LINK = "link";
public static final Object REQ_MESSAGE_TYPE_EVENT = "event";
public static final Object EVENT_TYPE_SUBSCRIBE = "SUBSCRIBE";
public static final Object EVENT_TYPE_UNSUBSCRIBE = "UNSUBSCRIBE";
public static final Object EVENT_TYPE_SCAN = "SCAN";
public static final Object EVENT_TYPE_LOCATION = "LOCATION";
public static final Object EVENT_TYPE_CLICK = "CLICK";
public static final String FromUserName = "FromUserName";
public static final String ToUserName = "ToUserName";
public static final String MsgType = "MsgType";
public static final String Content = "Content";
public static final String Event = "Event";
这样我们就配置完对接微信个人订阅号的SpringBoot程序了,是不是很简单。只需要一个Controller、Helper、Service即可。
3. docker部署
程序写完后,我们就需要部署了,将写好的SpringBoot程序打成Dokcer镜像,部署到对接的服务器上。如果有小伙伴不会使用docker打包镜像,可以参考文章如何使用DockerFile构建SpringBoot工程镜像?
镜像上传到服务器之后,进行镜像load以及run,注意这里建议使用如下命令进行run:
//--name 容器名称
//-p 80:8001 注意这里一定要将对外暴露80端口,因为微信服务器验证只认识80端口,8001是springboot工程端口,可以任意设置。
//images 镜像名
docker run --name myFirstWechat -idt -p 80:8001 images
4. 验证
启动容器后,进入基本配置页面,点击启用,进行接入。
启动成功后,我们就可以关注该微信公众号,进行验证了。
截取部分日志信息(这里的日志是根据自身的业务逻辑截取的,大家可各自实现对应的业务逻辑)
16:31:27.034 default [http-nio-8001-exec-8] INFO c.m.m.a.controller.WeChatController - message received, start process message!
16:31:27.035 default [http-nio-8001-exec-8] INFO c.m.m.a.s.impl.WeChatServiceImpl - wechat request message is 三体
16:31:27.035 default [http-nio-8001-exec-8] INFO c.m.m.a.s.impl.WeChatServiceImpl - get into received wechat message processing…
- 我的微信
- 加好友一起交流!
-
- 微信公众号
- 关注公众号获取分享资源!
-