洛塔服务号回复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三个参数进行字典序排序
List list = 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")
List list = root.elements();
for (Element element : list) {
map.put(element.getName(), element.getText());
}
return map;
}
微信提供的辅助类
微信提供了多种语言版本的辅助类,可以点击这里直接下载。使用过程中,需要maven引入包。其中codec是必须引入的,dom4j是我测试代码解析使用的。
commons-codec
commons-codec
dom4j
dom4j
1.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三个参数进行字典序排序
List list = 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")
List list = 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);
}
}
}
}