微信公眾號之Spring mvc訊息伺服器實現自定義規則回覆
微信公眾號的訊息自動回覆是微信公眾平臺給公眾賬號提供的一種基礎能力。在微信公眾號的管理平臺,微信開放了三種簡單基礎的訊息自動回覆規則,用Spring mvc實現訊息伺服器還是比較簡單高效。
- 關鍵詞回覆:根據使用者傳送內容進行關鍵字的匹配回覆,相應關鍵字可觸發相應的回覆。此類回覆無回覆限制
- 收到訊息回覆:除了非關鍵詞的其它回覆,功能單一且回覆次數有限制,頻率大概在1小時1-2次左右
- 被關注回覆:即普通使用者首次關注公眾賬號鎖收到的訊息回覆設定,僅一次回覆
自定義伺服器回覆
當我們需要強大的訊息回覆邏輯時,微信公眾平臺自帶的回覆規則設定就無法滿足我們的需求,這時就需要我們自己實現回覆規則。微信開放這種能力,接下來幾步實現自定義訊息回覆。
1、基本設定概述
在微信公眾號的開發基本設定中有一個“伺服器配置”的區域,很多做公眾號的都不知道其配置有何作用,接下來一一解答。
微信基礎的功能“自動回覆”和“自定義選單”,在未做任何設定的之前,我們都可以通過微信公眾平臺做出預期設定。但當我們設定了“伺服器配置”後,至少這兩大功能,微信公眾平臺就不提供了,需要我們自己呼叫微信的介面實現。會有以下提示:
這樣, 你大概就明白配置的作用了。既然伺服器配置是必經之路,那該配置到底該如果配置呢?先看一下我的配置吧
注意:如果伺服器收不到微信的請求,可能是你的伺服器配置雖然配置了,但並沒有啟用,請看右上角。
2、伺服器地址配置
伺服器地址URL必須是一個可以通過http正常訪問的連結,當用戶關注或回覆公眾號時,微信會將訊息以HTTP GET轉發到該伺服器地址中。令牌(Token)是配置伺服器地址驗證的關鍵,微信為防止他人非法冒充該公眾號伺服器,引入Token該拂去必須正確響應微信傳送的Token驗證。Token自定義設定。
以下是伺服器驗證程式碼:
@RequestMapping(value="/wechat", method=RequestMethod.GET)
public void wechatGet(HttpServletRequest request, HttpServletResponse response) {
logger.info("wechat get request start...");
PrintWriter print;
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
logger.info("\n[signature=" + signature
+ "][timestamp=" + timestamp
+ "][nonce=" + nonce
+ "][echostr=" + echostr
+ "][token=" + token + "]");
// 通過檢驗signature對請求進行校驗,若校驗成功則原樣返回echostr,表示接入成功,否則接入失敗
if (signature != null && WeChatUtil.checkSignature(signature, token, timestamp, nonce)) {
try {
print = response.getWriter();
print.write(echostr);
print.flush();
logger.info("wechat auth success...");
} catch (IOException e) {
e.printStackTrace();
}
}else{
logger.info("wechat auth failure...");
}
}
把我的工具類也貼進來
/**
* WeChat token validate tools.
* @author
* @date 2018-09-01
*
* 1、獲取get請求引數:echostr, timestamp, nonce, signature
* 2、對token, timestamp, nonce進行排序
* 3、對排序後的資料進行加密,得到hashCode
* 4、hashCode與signature比較進行驗籤
* 5、驗籤成功返回:echostr, 失敗返回:""
*/
public class WeChatUtil {
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* Takes the raw bytes from the digest and formats them correct.
*
* @param bytes the raw bytes from the digest.
* @return the formatted bytes.
*/
private static String getFormattedText(byte[] bytes) {
int len = bytes.length;
StringBuilder buf = new StringBuilder(len * 2);
// 把密文轉換成十六進位制的字串形式
for (int j = 0; j < len; j++) {
buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
}
return buf.toString();
}
/**
*
* @param str 待加密資料
* @return
*/
public static String encode(String str) {
if (str == null) {
return null;
}
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(str.getBytes());
return getFormattedText(messageDigest.digest());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param token 自定義驗證token
* @param timestamp 時間戳
* @param nonce 隨機數
* @return
*
*/
public static String sort(String token, String timestamp, String nonce){
String[] arrTemp = {token, timestamp, nonce};
Arrays.sort(arrTemp);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < arrTemp.length; i++) {
sb.append(arrTemp[i]);
}
return sb.toString();
}
/**
*
* @param signature 簽名
* @param token 自定義驗證token
* @param timestamp 時間戳
* @param nonce 隨機字串
* @return
*/
public static boolean checkSignature(String signature, String token, String timestamp, String nonce) {
String sortedStr = sort(token, timestamp, nonce);
String encodeStr = encode(sortedStr);
if(signature.equals(encodeStr)){
return true;
}
return false;
}
}
因為我的服務配置以.do結尾,所以伺服器地址配置成https://xxx.xxx.xxx/projectname/wechat.do
3、微信訊息回覆
使用者給公眾號傳送的訊息,微信都會以HTTP POST 資料型別為xml的形式傳送到伺服器地址。上面的方法處理GET請求,接下來要處理POST請求。由於xml不好操作,我們需要兩個方法,分別將xml字串轉為Map和Map轉為xml字串。
/**
* XML轉Map
* @param strXML
* @return
*/
@SuppressWarnings("unchecked")
public Map<String, String> xmlToMap(String strXML) {
SAXReader reader = new SAXReader();
Document doc;
Map<String, String> map = new HashMap<String, String>();
try {
InputStream in = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
doc = reader.read(in);
Element root = doc.getRootElement();
List<Element> list = (List<Element>) root.elements();
for (Element element : list) {
map.put(element.getName(), element.getText());
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
}
return map;
}
/**
* XML轉Map
* @param strXML
* @return
*/
public String mapToXml2(Map<String, String> map) {
SAXReader reader = new SAXReader();
Document doc = null;
String xml = "<xml></xml>";
try {
InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8"));
doc = reader.read(in);
Element root = doc.getRootElement();
for (String key: map.keySet()) {
Element item = root.addElement(key);
item.addCDATA(map.get(key));
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return doc.asXML().toString();
}
處理POST請求:
@RequestMapping(value="/wechat", method=RequestMethod.POST, produces="application/xml;charset=UTF-8")
public void wechatPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
logger.info("wechat post request start...");
PrintWriter print = response.getWriter();
String reqStr = IOUtils.toString(request.getInputStream());
logger.info("解密請求訊息:");
try {
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, "cX8goXy3hpewENeykcU3wOjxqbDpQcaw2gLbb7DWXsP", "wxe43240c5a6750ef8");
String timeStamp = String.valueOf(System.currentTimeMillis()); // 時間戳
String nonce = DateUtil.format(new Date(), "yyyyMMddHHmmss"); // 隨機字串
print = response.getWriter();
String msgSignature = request.getParameter("msg_signature");
String timestamp = request.getParameter("timestamp");
String urlnonce = request.getParameter("nonce");
String openid = request.getParameter("openid");
String encrypt_type = request.getParameter("encrypt_type");
String decryptStr= pc.decryptMsg(msgSignature, timestamp, urlnonce, reqStr);
logger.info("解密後的請求訊息:" + decryptStr);
Map<String, String> req = xmlToMap(decryptStr);
Map<String, String> rsp = new HashMap<String, String>();
rsp.put("ToUserName", req.get("FromUserName"));
rsp.put("FromUserName", req.get("ToUserName"));
rsp.put("CreateTime", req.get("CreateTime"));
rsp.put("MsgType", "text");
Map replyMap = new HashMap();
replyMap.put("你好","你好啊");
replyMap.put("開玩笑","我認真的");
String replymsg = "";
if(replymsg != null && replyMap.containsKey(req.get("Content"))) {
replymsg = (String) replyMap.get(req.get("Content"));
}
rsp.put("Content", replymsg);
logger.info("加密前響應報文:\n" + mapToXml2(rsp));
String miwen = pc.encryptMsg(mapToXml2(rsp), timestamp, urlnonce);
print.write(miwen);
logger.info("加密後響應報文:\n" + miwen);
print.flush();
} catch (IOException e) {
e.printStackTrace();
} catch (AesException e) {
e.printStackTrace();
}
}
4、訊息加解密
微信公眾平臺採用AES對稱加密演算法對推送給公眾帳號的訊息體對行加密,EncodingAESKey則是加密所用的祕鑰。公眾帳號用此祕鑰對收到的密文訊息體進行解密,回覆訊息體也用此祕鑰加密。
訊息報文的三種模式:
- 明文模式:維持現有模式,沒有適配加解密新特性,訊息體明文收發,預設設定為明文模式
- 相容模式:公眾平臺傳送訊息內容將同時包括明文和密文,訊息包長度增加到原來的3倍左右;公眾號回覆明文或密文均可,不影響現有訊息收發;開發者可在此模式下進行除錯
- 安全模式(推薦):公眾平臺傳送訊息體的內容只含有密文,公眾賬號回覆的訊息體也為密文,建議開發者在除錯成功後使用此模式收發訊息
微信公眾平臺為開發者提供了5種語言的示例程式碼(包括C++、php、Java、Python和C#版本),請自行下載。
5、訊息報文
解密後的普通文字訊息
<xml>
<ToUserName><![CDATA[gh_1fc3764cbbdb]]></ToUserName>
<FromUserName><![CDATA[oFcn2jkbrUcqgt18Xe4jOZNHA8c0]]></FromUserName>
<CreateTime>1545013410</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
<MsgId>6635782068263705931</MsgId>
</xml>
當用戶關注公眾號時,微信會發送什麼給伺服器呢?嗯,這是一個事件訊息。
{CreateTime=1545007942, EventKey=, Event=subscribe, ToUserName=gh_1fc3764cbbdb, FromUserName=oFcn2jkbrUcqgt18Xe4jOZNHA8c0, MsgType=event}
以下: