开发背景:
需要用户通过二维码关注公司的公众号以后获得openID和用户ID(userid)关联,然后根据需求给用户发送预警消息
注意:在微信公众号后台,设置了服务器配置URL并启用后,会导致微信后台设置的回复规则,以及底部菜单都全部失效!直接清空了!因为这时候微信已经把公众号消息和事件推送给开发者配置的url中,让开发者进行处理了。
开发准备:
1.可以先阅读下官方文档https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140454
2.需要在微信后台配置服务器配置,微信会在你修改这个配置的时候给你填写的URL(一定要外网能访问的到)发送数据,需要提前把项目上传服务器
3.这个东西配置好之后可以点击开启了,开启后微信会把所有用户发送的信息,给转发给填写的url上。开发者必须在五秒内回复微信服务器。否则微信会进行三次重试,重试均失败后,会给用户提示“该公众号暂时无法提供服务,请稍后再试”。如果你的填写url的程序因某种原因挂掉的话根本就无法请求到的话会给用户提示“该公众号提供的服务出现故障,请稍后再试”
注意:
1.填写URL下方有这么一句话“必须以http://或https://开头,分别支持80端口和443端口。”。如果填ip的话必须得是这两个端口,要不然就会提示“请输入合法的URL”(如:“ http://36.105.244.13/openwx/eventpush ”等同于 “ http://36.105.244.13:80/openwx/eventpush ”、“ http://36.105.244.13:443/openwx/eventpush ”(不知道这个为啥不合法了就)),如果你的程序是其他端口如8081的话,可以用域名(该域名代理到程序ip的8081端口)的方式填写(如:“ http://xiaoqiang.tunnel.qydev.com/openwx/eventpush ”)
2.如果无法访问到你的url会报“请求URL超时”,如果填写的token和你程序里的不一致的话会报“token验证失败”,也不知道啥情况下会报“系统发生错误,请稍后重试”。(垃圾微信有时候好像报的不准)
3.填写的url接口还必须同时支持get(修改配置填写好url后点“提交”按钮后微信会向你的这个url发送get请求)和post(事件推送如取消/关注公众号事件、扫描带参数的二维码)请求
说明:
1.在我们首次提交验证申请时,微信服务器将发送GET请求到填写的URL上,并且带上四个参数(signature、timestamp、nonce、echostr),通过对签名(即signature)的效验,来判断此条消息的真实性。此后,每次接收用户消息的时候,微信也都会带上这三个参数(signature、timestamp、nonce)访问我们设置的URL,和第一次相同我们依然需要通过对签名的效验判断此条消息的真实性。效验方式与首次提交验证申请一致。
signature:微信加密签名,signature结合了我们自己填写的token参数和请求中的timestamp参数、nonce参数。通过检验signature对请求进行校验(代码在下面提供)。若确认此次GET请求来自微信服务器,则原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。
timestamp:时间戳
nonce:随机数
echostr:随机字符串
2.令牌Token
Token:可由我们自行定义,主要作用是参与生成签名,与微信请求的签名进行比较
3.消息加解密密钥EncodingAESKey
EncodingAESKey:可由我们自行定义或随机生成,主要作用是参与接收和推送给公众平台消息的加解密
4.消息加解密方式
此处我选择的是明文模式,大家可以根据自己的具体需求,选择相应的模式
代码实例一:
package com.imooc.controller;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.imooc.conf.DefaultExpireKey;
import com.imooc.conf.ExpireKey;
import com.imooc.conf.SignatureUtil;
import com.imooc.conf.XMLMessage;
import com.imooc.conf.XMLTextMessage;
/**
* 消息接收控制层
* @author YaoShiHang
* @Date 15:15 2017-10-16
*/
@Controller
//或者@RestController
public class WxqrcodeController {
private final String TOKEN="weixin4"; //开发者设置的token
private Logger loger = Logger.getLogger(getClass());
//重复通知过滤
private static ExpireKey expireKey = new DefaultExpireKey();
//微信推送事件 url
@RequestMapping("/openwx/getticket")
public void getTicket(HttpServletRequest request, HttpServletResponse response)
throws Exception {
ServletOutputStream outputStream = response.getOutputStream();
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
//首次请求申请验证,返回echostr
if(echostr!=null){
outputStreamWrite(outputStream,echostr);
return;
}
System.out.println("signature--->"+signature);
// 验证请求签名,要不然不知道是不是你本人操作后向你程序发送的请求,无法保证安全性
if(!signature.equals(SignatureUtil.generateEventMessageSignature(TOKEN,timestamp,nonce))){
System.out.println("The request signature is invalid");
return;
}
boolean isreturn= false;
loger.info("1.收到微信服务器消息");
Map<String, String> wxdata=parseXml(request);
if(wxdata.get("MsgType")!=null){
if("event".equals(wxdata.get("MsgType"))){
loger.info("2.1解析消息内容为:事件推送");
if( "subscribe".equals(wxdata.get("Event"))){
loger.info("2.2用户第一次关注 返回true哦");
isreturn=true;
}
}
}
if(isreturn == true){
//转换XML
System.out.println("wxdata--->"+wxdata);
String key = wxdata.get("FromUserName")+ "__"
+ wxdata.get("ToUserName")+ "__"
+ wxdata.get("MsgId") + "__"
+ wxdata.get("CreateTime");
loger.info("3.0 进入回复 转换对象:"+key);
if(expireKey.exists(key)){
//重复通知不作处理
loger.info("3.1 重复通知了");
return;
}else{
loger.info("3.1 第一次通知");
expireKey.add(key);
}
loger.info("3.2 回复你好");
//创建回复
XMLMessage xmlTextMessage = new XMLTextMessage(
wxdata.get("FromUserName"),
wxdata.get("ToUserName"),
"你好");
//回复
xmlTextMessage.outputStreamWrite(outputStream);
return;
}
loger.info("3.2 回复空");
outputStreamWrite(outputStream,"");
}
/**
* 数据流输出
* @param outputStream
* @param text
* @return
*/
private boolean outputStreamWrite(OutputStream outputStream, String text){
try {
outputStream.write(text.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* dom4j 解析 xml 转换为 map
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从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;
}
}
其他的依赖文件可去这个开源项目里找:https://github.com/liyiorg/weixin-popular
代码实例二:
本来我想直接用上面的方式来做这个事件推送功能,虽然我们公司是spring boot项目,但是整合了Jersey,导致无法用一个接口同时处理get和post请求
失败代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.apache.commons.codec.digest.DigestUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Path("/openwx") //这个必须有不能注释否则访问不到这个url链接,也是奇了怪了,好像和spring boot中的@RequestMapping用法不一样
@Component
public class BangdanItemResource {
private static Logger logger = LoggerFactory.getLogger(BangdanItemResource.class);
private final String TOKEN = "weixin4";
@GET
// @POST //用@GET注解就无法使用@POST注解,就这一点导致该方案无法通过,其他代码逻辑都是对的
@Path("/getticket")
public Response getTicket(@Context HttpServletRequest request,
@Context HttpServletResponse response) throws Exception {
ServletOutputStream outputStream = response.getOutputStream();
String signature = request.getParameter("signature");
System.out.println("signature--->" + signature);
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
// 首次请求申请验证,返回echostr
if (echostr != null) {
outputStreamWrite(outputStream, echostr);
System.out.println("1-------------------->");
return Response.status(Response.Status.OK).build();
}
// 验证请求签名
if (!signature.equals(generateEventMessageSignature(TOKEN, timestamp, nonce))) {
System.out.println("The request signature is invalid");
return Response.status(Response.Status.OK).build();
}
boolean isreturn = false;
logger.info("收到微信服务器消息");
Map<String, String> wxdata = parseXml(request);
if (wxdata.get("MsgType") != null) {
if ("event".equals(wxdata.get("MsgType"))) {
logger.info("解析消息内容为:事件推送");
if ("subscribe".equals(wxdata.get("Event"))) {
logger.info("用户第一次关注");
isreturn = true;
}
}
}
if (isreturn == true) {
// 转换XML
if (wxdata.get("Ticket") != null) { // 如果是通过扫描带参数二维码关注则可获得用户的openID
String openid = wxdata.get("FromUserName");
System.out.println("openid-->" + openid);
}
}
return Response.status(Response.Status.OK).build();
}
// 数据流输出
private boolean outputStreamWrite(OutputStream outputStream, String text) {
try {
outputStream.write(text.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
// 生成事件消息接收签名
public static String generateEventMessageSignature(String token, String timestamp, String nonce) {
String[] array = new String[] { token, timestamp, nonce };
Arrays.sort(array);
String s = arrayToDelimitedString(array, "");
return DigestUtils.shaHex(s);
}
public static String arrayToDelimitedString(Object[] arr, String delim) {
if (arr == null || arr.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
if (i > 0) {
sb.append(delim);
}
sb.append(arr[i]);
}
return sb.toString();
}
// dom4j 解析 xml 转换为 map
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从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;
}
}
转换思路:
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@WebServlet(urlPatterns = "/openwx/getticket")
public class HuiController extends HttpServlet {
private static final long serialVersionUID = -2776902810130266533L;
private static Logger log = LoggerFactory.getLogger(HuiController.class);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String signature = req.getParameter("signature");
String timestamp = req.getParameter("timestamp");
String nonce = req.getParameter("nonce");
String echostr = req.getParameter("echostr");
// 此处需要检验signature对网址接入合法性进行校验。我这里为了方便没弄,想弄的话可参考上面两例的代码
log.info(signature + " : " + timestamp + " : " + nonce + " : " + echostr);
PrintWriter out = resp.getWriter();
out.write(echostr);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("1--->");
// 此处需要检验signature对网址接入合法性进行校验。
Scanner scanner = new Scanner(req.getInputStream());
resp.setContentType("application/xml");
resp.setCharacterEncoding("UTF-8");
// 1、获取用户发送的信息
StringBuffer sb = new StringBuffer(100);
while (scanner.hasNextLine()) {
sb.append(scanner.nextLine());
}
}
}
注意:在spring boot中使用Servlet可能会报404访问不到页面的问题,那是可能因为你没在主方法上使用@ServletComponentScan注解造成的,详情请看我的另一篇文章:https://blog.csdn.net/m0_37739193/article/details/85097477
参考:
https://blog.csdn.net/chenmmo/article/details/78299238
https://www.oschina.net/code/snippet_778955_17411
https://blog.csdn.net/Goodbye_Youth/article/details/80590831 (文章里面抛出异常的写法值得借鉴)