JAVA實現 springMVC方式的微信接入、實現消息自動回覆

前段時間小忙了一陣,微信公衆號的開發,從零開始看文檔,踩了很多坑,也算是熬過來了,最近考慮作一些總結,方便之後再開發的時候回顧,也給正在作相關項目的同窗作個參考。
  1. 思路

    微信接入:用戶消息和開發者須要的事件推送都會經過微信方服務器發起一個請求,轉發到你在公衆平臺配置的服務器url地址,微信方將帶上signature,timestamp,nonce,echostr四個參數,咱們本身服務器經過拼接公衆平臺配置的token,以及傳上來的timestamp,nonce進行SHA1加密後匹配signature,返回ture說明接入成功。

 

   消息回覆:當用戶給公衆號發送消息時,微信服務器會將用戶消息以xml格式經過POST請求到咱們配置好的服務器對應的接口,而咱們要作的事情就是根據消息類型等作相應的邏輯處理,並將最後的返回結果也經過xml格式return給微信服務器,微信方再傳達給用戶的這樣一個過程。 java

    

 

  1. 公衆平臺配置



  2. Controller

    複製代碼
    @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{
            // 將請求、響應的編碼均設置爲UTF-8(防止中文亂碼)  
            request.setCharacterEncoding("UTF-8");  //微信服務器POST消息時用的是UTF-8編碼,在接收時也要用一樣的編碼,不然中文會亂碼;
            response.setCharacterEncoding("UTF-8"); //在響應消息(回覆消息給用戶)時,也將編碼方式設置爲UTF-8,原理同上;boolean isGet = request.getMethod().toLowerCase().equals("get"); 
          
            PrintWriter out = response.getWriter();
             
            try {
                if (isGet) {
                    String signature = request.getParameter("signature");// 微信加密簽名  
                    String timestamp = request.getParameter("timestamp");// 時間戳  
                    String nonce = request.getParameter("nonce");// 隨機數  
                    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,調用了裏面的一個叫checkSignature,傳入了四個值,DNBX_TOKEN, signature, timestamp, nonce。這個過程很是重要,其實咱們能夠理解爲將微信傳過來的值進行一個加解密的過程,不少大型的項目全部的接口爲保證安全性都會有這樣一個驗證的過程。DNBX_TOKEN咱們在微信公衆平臺配置的一個token字符串,主意保密哦!其餘三個都是微信服務器發送get請求傳過來的參數,咱們進行一層sha1加密:
複製代碼
public class SignUtil {  
  
    /** 
     * 驗證簽名 
     * 
     * @param token 微信服務器token,在env.properties文件中配置的和在開發者中心配置的必須一致 
     * @param signature 微信服務器傳過來sha1加密的證書籤名
     * @param timestamp 時間戳
     * @param nonce 隨機數 
     * @return 
     */  
    public static boolean checkSignature(String token,String signature, String timestamp, String nonce) {  
        String[] arr = new String[] { token, timestamp, nonce };  
        // 將token、timestamp、nonce三個參數進行字典序排序  
        Arrays.sort(arr);  
        
        // 將三個參數字符串拼接成一個字符串進行sha1加密  
        String tmpStr = SHA1.encode(arr[0] + arr[1] + arr[2]);  
        
        // 將sha1加密後的字符串可與signature對比,標識該請求來源於微信  
        return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;  
    }  
    
}  
複製代碼

SHA1:算法

複製代碼
/** 
 * 微信公衆平臺(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);  
        // 把密文轉換成十六進制的字符串形式  
        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);  
        }  
    }  
}  
複製代碼

 

當你在公衆平臺提交保存,而且看到綠色的提示「接入成功」以後,恭喜你已經完成微信接入。這個過程須要細心一點,注意加密算法裏的大小寫,若是接入不成功,大多數狀況都是加密算法的問題,多檢查檢查。json

    

   4. 實現消息自動回覆service

複製代碼
/**
     * 處理微信發來的請求
     * 
     * @param request
     * @return
     */
    public String weixinPost(HttpServletRequest request) {
        String respMessage = null;
        try {

            // xml請求解析
            Map<String, String> requestMap = MessageUtil.xmlToMap(request);

            // 發送方賬號(open_id)
            String fromUserName = requestMap.get("FromUserName");
            // 公衆賬號
            String toUserName = requestMap.get("ToUserName");
            // 消息類型
            String msgType = requestMap.get("MsgType");
            // 消息內容
            String content = requestMap.get("Content");
            
            LOGGER.info("FromUserName is:" + fromUserName + ", ToUserName is:" + toUserName + ", MsgType is:" + msgType);

            // 文本消息
            if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
                //這裏根據關鍵字執行相應的邏輯,只有你想不到的,沒有作不到的
                if(content.equals("xxx")){
                    
                }
                
                //自動回覆
                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 = "歡迎關注xxx公衆號!";
                    return MessageResponse.getTextMessage(fromUserName , toUserName , respContent);
                } else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {// 自定義菜單點擊事件
                    String eventKey = requestMap.get("EventKey");// 事件KEY值,與建立自定義菜單時指定的KEY值對應
                    logger.info("eventKey is:" +eventKey);
                    return xxx;
                }
            }
            //開啓微信聲音識別測試 2015-3-30
            else if(msgType.equals("voice"))
            {
                String recvMessage = requestMap.get("Recognition");
                //respContent = "收到的語音解析結果:"+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("歡迎關注,xxx");
                    text.setToUserName(fromUserName);
                    text.setFromUserName(toUserName);
                    text.setCreateTime(new Date().getTime() + "");
                    text.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
                    
                    respMessage = MessageUtil.textMessageToXml(text);
                } 
                // TODO 取消訂閱後用戶再收不到公衆號發送的消息,所以不須要回復消息
                else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {// 取消訂閱
                    
                    
                } 
                // 自定義菜單點擊事件
                else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {
                    String eventKey = requestMap.get("EventKey");// 事件KEY值,與建立自定義菜單時指定的KEY值對應
                    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恰好相反,這也是坑之一,還記得我當時調了好久,明明都沒有問題就是不通,最後把這兩個一換消息就收到了!其實回過頭想也對,返回給微信服務器這時自己角色就變了,因此發送和接收方也確定是相反的。微信

 

    5.MessageUtil

    

複製代碼
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";  
}
複製代碼

 

這裏爲了程序可讀性、擴展性更好一點,我作了一些封裝,定義了幾個常量,以及將微信傳過來的一些參數封裝成java bean持久化對象,核心代碼如上。重點講下xml和map之間的轉換app

其實這個問題要歸咎於微信是用xml通信,而咱們平時通常是用json,因此可能短期內會有點不適應微信公衆平臺

1.引入jar包

複製代碼
<!-- 解析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轉map集合對象

複製代碼
/**
     * xml轉換爲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.文本消息對象轉換成xml 

複製代碼
/** 
     * 文本消息對象轉換成xml 
     *  
     * @param textMessage 文本消息對象 
     * @return xml 
     */ 
    public static String textMessageToXml(TextMessage textMessage){
        XStream xstream = new XStream();
        xstream.alias("xml", textMessage.getClass());
        return xstream.toXML(textMessage);
    }
複製代碼

 

到此爲止已經大功告成了,這個時候能夠在公衆號裏嘗試發送「測試」,你會收到微信回覆的「the text is 測試」,這也是上面代碼裏作的回覆處理,固然你也能夠發揮你的想象用他作全部你想作的事了,好比回覆1查天氣,2查違章等等....dom

相關文章
相關標籤/搜索