開發背景:
需要用戶通過二維碼關注公司的公衆號以後獲得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());
}
}
}
參考:
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 (文章裏面拋出異常的寫法值得借鑑)