微信開發二--事件推送
開發背景:
需要使用者通過二維碼關注公司的公眾號以後獲得openID和使用者ID(userid)關聯,然後根據需求給使用者傳送預警訊息
注意:在微信公眾號後臺,設定了伺服器配置URL並啟用後,會導致微信後臺設定的回覆規則,以及底部選單都全部失效!直接清空了!因為這時候微信已經把公眾號訊息和事件推送給開發者配置的url中,讓開發者進行處理了。
開發準備:
1.可以先閱讀下官方文件https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140454
2.需要在微信後臺配置伺服器配置,微信會在你修改這個配置的時候給你填寫的URL(一定要外網能訪問的到)傳送資料,需要提前把專案上傳伺服器
3.
注意:
1.填寫URL下方有這麼一句話“必須以http://或https://開頭,分別支援80埠和443埠。”。如果填ip的話必須得是這兩個埠,要不然就會提示“請輸入合法的URL”(如:“ http://36.105.244.13/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 (文章裡面丟擲異常的寫法值得借鑑)