手把手教你開發微信公衆號後臺

鬆哥原創的 Spring Boot 視頻教程已經殺青,感興趣的小夥伴戳這裏-->Spring Boot+Vue+微人事視頻教程java



  • 1.緣起web

  • 2.實現思路spring

  • 3.公衆號後臺配置數據庫

  • 4.開發安全

    • 4.1 服務器有效性校驗服務器

    • 4.2 消息接收接口微信

  • 5.消息分類app

  • 6.返回消息類型定義微信公衆平臺

  • 7.返回消息生成編輯器

  • 8.返回消息分發


Hello 各位小夥伴,鬆哥今天要和你們聊一個有意思的話題,就是使用 Spring Boot 開發微信公衆號後臺。

不少小夥伴可能注意到鬆哥的我的網站(http://www.javaboy.org)前一陣子上線了一個公衆號內回覆口令解鎖網站文章的功能,還有以前就有的公衆號內回覆口令獲取超 2TB 免費視頻教程的功能(公號後臺回覆 2TB),這兩個都是鬆哥基於 Spring Boot 來作的,最近鬆哥打算經過一個系列的文章,來向小夥伴們介紹下如何經過 Spring Boot 來開發公衆號後臺。

1.緣起

今年 5 月份的時候,我想把我本身以前收集到的一些視頻教程分享給公衆號上的小夥伴,但是這些視頻教程大太了,沒法一次分享,單次分享分享連接立馬就失效了,爲了把這些視頻分享給你們,我把視頻拆分紅了不少份,而後設置了不一樣的口令,小夥伴們在公衆號後臺經過回覆口令就能夠獲取到這些視頻,口令前先後後有 100 多個,我一個一個手動的在微信後臺進行配置。這麼搞工做量很大,前先後後大概花了三個晚上才把這些東西搞定。

因而我就在想,該寫點代碼了。

上個月買了服務器,也備案了,該有的都有了,因而就打算把這些資源用代碼實現下,由於大學時候搞過公衆號開發,倒也沒什麼難度,因而說幹就幹。

2.實現思路

其實鬆哥這個回覆口令獲取視頻連接的實現原理很簡單,說白了,就是一個數據查詢操做而已,回覆的口令是查詢關鍵字,回覆的內容則是查詢結果。這個原理很簡單。

另外一方面你們須要明白微信公衆號後臺開發消息發送的一個流程,你們看下面這張圖:

這是你們在公衆號後臺回覆關鍵字的狀況。那麼這個消息是怎麼樣一個傳遞流程呢?咱們來看看下面這張圖:

這張圖,我給你們稍微解釋下:

  1. 首先 javaboy4096 這個字符從公衆號上發送到了微信服務器
  2. 接下來微信服務器會把 javaboy4096 轉發到我本身的服務器上
  3. 我收到 javaboy4096 這個字符以後,就去數據庫中查詢,將查詢的結果,按照騰訊要求的 XML 格式進行返回
  4. 微信服務器把從個人服務器收到的信息,再發回到微信上,因而小夥伴們就看到了返回結果了

大體的流程就是這個樣子。

接下來咱們就來看一下實現細節。

3.公衆號後臺配置

開發的第一步,是微信服務器要驗證咱們本身的服務器是否有效。

首先咱們登陸微信公衆平臺官網後,在公衆平臺官網的 「開發-基本設置」 頁面,勾選協議成爲開發者,而後點擊「修改配置」按鈕,填寫:

  • 服務器地址(URL)
  • Token
  • EncodingAESKey

這裏的 URL 配置好以後,咱們須要針對這個 URL 開發兩個接口,一個是 GET 請求的接口,這個接口用來作服務器有效性驗證,另外一個則是 POST 請求的接口,這個用來接收微信服務器發送來的消息。也就是說,微信服務器的消息都是經過 POST 請求發給個人。

Token 可由開發者能夠任意填寫,用做生成簽名(該 Token 會和接口 URL 中包含的 Token 進行比對,從而驗證安全性)。

EncodingAESKey 由開發者手動填寫或隨機生成,將用做消息體加解密密鑰。

同時,開發者可選擇消息加解密方式:明文模式、兼容模式和安全模式。明文模式就是咱們本身的服務器收到微信服務器發來的消息是明文字符串,直接就能夠讀取而且解析,安全模式則是咱們收到微信服務器發來的消息是加密的消息,須要咱們手動解析後才能使用。

4.開發

公衆號後臺配置完成後,接下來咱們就能夠寫代碼了。

4.1 服務器有效性校驗

咱們首先來建立一個普通的 Spring Boot 項目,建立時引入 spring-boot-starter-web 依賴,項目建立成功後,咱們建立一個 Controller ,添加以下接口:

@GetMapping("/verify_wx_token")
public void login(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
    request.setCharacterEncoding("UTF-8");
    String signature = request.getParameter("signature");
    String timestamp = request.getParameter("timestamp");
    String nonce = request.getParameter("nonce");
    String echostr = request.getParameter("echostr");
    PrintWriter out = null;
    try {
        out = response.getWriter();
        if (CheckUtil.checkSignature(signature, timestamp, nonce)) {
            out.write(echostr);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        out.close();
    }
}

關於這段代碼,我作以下解釋:

  1. 首先經過 request.getParameter 方法獲取到微信服務器發來的 signature、timestamp、nonce 以及 echostr 四個參數,這四個參數中:signature 表示微信加密簽名,signature 結合了開發者填寫的 token 參數和請求中的timestamp參數、nonce參數;timestamp 表示時間戳;nonce 表示隨機數;echostr 則表示一個隨機字符串。
  2. 開發者經過檢驗 signature 對請求進行校驗,若是確認這次 GET 請求來自微信服務器,則原樣返回 echostr 參數內容,則接入生效,成爲開發者成功,不然接入失敗。
  3. 具體的校驗就是鬆哥這裏的 CheckUtil.checkSignature 方法,在這個方法中,首先將token、timestamp、nonce 三個參數進行字典序排序,而後將三個參數字符串拼接成一個字符串進行 sha1 加密,最後開發者得到加密後的字符串可與 signature 對比,標識該請求來源於微信。

校驗代碼以下:

public class CheckUtil {
    private static final String token = "123456";
    public static boolean checkSignature(String signature, String timestamp, String nonce) {
        String[] str = new String[]{token, timestamp, nonce};
        //排序
        Arrays.sort(str);
        //拼接字符串
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < str.length; i++) {
            buffer.append(str[i]);
        }
        //進行sha1加密
        String temp = SHA1.encode(buffer.toString());
        //與微信提供的signature進行匹對
        return signature.equals(temp);
    }
}
public class SHA1 {
    private static final char[] HEX_DIGITS = {'0''1''2''3''4''5',
            '6''7''8''9''a''b''c''d''e''f'};
    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);
        }
    }
}

OK,完成以後,咱們的校驗接口就算是開發完成了。接下來就能夠開發消息接收接口了。

4.2 消息接收接口

接下來咱們來開發消息接收接口,消息接收接口和上面的服務器校驗接口地址是同樣的,都是咱們一開始在公衆號後臺配置的地址。只不過消息接收接口是一個 POST 請求。

我在公衆號後臺配置的時候,消息加解密方式選擇了明文模式,這樣我在後臺收到的消息直接就能夠處理了。微信服務器給我發來的普通文本消息格式以下:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[this is a test]]></Content>
  <MsgId>1234567890123456</MsgId>
</xml>

這些參數含義以下:

看到這裏,你們內心大概就有數了,當咱們收到微信服務器發來的消息以後,咱們就進行 XML 解析,提取出來咱們須要的信息,去作相關的查詢操做,再將查到的結果返回給微信服務器。

這裏咱們先來個簡單的,咱們將收到的消息解析並打印出來:

@PostMapping("/verify_wx_token")
public void handler(HttpServletRequest request, HttpServletResponse response) throws Exception {
    request.setCharacterEncoding("UTF-8");
    response.setCharacterEncoding("UTF-8");
    PrintWriter out = response.getWriter();
    Map<String, String> parseXml = MessageUtil.parseXml(request);
    String msgType = parseXml.get("MsgType");
    String content = parseXml.get("Content");
    String fromusername = parseXml.get("FromUserName");
    String tousername = parseXml.get("ToUserName");
    System.out.println(msgType);
    System.out.println(content);
    System.out.println(fromusername);
    System.out.println(tousername);
}
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
    Map<String, String> map = new HashMap<String, String>();
    InputStream inputStream = request.getInputStream();
    SAXReader reader = new SAXReader();
    Document document = reader.read(inputStream);
    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;
}

你們看到其實都是一些常規代碼,沒有什麼難度。

作完這些以後,咱們將項目打成 jar 包在服務器上部署啓動。啓動成功以後,確認微信的後臺配置也沒問題,咱們就能夠在公衆號上發一條消息了,這樣咱們本身的服務端就會打印出來剛剛消息的信息。

5.消息分類

在討論如何給微信服務器回覆消息以前,咱們須要先來了解下微信服務器發來的消息主要有哪些類型以及咱們回覆給微信的消息都有哪些類型。

在上文中你們瞭解到,微信發送來的 xml 消息中有一個 MsgType 字段,這個字段就是用來標記消息的類型。這個類型能夠標記出這條消息是普通消息仍是事件消息仍是圖文消息等。

普通消息主要是指:

  • 文本消息
  • 圖片消息
  • 語音消息
  • 視頻消息
  • 小視頻消息
  • 地址位置消息
  • 連接消息

不一樣的消息類型,對應不一樣的 MsgType,這裏我仍是以普通消息爲例,以下:

消息類型 MsgType
文本消息 text
圖片消息 image
語音消息 voice
視頻消息 video
小視頻消息 shortvideo
地址位置消息 location
連接消息 link

你們千萬不要覺得不一樣類型消息的格式是同樣的,實際上是不同的,也就是說,MsgType 爲 text 的消息和 MsgType 爲 image 的消息,微信服務器發給咱們的消息內容是不同的,這樣帶來一個問題就是我沒法使用一個 Bean 去接收不一樣類型的數據,所以這裏咱們通常使用 Map 接收便可。

這是消息的接收,除了消息的接收以外,還有一個消息的回覆,咱們回覆的消息也有不少類型,能夠回覆普通消息,也能夠回覆圖片消息,回覆語音消息等,不一樣的回覆消息咱們能夠進行相應的封裝。由於不一樣的返回消息實例也是有一些共同的屬性的,例如消息是誰發來的,發給誰,消息類型,消息 id 等,因此咱們能夠將這些共同的屬性定義成一個父類,而後不一樣的消息再去繼承這個父類。

6.返回消息類型定義

首先咱們來定義一個公共的消息類型:

public class BaseMessage {
    private String ToUserName;
    private String FromUserName;
    private long CreateTime;
    private String MsgType;
    private long MsgId;
    //省略 getter/setter
}

在這裏:

  • ToUserName 表示開發者的微信號
  • FromUserName 表示發送方帳號(用戶的 OpenID)
  • CreateTime 消息的建立時間
  • MsgType 表示消息的類型
  • MsgId 表示消息 id

這是咱們的基本消息類型,就是說,咱們返回給用戶的消息,不管是什麼類型的消息,都有這幾個基本屬性。而後在此基礎上,咱們再去擴展出文本消息、圖片消息 等。

咱們來看下文本消息的定義:

public class TextMessage extends BaseMessage {
    private String Content;
    //省略 getter/setter
}

文本消息在前面消息的基礎上多了一個 Content 屬性,所以文本消息繼承自 BaseMessage ,再額外添加一個 Content 屬性便可。

其餘的消息類型也是相似的定義,我就不一一列舉了,至於其餘消息的格式,你們能夠參考微信開放文檔(http://1t.click/aPXK)。

7.返回消息生成

消息類型的 Bean 定義完成以後,接下來就是將實體類生成 XML。

首先咱們定義一個消息工具類,將常見的消息類型枚舉出來:

/**
 * 返回消息類型:文本
 */

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 RESP_MESSAGE_TYPE_Image = "image";
/**
 * 返回消息類型:語音
 */

public static final String RESP_MESSAGE_TYPE_Voice = "voice";
/**
 * 返回消息類型:視頻
 */

public static final String RESP_MESSAGE_TYPE_Video = "video";
/**
 * 請求消息類型:文本
 */

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_VIDEO = "video";
/**
 * 請求消息類型:推送
 */

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";
/**
 * 事件類型:VIEW(自定義菜單 URl 視圖)
 */

public static final String EVENT_TYPE_VIEW = "VIEW";
/**
 * 事件類型:LOCATION(上報地理位置事件)
 */

public static final String EVENT_TYPE_LOCATION = "LOCATION";
/**
 * 事件類型:LOCATION(上報地理位置事件)
 */

public static final String EVENT_TYPE_SCAN = "SCAN";

你們注意這裏消息類型的定義,以 RESP 開頭的表示返回的消息類型,以 REQ 表示微信服務器發來的消息類型。而後在這個工具類中再定義兩個方法,用來將返回的對象轉換成 XML:

public static String textMessageToXml(TextMessage textMessage) {
    xstream.alias("xml", textMessage.getClass());
    return xstream.toXML(textMessage);
}
private static XStream xstream = new XStream(new XppDriver() {
    public HierarchicalStreamWriter createWriter(Writer out) {
        return new PrettyPrintWriter(out) {
            boolean cdata = true;
            @SuppressWarnings("rawtypes")
            public void startNode(String name, Class clazz) {
                super.startNode(name, clazz);
            }
            protected void writeText(QuickWriter writer, String text) {
                if (cdata) {
                    writer.write("<![CDATA[");
                    writer.write(text);
                    writer.write("]]>");
                } else {
                    writer.write(text);
                }
            }
        };
    }
});

textMessageToXML 方法用來將 TextMessage 對象轉成 XML 返回給微信服務器,相似的方法咱們還須要定義 imageMessageToXml、voiceMessageToXml 等,不過定義的方式都基本相似,我就不一一列出來了。

8.返回消息分發

因爲用戶發來的消息可能存在多種狀況,咱們須要分類進行處理,這個就涉及到返回消息的分發問題。所以我在這裏再定義一個返回消息分發的工具類,以下:

public class MessageDispatcher {
    public static String processMessage(Map<String, String> map) {
        String openid = map.get("FromUserName"); //用戶 openid
        String mpid = map.get("ToUserName");   //公衆號原始 ID
        if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { 
            //普通文本消息
            TextMessage txtmsg = new TextMessage();
            txtmsg.setToUserName(openid);
            txtmsg.setFromUserName(mpid);
            txtmsg.setCreateTime(new Date().getTime());
            txtmsg.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
            txtmsg.setContent("這是返回消息");
            return MessageUtil.textMessageToXml(txtmsg);
        }
        return null;
    }
    public String processEvent(Map<String, String> map) {
        //在這裏處理事件
    }
}

這裏咱們還能夠多加幾個 elseif 去判斷不一樣的消息類型,我這裏由於只有普通文本消息,因此一個 if 就夠用了。

在這裏返回值我寫死了,實際上這裏須要根據微信服務端傳來的 Content 去數據中查詢,將查詢結果返回,數據庫查詢這一套相信你們都能搞定,我這裏就不重複介紹了。

最後在消息接收 Controller 中調用該方法,以下:

@PostMapping(value = "/verify_wx_token",produces = "application/xml;charset=utf-8")
public String handler(HttpServletRequest request, HttpServletResponse response) throws Exception {
    request.setCharacterEncoding("UTF-8");
    Map<String, String> map = MessageUtil.parseXml(request);
    String msgType = map.get("MsgType");
    if (MessageUtil.REQ_MESSAGE_TYPE_EVENT.equals(msgType)) {
        return messageDispatcher.processEvent(map);
    }else{
        return messageDispatcher.processMessage(map);
    }
}

在 Controller 中,咱們首先判斷消息是不是事件,若是是事件,進入到事件處理通道,若是不是事件,則進入到消息處理通道。

「注意,這裏須要配置一下返回消息的編碼,不然可能會出現中文亂碼。」

如此以後,咱們的服務器就能夠給公衆號返回消息了。

好了,本文咱們就先說到這裏,感興趣的小夥伴不妨試試。

本文分享自微信公衆號 - 江南一點雨(a_javaboy)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索