洛塔服务号回复005获取代码。
功能说明用户订阅公众号、取消订阅、扫码公众号、开启地理位置、自定义菜单事件等,都会有收到微信发送的事件推送。本篇测试的事件推送
- 订阅公众号:含扫码关注和其他关注方式
- 取消订阅
- 扫描带参数二维码
- 上报地理位置:公众号后台需开启
- 自定义菜单事件
- 自定义菜单点击链接
公众号后台的消息加解密方式选择安全模式。如果用的明文模式,需要对应调整。
- URL:和上一篇的一模一样,本篇使用http://test.lootaa.com/lootaa-wechat/wx3
- Token:任意填写,和代码中的一致
- EncodingAESKey:随机生成即可,代码中要用
- 消息加解密方式:安全模式(推荐)
其中,URL对应代码部署方式为,后台使用springboot来开发,nginx做端口转发。 nginx配置:
location /lootaa-wechat/ { proxy_pass http://127.0.0.1:2022/lootaa-wechat/; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; }
application.properties配置
server.port=2022 server.servlet.context-path=/lootaa-wechat
get方法访问配置(路径使用的http://test.lootaa.com/lootaa-wechat/wx3)
@RestController public class Test005 { @GetMapping("wx3") public void wxGet(HttpServletRequest request, PrintWriter pw) { // 微信加密签名,需要使用本地计算出来的和这个对比,确认是微信发送的消息 String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); // 时间戳 String nonce = request.getParameter("nonce"); // 随机数 String echostr = request.getParameter("echostr"); // 随机字符串 // 将token、timestamp、nonce三个参数进行字典序排序 Listlist = new ArrayList(); list.add("lootaa"); // 公众号后台设置的token list.add(timestamp); list.add(nonce); Collections.sort(list); // 将三个参数字符串拼接成一个字符串进行sha1加密 String tokenStr = ""; for (int i = 0; i < list.size(); i++) { tokenStr += list.get(i); } String signatureStr = DigestUtils.sha1Hex(tokenStr); // 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 if (signature.equals(signatureStr)) { pw.write(echostr); // 原样返回echostr参数内容 } else { pw.write(""); } }
打包部署到服务器后,将此路径和token填写到后台中,点击保存即可。
辅助类为了方便解析,需要两个辅助类,分别是将接收到的request转化为Document(dom4j下的)、将Document转化为map。
- 将request转化为Document
public static Document getDocument(HttpServletRequest request) { SAXReader reader = new SAXReader(); try { InputStream ins = request.getInputStream(); Document doc = reader.read(ins); return doc; } catch (IOException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } return null; }
- 将Document转化为map
public static Map docToMap(Document doc) { Map map = new HashMap(); Element root = doc.getRootElement(); @SuppressWarnings("unchecked") Listlist = root.elements(); for (Element element : list) { map.put(element.getName(), element.getText()); } return map; }微信提供的辅助类
微信提供了多种语言版本的辅助类,可以点击这里直接下载。使用过程中,需要maven引入包。其中codec是必须引入的,dom4j是我测试代码解析使用的。
commons-codeccommons-codecdom4jdom4j1.6.1
微信提供的辅助类包括
- AesException.java
- ByteGroup.java
- PKCS7Encoder.java
- SHA1.java
- WXBizMsgCrypt.java
- XMLParse.java
为了方便测试带参数的二维码关注和扫描事件,需要先生成对应的二维码。本篇使用了微信提供的工具网站:https://mp.weixin.qq.com/debug/cgi-bin/apiinfo
-
先获取token
-
用token获取到ticket
-
拼接得到二维码 最终二维码为https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=【上一步得到的ticket】
用到了上面提供的辅助类。得到map后,处理方式就和明文的基本一致了(除了被动回复需要加密)
String token = "lootaa"; String encodingAesKey = "FpKEYJDuwK92k2juU2z0sUvTmc3hB4W5wGLJEKay8oK"; String appid = "wx276049d6a7551dca"; WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appid); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String msgSignature = request.getParameter("msg_signature"); Document doc = getDocument(request); String result2 = pc.decryptMsg(msgSignature, timestamp, nonce, doc.asXML()); System.out.println("解密后明文: " + result2); Map map = docToMap(DocumentHelper.parseText(result2));被动回复加密
result就是明文情况下要返回的消息,直接调用下encryptMsg即可(pc是加密时候的WXBizMsgCrypt、timestamp和nonce是从request中接收到的原样信息)。
result = pc.encryptMsg(result, timestamp, nonce);公众号订阅事件
如果是普通关注公众号,并不会有参数。如果是通过扫描带参数二维码关注,则会出现字段EventKey,去掉开头的qrscene_就是二维码中的参数。
if(Objects.equals(event, "subscribe")) { //公众号订阅 String eventKey = map.get("EventKey"); //如果是扫码关注,同时二维码有参数,会有这个值,qrscene_开头 if(eventKey != null) { System.out.println("二维码参数" + eventKey.replaceFirst("qrscene_", "")); } String ticket = map.get("Ticket"); //用来生成二维码 if(ticket != null) { String code = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket; System.out.println("二维码:" + code); } String result = "" + "" + "" + "" + System.currentTimeMillis() + "" + "" + ""; result = pc.encryptMsg(result, timestamp, nonce); pw.write(result); }公众号取消订阅
if(Objects.equals(event, "unsubscribe")) { //公众号订取消阅 System.out.println("取消订阅"); pw.write(nonce); }已关注用户扫码事件
这里有个坑,收到推送消息后,返回给微信的如果是nonce,公众号则会提示异常。必须返回空字符串,或者success。
if(Objects.equals(event, "SCAN")) { //已关注的用户扫码 String eventKey = map.get("EventKey"); //qrscene_开头 if(eventKey != null && eventKey.length() > 0) { System.out.println("二维码参数" + eventKey.replaceFirst("qrscene_", "")); } String ticket = map.get("Ticket"); //用来生成二维码 if(ticket != null) { String code = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket; System.out.println("二维码:" + code); } pw.write(""); }上报地理位置事件
必须在公众号后台开启地理位置获取,开启的时候会让选择模式。
开启后,打开公众号的时候就有位置信息了
if(Objects.equals(event, "LOCATION")) { //上报地理位置事件 String latitude = map.get("Latitude"); //纬度 String longitude = map.get("Longitude"); //经度 String precision = map.get("Precision"); //精度,感觉没啥用 System.out.println(latitude + "," + longitude + "," + precision); pw.write(nonce); }点击菜单事件
实现方式可以参照之前写的自定义菜单部分。
if(Objects.equals(event, "CLICK")) { //点击菜单事件 String eventKey = map.get("EventKey"); //事件 KEY 值 String result = "" + "" + "" + "" + System.currentTimeMillis() + "" + "" + ""; result = pc.encryptMsg(result, timestamp, nonce); pw.write(result); }点击菜单跳转链接
if(Objects.equals(event, "VIEW")) { //点击菜单跳转链接 String eventKey = map.get("EventKey"); //链接地址 System.out.println(eventKey); pw.write(nonce); }完整代码
package com.lootaa.wechat; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.digest.DigestUtils; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import com.lootaa.wechat.util.WXBizMsgCrypt; /** * 前置条件:基本配置中开启了服务器配置 * 完整项目源码可关注公众号"lootaayun"(洛塔),回复005获取 */ @RestController public class Test005 { @GetMapping("wx3") public void wxGet(HttpServletRequest request, PrintWriter pw) { // 微信加密签名,需要使用本地计算出来的和这个对比,确认是微信发送的消息 String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); // 时间戳 String nonce = request.getParameter("nonce"); // 随机数 String echostr = request.getParameter("echostr"); // 随机字符串 // 将token、timestamp、nonce三个参数进行字典序排序 Listlist = new ArrayList(); list.add("lootaa"); // 公众号后台设置的token list.add(timestamp); list.add(nonce); Collections.sort(list); // 将三个参数字符串拼接成一个字符串进行sha1加密 String tokenStr = ""; for (int i = 0; i < list.size(); i++) { tokenStr += list.get(i); } String signatureStr = DigestUtils.sha1Hex(tokenStr); // 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 if (signature.equals(signatureStr)) { pw.write(echostr); // 原样返回echostr参数内容 } else { pw.write(""); } } public static Map docToMap(Document doc) { Map map = new HashMap(); Element root = doc.getRootElement(); @SuppressWarnings("unchecked") Listlist = root.elements(); for (Element element : list) { map.put(element.getName(), element.getText()); } return map; } public static Document getDocument(HttpServletRequest request) { SAXReader reader = new SAXReader(); try { InputStream ins = request.getInputStream(); Document doc = reader.read(ins); return doc; } catch (IOException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } return null; } /** * 如果不能确保5秒钟之内回复消息,微信会发起重试。对于此种情况,两种解决方法 1. 所有消息都有MsgId字段,用来做判断避免重复处理 2. * 先返回一条固定消息,然后再启线程单独处理(比如处理完成后在用客服消息接口发送给用户) */ @PostMapping("wx3") public void wxPost(HttpServletRequest request, HttpServletResponse response, PrintWriter pw) throws Exception { String token = "lootaa"; String encodingAesKey = "FpKEYJDuwK92k2juU2z0sUvTmc3hB4W5wGLJEKay8oK"; String appid = "wx276049d6a7551dca"; WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appid); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String msgSignature = request.getParameter("msg_signature"); Document doc = getDocument(request); String result2 = pc.decryptMsg(msgSignature, timestamp, nonce, doc.asXML()); System.out.println("解密后明文: " + result2); Map map = docToMap(DocumentHelper.parseText(result2)); String messageType = map.get("MsgType"); //这个值如果是event表示是事件推送;如果是其他字符,参照Test004 if(Objects.equals("event", messageType)) { String event = map.get("Event"); //这个是事件的具体类型 if(Objects.equals(event, "subscribe")) { //公众号订阅 String eventKey = map.get("EventKey"); //如果是扫码关注,同时二维码有参数,会有这个值,qrscene_开头 if(eventKey != null) { System.out.println("二维码参数" + eventKey.replaceFirst("qrscene_", "")); } String ticket = map.get("Ticket"); //用来生成二维码 if(ticket != null) { String code = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket; System.out.println("二维码:" + code); } String result = "" + "" + "" + "" + System.currentTimeMillis() + "" + "" + ""; result = pc.encryptMsg(result, timestamp, nonce); pw.write(result); } if(Objects.equals(event, "unsubscribe")) { //公众号订取消阅 System.out.println("取消订阅"); pw.write(nonce); } if(Objects.equals(event, "SCAN")) { //已关注的用户扫码 String eventKey = map.get("EventKey"); //qrscene_开头 if(eventKey != null && eventKey.length() > 0) { System.out.println("二维码参数" + eventKey.replaceFirst("qrscene_", "")); } String ticket = map.get("Ticket"); //用来生成二维码 if(ticket != null) { String code = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket; System.out.println("二维码:" + code); } pw.write(""); } if(Objects.equals(event, "LOCATION")) { //上报地理位置事件 String latitude = map.get("Latitude"); //纬度 String longitude = map.get("Longitude"); //经度 String precision = map.get("Precision"); //精度,感觉没啥用 System.out.println(latitude + "," + longitude + "," + precision); pw.write(nonce); } if(Objects.equals(event, "CLICK")) { //点击菜单事件 String eventKey = map.get("EventKey"); //事件 KEY 值 String result = "" + "" + "" + "" + System.currentTimeMillis() + "" + "" + ""; result = pc.encryptMsg(result, timestamp, nonce); pw.write(result); } if(Objects.equals(event, "VIEW")) { //点击菜单跳转链接 String eventKey = map.get("EventKey"); //链接地址 System.out.println(eventKey); pw.write(nonce); } } } }