Spring Boot 開發微信公眾號後臺

_江南一點雨發表於2020-01-29

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

很多小夥伴可能注意到鬆哥的個人網站(http://www.javaboy.org)前一陣子上線了一個公眾號內回覆口令解鎖網站文章的功能,還有之前就有的公眾號內回覆口令獲取超 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>

這些引數含義如下:

引數 描述
ToUserName 開發者微訊號
FromUserName 傳送方帳號(一個OpenID)
CreateTime 訊息建立時間 (整型)
MsgType 訊息型別,文字為text
Content 文字訊息內容
MsgId 訊息id,64位整型

看到這裡,大家心裡大概就有數了,當我們收到微信伺服器發來的訊息之後,我們就進行 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 包在伺服器上部署啟動。啟動成功之後,確認微信的後臺配置也沒問題,我們就可以在公眾號上發一條訊息了,這樣我們自己的服務端就會列印出來剛剛訊息的資訊。

好了,篇幅限制,今天就和大家先聊這麼多,後面再聊不同訊息型別的解析和訊息的返回問題。

不知道小夥伴們看懂沒?有問題歡迎留言討論。

參考資料:微信開放文件

關注公眾號【江南一點雨】,專注於 Spring Boot+微服務以及前後端分離等全棧技術,定期視訊教程分享,關注後回覆 Java ,領取鬆哥為你精心準備的 Java 乾貨!

相關文章