前段時間小忙了一陣,微信公眾號的開發(fā),從零開始看文檔,踩了不少坑,也算是熬過來了,最近考慮做一些總結(jié),方便以后再開發(fā)的時候回顧,也給正在做相關(guān)項目的同學(xué)做個參考。
1.思路
微信接入:用戶消息和開發(fā)者需要的事件推送都會通過微信方服務(wù)器發(fā)起一個請求,轉(zhuǎn)發(fā)到你在公眾平臺配置的服務(wù)器url地址,微信方將帶上signature,timestamp,nonce,echostr四個參數(shù),我們自己服務(wù)器通過拼接公眾平臺配置的token,以及傳上來的timestamp,nonce進行SHA1加密后匹配signature,返回ture說明接入成功。
消息回復(fù):當用戶給公眾號發(fā)送消息時,微信服務(wù)器會將用戶消息以xml格式通過POST請求到我們配置好的服務(wù)器對應(yīng)的接口,而我們要做的事情就是根據(jù)消息類型等做相應(yīng)的邏輯處理,并將最后的返回結(jié)果也通過xml格式return給微信服務(wù)器,微信方再傳達給用戶的這樣一個過程。
1.公眾平臺配置
2.Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
@Controller @RequestMapping ( "/wechat" ) publicclass WechatController { @Value ( "${DNBX_TOKEN}" ) private String DNBX_TOKEN; private static final Logger LOGGER = LoggerFactory.getLogger(WechatController. class ); @Resource WechatService wechatService; /** * 微信接入 * @param wc * @return * @throws IOException */ @RequestMapping (value= "/connect" ,method = {RequestMethod.GET, RequestMethod.POST}) @ResponseBody publicvoid connectWeixin(HttpServletRequest request, HttpServletResponse response) throws IOException{ // 將請求、響應(yīng)的編碼均設(shè)置為UTF-8(防止中文亂碼) request.setCharacterEncoding( "UTF-8" ); //微信服務(wù)器POST消息時用的是UTF-8編碼,在接收時也要用同樣的編碼,否則中文會亂碼; response.setCharacterEncoding( "UTF-8" ); //在響應(yīng)消息(回復(fù)消息給用戶)時,也將編碼方式設(shè)置為UTF-8,原理同上;boolean isGet = request.getMethod().toLowerCase().equals("get"); PrintWriter out = response.getWriter(); try { if (isGet) { String timestamp = request.getParameter( "timestamp" ); // 時間戳 String nonce = request.getParameter( "nonce" ); // 隨機數(shù) String echostr = request.getParameter( "echostr" ); //隨機字符串 // 通過檢驗signature對請求進行校驗,若校驗成功則原樣返回echostr,表示接入成功,否則接入失敗 if (SignUtil.checkSignature(DNBX_TOKEN, signature, timestamp, nonce)) { LOGGER.info( "Connect the weixin server is successful." ); response.getWriter().write(echostr); } else { LOGGER.error( "Failed to verify the signature!" ); } } else { String respMessage = "異常消息!" ; try { respMessage = wechatService.weixinPost(request); out.write(respMessage); LOGGER.info( "The request completed successfully" ); LOGGER.info( "to weixin server " +respMessage); } catch (Exception e) { LOGGER.error( "Failed to convert the message from weixin!" ); } } } catch (Exception e) { LOGGER.error( "Connect the weixin server is error." ); } finally { out.close(); } } } |
3.簽名驗證 checkSignature
從上面的controller我們可以看到,我封裝了一個工具類SignUtil,調(diào)用了里面的一個叫checkSignature,傳入了四個值,DNBX_TOKEN, signature, timestamp, nonce。這個過程非常重要,其實我們可以理解為將微信傳過來的值進行一個加解密的過程,很多大型的項目所有的接口為保證安全性都會有這樣一個驗證的過程。DNBX_TOKEN我們在微信公眾平臺配置的一個token字符串,主意保密哦!其他三個都是微信服務(wù)器發(fā)送get請求傳過來的參數(shù),我們進行一層sha1加密:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class SignUtil { /** * 驗證簽名 * * @param token 微信服務(wù)器token,在env.properties文件中配置的和在開發(fā)者中心配置的必須一致 * @param signature 微信服務(wù)器傳過來sha1加密的證書簽名 * @param timestamp 時間戳 * @param nonce 隨機數(shù) * @return */ public static boolean checkSignature(String token,String signature, String timestamp, String nonce) { String[] arr = new String[] { token, timestamp, nonce }; // 將token、timestamp、nonce三個參數(shù)進行字典序排序 Arrays.sort(arr); // 將三個參數(shù)字符串拼接成一個字符串進行sha1加密 String tmpStr = SHA1.encode(arr[ 0 ] + arr[ 1 ] + arr[ 2 ]); // 將sha1加密后的字符串可與signature對比,標識該請求來源于微信 return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false ; } } |
SHA1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
/** * 微信公眾平臺(JAVA) SDK * * SHA1算法 * * @author helijun 2016/06/15 19:49 */ public final class SHA1 { 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 ); // 把密文轉(zhuǎn)換成十六進制的字符串形式 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(); } 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); } } } |
當你在公眾平臺提交保存,并且看到綠色的提示“接入成功”之后,恭喜你已經(jīng)完成微信接入。這個過程需要細心一點,注意加密算法里的大小寫,如果接入不成功,大多數(shù)情況都是加密算法的問題,多檢查檢查。
4. 實現(xiàn)消息自動回復(fù)service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
/** * 處理微信發(fā)來的請求 * * @param request * @return */ public String weixinPost(HttpServletRequest request) { String respMessage = null ; try { // xml請求解析 Map<String, String> requestMap = MessageUtil.xmlToMap(request); // 發(fā)送方帳號(open_id) String fromUserName = requestMap.get( "FromUserName" ); // 公眾帳號 String toUserName = requestMap.get( "ToUserName" ); // 消息類型 String msgType = requestMap.get( "MsgType" ); // 消息內(nèi)容 String content = requestMap.get( "Content" ); LOGGER.info( "FromUserName is:" + fromUserName + ", ToUserName is:" + toUserName + ", MsgType is:" + msgType); // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { //這里根據(jù)關(guān)鍵字執(zhí)行相應(yīng)的邏輯,只有你想不到的,沒有做不到的 if (content.equals( "xxx" )){ } //自動回復(fù) TextMessage text = new TextMessage(); text.setContent( "the text is" + content); text.setToUserName(fromUserName); text.setFromUserName(toUserName); text.setCreateTime( new Date().getTime() + "" ); text.setMsgType(msgType); respMessage = MessageUtil.textMessageToXml(text); } /*else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {// 事件推送 String eventType = requestMap.get("Event");// 事件類型 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {// 訂閱 respContent = "歡迎關(guān)注xxx公眾號!"; return MessageResponse.getTextMessage(fromUserName , toUserName , respContent); } else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {// 自定義菜單點擊事件 String eventKey = requestMap.get("EventKey");// 事件KEY值,與創(chuàng)建自定義菜單時指定的KEY值對應(yīng) logger.info("eventKey is:" +eventKey); return xxx; } } //開啟微信聲音識別測試 2015-3-30 else if(msgType.equals("voice")) { String recvMessage = requestMap.get("Recognition"); //respContent = "收到的語音解析結(jié)果:"+recvMessage; if(recvMessage!=null){ respContent = TulingApiProcess.getTulingResult(recvMessage); }else{ respContent = "您說的太模糊了,能不能重新說下呢?"; } return MessageResponse.getTextMessage(fromUserName , toUserName , respContent); } //拍照功能 else if(msgType.equals("pic_sysphoto")) { } else { return MessageResponse.getTextMessage(fromUserName , toUserName , "返回為空"); }*/ // 事件推送 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) { String eventType = requestMap.get( "Event" ); // 事件類型 // 訂閱 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { TextMessage text = new TextMessage(); text.setContent( "歡迎關(guān)注,xxx" ); text.setToUserName(fromUserName); text.setFromUserName(toUserName); text.setCreateTime( new Date().getTime() + "" ); text.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); respMessage = MessageUtil.textMessageToXml(text); } // TODO 取消訂閱后用戶再收不到公眾號發(fā)送的消息,因此不需要回復(fù)消息 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) { // 取消訂閱 } // 自定義菜單點擊事件 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) { String eventKey = requestMap.get( "EventKey" ); // 事件KEY值,與創(chuàng)建自定義菜單時指定的KEY值對應(yīng) if (eventKey.equals( "customer_telephone" )) { TextMessage text = new TextMessage(); text.setContent( "0755-86671980" ); text.setToUserName(fromUserName); text.setFromUserName(toUserName); text.setCreateTime( new Date().getTime() + "" ); text.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); respMessage = MessageUtil.textMessageToXml(text); } } } } catch (Exception e) { Logger.error( "error......" ) } return respMessage; } |
先貼代碼如上,大多都有注釋,讀一遍基本語義也懂了不需要多解釋。
有一個地方格外需要注意:
上面標紅的fromUserName和toUserName剛好相反,這也是坑之一,還記得我當時調(diào)了很久,明明都沒有問題就是不通,最后把這兩個一換消息就收到了!其實回過頭想也對,返回給微信服務(wù)器這時本身角色就變了,所以發(fā)送和接收方也肯定是相反的。
5.MessageUtil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
public class MessageUtil { /** * 返回消息類型:文本 */ public static final String RESP_MESSAGE_TYPE_TEXT = "text" ; /** * 返回消息類型:音樂 */ public static final String RESP_MESSAGE_TYPE_MUSIC = "music" ; /** * 返回消息類型:圖文 */ public static final String RESP_MESSAGE_TYPE_NEWS = "news" ; /** * 請求消息類型:文本 */ public static final String REQ_MESSAGE_TYPE_TEXT = "text" ; /** * 請求消息類型:圖片 */ public static final String REQ_MESSAGE_TYPE_IMAGE = "image" ; /** * 請求消息類型:鏈接 */ public static final String REQ_MESSAGE_TYPE_LINK = "link" ; /** * 請求消息類型:地理位置 */ public static final String REQ_MESSAGE_TYPE_LOCATION = "location" ; /** * 請求消息類型:音頻 */ public static final String REQ_MESSAGE_TYPE_VOICE = "voice" ; /** * 請求消息類型:推送 */ public static final String REQ_MESSAGE_TYPE_EVENT = "event" ; /** * 事件類型:subscribe(訂閱) */ public static final String EVENT_TYPE_SUBSCRIBE = "subscribe" ; /** * 事件類型:unsubscribe(取消訂閱) */ public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe" ; /** * 事件類型:CLICK(自定義菜單點擊事件) */ public static final String EVENT_TYPE_CLICK = "CLICK" ; } |
這里為了程序可讀性、擴展性更好一點,我做了一些封裝,定義了幾個常量,以及將微信傳過來的一些參數(shù)封裝成java bean持久化對象,核心代碼如上。重點講下xml和map之間的轉(zhuǎn)換
其實這個問題要歸咎于微信是用xml通訊,而我們平時一般是用json,所以可能短時間內(nèi)會有點不適應(yīng)
1.引入jar包
1
2
3
4
5
6
7
8
9
10
11
12
|
<!-- 解析xml --> < dependency > < groupId >dom4j</ groupId > < artifactId >dom4j</ artifactId > < version >1.6.1</ version > </ dependency > < dependency > < groupId >com.thoughtworks.xstream</ groupId > < artifactId >xstream</ artifactId > < version >1.4.9</ version > </ dependency > |
2.xml轉(zhuǎn)map集合對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
/** * xml轉(zhuǎn)換為map * @param request * @return * @throws IOException */ @SuppressWarnings ( "unchecked" ) public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException{ Map<String, String> map = new HashMap<String, String>(); SAXReader reader = new SAXReader(); InputStream ins = null ; try { ins = request.getInputStream(); } catch (IOException e1) { e1.printStackTrace(); } Document doc = null ; try { doc = reader.read(ins); Element root = doc.getRootElement(); List<Element> list = root.elements(); for (Element e : list) { map.put(e.getName(), e.getText()); } return map; } catch (DocumentException e1) { e1.printStackTrace(); } finally { ins.close(); } return null ; } |
3.文本消息對象轉(zhuǎn)換成xml
1
2
3
4
5
6
7
8
9
10
11
|
/** * 文本消息對象轉(zhuǎn)換成xml * * @param textMessage 文本消息對象 * @return xml */ public static String textMessageToXml(TextMessage textMessage){ XStream xstream = new XStream(); xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } |
到此為止已經(jīng)大功告成了,這個時候可以在公眾號里嘗試發(fā)送“測試”,你會收到微信回復(fù)的“the text is 測試”,這也是上面代碼里做的回復(fù)處理,當然你也可以發(fā)揮你的想象用他做所有你想做的事了,比如回復(fù)1查天氣,2查違章等等....
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持服務(wù)器之家。
原文鏈接:http://www.cnblogs.com/liliangel/p/6036627.html