1. 程式人生 > >微信公眾號開發步驟

微信公眾號開發步驟

微信公眾號主要有以下幾個步驟

微信公眾號的通訊機制

image.png

微信公眾號簡介

微信公眾號分為服務號、訂閱號、企業號,訂閱號可以個人申請,服務號和企業號要有企業資質才可以。

我們所說的微信公眾號開發指的是訂閱號和服務號。關於訂閱號和伺服器的區別,官方是這樣解釋的

  • 服務號:主要偏向於服務互動(功能類似12315,114,銀行,提供繫結資訊,服務互動),每月可群發4條訊息;服務號適用人群:**媒體、企業、政府或其他組織。

  • 訂閱號:主要偏向於為使用者傳達資訊,(功能類似報紙雜誌,為使用者提供新聞資訊或娛樂趣事),每天可群發1條訊息;訂閱號適用人群**:個人、媒體、企業、政府或其他組織。

1.註冊微信公眾號

進入微信公眾號註冊頁面https://mp.weixin.qq.com/點選公眾號右上方的註冊按鈕,進入註冊介面,填寫基本資訊,選擇訂閱號, 完成身份認證, 此處我選擇的是個人訂閱號,如下完善即可:

image.png

image.png

然後註冊成功之後進入微信公眾平臺後臺,然後完善微訊號名稱和微訊號ID:

image.png

image.png

微訊號名稱預設是新註冊公眾號,還需要修改微訊號名稱, 修改的時候需要經過微信認證,並且稽核通過之後才可以使用該公眾號.

image.png

2.註冊測試公眾號

個人訂閱號有一些介面是沒有許可權的,也就是說個人訂閱號無法呼叫一些高階的許可權介面,如生成二維碼、網頁授權、自定義選單、微信支付這樣的介面許可權個人訂閱號是沒有呼叫許可權的, 幸運的是,微信公眾平臺提供了測試公眾賬號,測試公眾號有很多個人訂閱號不具備的許可權, 測試公眾號的註冊地址為:

http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login

用微信掃描頁面中的二維碼進行登入,登入成功後,就可以看到騰訊分配給我們的測試公眾號的資訊了,如下圖所示, 接下來我們就可以搭建環境,進行開發測試了

image.png

測試公眾號的所擁有的介面許可權如下:

image.png
image.png
image.png
image.png

3.搭建微信本地除錯環境

開發基於微信公眾號的應用最大的痛苦之處就是除錯問題,每次實現一個功能後都需要部署到一個公網伺服器進行測試,因為微信使用者每次向公眾號發起請求時,微信伺服器會先接收到使用者的請求,然後再轉發到我們的伺服器上,也就是說,微信伺服器是要和我們的伺服器進行網路互動,所以我們必須保證我們的伺服器外網可以訪問到,這種部署到公網伺服器進行測試的做法對於我們開發者來說簡直是噩夢。所以我們要想一個辦法可以做到本地部署,本地除錯程式碼,而要做到這一點,那麼我們要解決的問題就是將內網的部署伺服器對映到外網,讓微信伺服器可以正常訪問到,幸運的是,藉助於第三方軟體Ngrok,我們就可以做得到。Ngrok是一個免費的軟體Ngrok,使用Ngrok後,我們就可以實現內網穿透,也就是說我們可以將內網的伺服器對映到外網給別人訪問

,這對於我們在本地開發環境中除錯微信程式碼是以及給使用者演示一些東西非常快速和有幫助的,因為可以直接使用我們自己的內網的電腦作為伺服器。不過需要翻牆訪問.

國內提供Ngrok服務比較好的網站是:http://natapp.cn/,如下圖所示:

image.png

1)下載客戶端natapp:

image.png

2)安裝natapp:

  • 解壓縮到目錄D:\Program Files\natapp

  • 直接雙擊開啟失敗,需要配置環境變數,編輯環境變數Path,新建natapp目錄

    image.png

  • 開啟cmd, 執行命令 natapp, 顯示認證錯誤

    image.png

  • 這個時候是需要token認證的, 所以我們的主要工作就是如何獲得authtoken

    進入https://natapp.cn/,根據提示註冊並建立免費隧道, 註冊的時候需要用支付寶實名認證的手機號註冊

    根據《中華人民共和國網路安全法》,以及防止隧道被非法使用,Natapp實行實名認證制度.
    本站承諾身份資訊僅用於身份驗證識別,不做任何其他用途,且嚴格加密儲存杜絕洩漏風險
    本實名認證系統基於阿里大資料的強個人識別驗證.手機,身份證資訊須匹配,比如手機號是你的支付寶實名認證的手機號,日常正常使用的.如其他小號可能無法通過驗證
    目前建立免費隧道強制要求實名認證.付費隧道可通過支付寶支付來實名認證

    image.png

    image.png

  • 複製authtoken, cmd進入natapp目錄執行 natapp -authtoken yourauthtoken 出現下圖即為成功

    image.png

  • 此時外網的使用者可以直接使用http://rzxjzk.natappfree.cc這個域名訪問到我內網的127.0.0.1:8080伺服器了,如下圖所示:

    image.png

    image.png

    使用了ngrok之後,我們就可以把內網的伺服器當成公網伺服器來使用了.訪問的速度也還在可以接受的範圍內吧,截止到目前為止ngrok是可用的,微信公眾號伺服器是可以訪問的,這樣一來也就不妨礙我們做本地調式了。到此,我們的微信本地除錯開發環境就算是搭建好了。

4.微信公眾號接入(校驗簽名)

微信公眾平臺開發者文件上,關於公眾號接入這一節內容在接入指南上寫的比較詳細的,文件中說接入公眾號需要3個步驟,分別是:

  1、填寫伺服器配置
  2、驗證伺服器地址的有效性
  3、依據介面文件實現業務邏輯

其實,第3步已經不能算做公眾號接入的步驟,而是接入之後,開發人員可以根據微信公眾號提供的介面所能做的一些開發。

第1步中伺服器配置包含伺服器地址(URL)、令牌(Token) 和 訊息加解密金鑰(EncodingAESKey)。

​ 可在開發–>基本配置–>伺服器配置中配置

​ 伺服器地址即公眾號後臺提供業務邏輯的入口地址,目前只支援80埠,之後包括接入驗證以及任何其它的操作的請求(例如訊息的傳送、選單管理、素材管理等)都要從這個地址進入。接入驗證和其它請求的區別就是,接入驗證時是get請求,其它時候是post請求;

  Token可由開發者可以任意填寫,用作生成簽名(該Token會和介面URL中包含的Token進行比對,從而驗證安全性);

  EncodingAESKey由開發者手動填寫或隨機生成,將用作訊息體加解密金鑰。本例中全部以未加密的明文訊息方式,不涉及此配置項。

第2步,驗證伺服器地址的有效性,當點選“提交”按鈕後,微信伺服器將傳送一個http的get請求到剛剛填寫的伺服器地址,並且攜帶四個引數:

image.png

接到請求後,我們需要做如下三步,若確認此次GET請求來自微信伺服器,原樣返回echostr引數內容,則接入生效,否則接入失敗。

    1. 將token、timestamp、nonce三個引數進行字典序排序
    2. 將三個引數字串拼接成一個字串進行sha1加密 (可逆加密解密函式)
    3. 開發者獲得加密後的字串可與signature對比,標識該請求來源於微信

  下面我們用Java程式碼來演示一下這個驗證過程

 使用IDE(Eclipse或者IntelliJ IDEA)建立一個JavaWeb專案,新建servlet weChatAccounts,程式碼如下:

package weChatServlet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;

public class weChatAccounts extends HttpServlet {
    static Logger logger = LoggerFactory.getLogger(weChatAccounts.class);

    /*
    * 自定義token, 用作生成簽名,從而驗證安全性
    * */
    private final String TOKEN = "cherry";

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("-----開始校驗簽名-----");

        /**
         * 接收微信伺服器傳送請求時傳遞過來的引數
         */
        String signature = req.getParameter("signature");
        String timestamp = req.getParameter("timestamp");
        String nonce = req.getParameter("nonce"); //隨機數
        String echostr = req.getParameter("echostr");//隨機字串

        /**
         * 將token、timestamp、nonce三個引數進行字典序排序
         * 並拼接為一個字串
         */
        String sortStr = sort(TOKEN,timestamp,nonce);
        /**
         * 字串進行shal加密
         */
        String mySignature = shal(sortStr);
        /**
         * 校驗微信伺服器傳遞過來的簽名 和  加密後的字串是否一致, 若一致則簽名通過
         */
        if(!"".equals(signature) && !"".equals(mySignature) && signature.equals(mySignature)){
            System.out.println("-----簽名校驗通過-----");
            resp.getWriter().write(echostr);
        }else {
            System.out.println("-----校驗簽名失敗-----");
        }
    }

    /**
     * 引數排序
     * @param token
     * @param timestamp
     * @param nonce
     * @return
     */
    public String sort(String token, String timestamp, String nonce) {
        String[] strArray = {token, timestamp, nonce};
        Arrays.sort(strArray);
        StringBuilder sb = new StringBuilder();
        for (String str : strArray) {
            sb.append(str);
        }
        return sb.toString();
    }

    /**
     * 字串進行shal加密
     * @param str
     * @return
     */
    public String shal(String str){
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            digest.update(str.getBytes());
            byte messageDigest[] = digest.digest();

            StringBuffer hexString = new StringBuffer();
            // 位元組陣列轉換為 十六進位制 數
            for (int i = 0; i < messageDigest.length; i++) {
                String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexString.append(0);
                }
                hexString.append(shaHex);
            }
            return hexString.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }
}

在web.xml中配置 servlet:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
          http://java.sun.com/xml/ns/javaee/web-app_3_1.xsd"
           version="3.1">
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>weChatServlet</servlet-name>
        <servlet-class>weChatServlet.weChatAccounts</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>weChatServlet</servlet-name>
        <url-pattern>/weChatServlet</url-pattern> <!--url-pattern必須與servlet-name一致-->
    </servlet-mapping>
</web-app>

然後在index.jsp中寫hello world 測試:

<%-- Created by IntelliJ IDEA. --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
  <head>
    <title></title>
  </head>
  <body>
      <h3>微信公眾號測試!</h3>
  </body>
</html>

啟動專案結果如下:

image.png

然後啟動natapp,進行內網透傳 natapp -authtoken mytoken

image.png

根據動態生成的ip地址訪問.,得到如下效果,則表示外網可以成功訪問:

image.png

進入微信測試公眾號管理介面,在介面配置資訊中填入對映的外網地址和程式碼中宣告的token,如下圖所示:

點選提交,會顯示配置成功, 控制檯就會列印資訊, 顯示簽名校驗通過.

注意: URL是 外網的ip地址加上 web.xml中配置的servlet名稱

image.png

image.png

到此,我們的公眾號應用已經能夠和微信伺服器正常通訊了,也就是說我們的公眾號已經接入到微信公眾平臺了。

5.access_token管理

我們的公眾號和微信伺服器對接成功之後,接下來要做的就是根據我們的業務需求呼叫微信公眾號提供的介面來實現相應的邏輯了。在使用微信公眾號介面中都需要一個access_token。

1)access_token介紹

 access_token是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需使用access_token。開發者需要進行妥善儲存。access_token的儲存至少要保留512個字元空間。access_token的有效期目前為2個小時,需定時重新整理,重複獲取將導致上次獲取的access_token失效。

  總結以上說明,access_token需要做到以下兩點:

  1.因為access_token有2個小時的時效性,要有一個機制保證最長2個小時重新獲取一次。

  2.因為介面呼叫上限每天2000次,所以不能呼叫太頻繁。

2)獲取access_token步驟

公眾號可以使用AppID和AppSecret呼叫本介面來獲取access_token。AppID和AppSecret可在“微信公眾平臺-開發-基本配置”頁中獲得(需要已經成為開發者,且帳號沒有異常狀態)。呼叫介面時,請登入“微信公眾平臺-開發-基本配置”提前將伺服器IP地址新增到IP白名單中,點選檢視設定方法,否則將無法呼叫成功。

介面呼叫請求說明

https請求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

引數說明

引數 是否必須 說明
grant_type 獲取access_token填寫client_credential
appid 第三方使用者唯一憑證
secret 第三方使用者唯一憑證金鑰,即appsecret

返回說明

正常情況下,微信會返回下述JSON資料包給公眾號:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

引數說明

引數 說明
access_token 獲取到的憑證
expires_in 憑證有效時間,單位:秒

錯誤時微信會返回錯誤碼等資訊,JSON資料包示例如下(該示例為AppID無效錯誤):

{"errcode":40013,"errmsg":"invalid appid"}

返回碼說明

返回碼 說明
-1 系統繁忙,此時請開發者稍候再試
0 請求成功
40001 AppSecret錯誤或者AppSecret不屬於這個公眾號,請開發者確認AppSecret的正確性
40002 請確保grant_type欄位值為client_credential
40164 呼叫介面的IP地址不在白名單中,請在介面IP白名單中進行設定

3)程式碼實現獲取access_token

定義一個預設啟動的servlet,在init方法中啟動一個Thread,這個程序中定義一個無限迴圈的方法,用來獲取access_token,當獲取成功後,此程序休眠7000秒(7000秒=1.944444444444444小時),否則休眠3秒鐘繼續獲取。流程圖如下:

image.png

定義一個dto AccessToken:

package AccessToken;

public class AccessToken {
    private String tokenName; //獲取到的憑證
    private int expireSecond;    //憑證有效時間  單位:秒

    public String getTokenName() {
        return tokenName;
    }

    public void setTokenName(String tokenName) {
        this.tokenName = tokenName;
    }

    public int getExpireSecond() {
        return expireSecond;
    }

    public void setExpireSecond(int expireSecond) {
        this.expireSecond = expireSecond;
    }
}
package AccessToken;

import AccessToken.AccessToken;

public class AccessTokenInfo {

    public static AccessToken accessToken = null;
}

編寫一個用於發起https請求的工具類NetWorkUtil,程式碼如下:

getHttpsResponse方法是請求一個https地址,引數requestMethod為字串“GET”或者“POST”,傳null或者“”預設為get方式。

package AccessToken;

import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * created by xiuhong.chen
 * 2017/12/28
 * 發起HTTPS請求的工具類
 * getHttpsResponse方法是請求一個https地址,引數requestMethod為字串“GET”或者“POST”,傳null或者“”預設為get方式。
 */
public class NetWorkUtil {
    /**
     * 發起HTTPS請求
     * @param reqUrl
     * @param requestMethod
     * @return 相應字串
     */
    public String getHttpsResponse(String reqUrl, String requestMethod) {
        URL url;
        InputStream is;
        String result ="";

        try {
            url = new URL(reqUrl);
            HttpsURLConnection con = (HttpsURLConnection) url.openConnection();

            TrustManager[] tm = {xtm};
            SSLContext ctx = SSLContext.getInstance("TLS");
            ctx.init(null, tm, null);

            con.setSSLSocketFactory(ctx.getSocketFactory());
            con.setHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }
            });

            con.setDoInput(true); //允許輸入流,即允許下載

            //在android中必須將此項設定為false
            con.setDoOutput(false); //允許輸出流,即允許上傳
            con.setUseCaches(false); //不使用緩衝
            if (null != requestMethod && !requestMethod.equals("")) {
                con.setRequestMethod(requestMethod); //使用指定的方式
            } else {
                con.setRequestMethod("GET"); //使用get請求
            }
            is = con.getInputStream();   //獲取輸入流,此時才真正建立連結
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader bufferReader = new BufferedReader(isr);
            String inputLine;
            while ((inputLine = bufferReader.readLine()) != null) {
                result += inputLine + "\n";
            }
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    X509TrustManager xtm = new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                throws CertificateException {
        }
    };
}

定義一個預設啟動的servlet,在init方法中啟動一個新的執行緒去獲取accessToken:

此處需要將JSON資料解析為object, 需要用到fastjson.jar

package AccessToken;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;


public class AccessTokenServlet extends HttpServlet {
    static Logger logger = LoggerFactory.getLogger(AccessTokenServlet.class);

    @Override
    public void init() throws ServletException {
        System.out.println("-----啟動AccessTokenServlet-----");
        super.init();

        final String appId = getInitParameter("appId");
        final String appSecret = getInitParameter("appSecret");

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        //獲取accessToken
                        AccessTokenInfo.accessToken = getAccessToken(appId, appSecret);
                        //獲取成功
                        if (AccessTokenInfo.accessToken != null) {
                            //獲取到access_token 休眠7000秒,大約2個小時左右
                            Thread.sleep(7000 * 1000);
                        } else {
                            //獲取失敗
                            Thread.sleep(1000 * 3); //獲取的access_token為空 休眠3秒
                        }
                    } catch (Exception e) {
                        System.out.println("發生異常:" + e.getMessage());
                        e.printStackTrace();
                        try {
                            Thread.sleep(1000 * 10); //發生異常休眠1秒
                        } catch (Exception e1) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

    private AccessToken getAccessToken(String appId, String appSecret) {
        NetWorkUtil netHelper = new NetWorkUtil();
        /**
         * 介面地址為https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,其中grant_type固定寫為client_credential即可。
         */
        String Url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
        //此請求為https的get請求,返回的資料格式為{"access_token":"ACCESS_TOKEN","expires_in":7200}
        String result = netHelper.getHttpsResponse(Url, "");
        System.out.println("獲取到的access_token="+result);

        //使用FastJson將Json字串解析成Json物件
        JSONObject json = JSON.parseObject(result);
        AccessToken token = new AccessToken();
        token.setTokenName(json.getString("access_token"));
        token.setExpireSecond(json.getInteger("expires_in"));
        return token;
    }

}

然後在web.xml中配置AccessTokenServlet:

    <servlet>
        <servlet-name>accessTokenServlet</servlet-name>
        <servlet-class>weChatServlet.AccessTokenServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>accessTokenServlet</servlet-name>
        <url-pattern>/accessTokenServlet</url-pattern> <!--url-pattern必須與servlet-name一致-->
    </servlet-mapping>

index.jsp:

<%-- Created by IntelliJ IDEA. --%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<c:set var="basePath" value="${pageContext.request.contextPath }"></c:set>
<%@ page import="AccessToken.AccessTokenInfo"%>
<html>
  <head>
    <title></title>
  </head>
  <body>

      <h3>微信公眾號測試!</h3>
      <form action="${pageContext.request.contextPath}/weChatServlet" method="get">
          <button onclick="submit">測試微信公眾號</button>
      </form>

      <hr/>

      <%--獲取access_token--%>
      <form action="${pageContext.request.contextPath}/accessTokenServlet" method="get">
          <button onclick="submit">獲取access_token</button>
      </form>
      <c:if test="AccessTokenInfo.accessToken != null">
          access_token為:<%=AccessTokenInfo.accessToken.getTokenName()%>
      </c:if>

  </body>
</html>

啟動專案, 點選獲取access_token按鈕: 獲取出錯, 返回碼40013,表示AppID無效錯誤

-----啟動AccessTokenServlet-----
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
{"errcode":40013,"errmsg":"invalid appid hint: [OwC0oa06551466]"}

獲取到的access_token={"errcode":40013,"errmsg":"invalid appid hint: [OwC0oa06551466]"}

再次確認appID和appsecret沒有錯誤之後,再次獲取token:

image.png

至此, access_token獲取成功 !

6.總結一下專案啟動之後, 通過微信公眾號測試的全過程:

1) 開啟外網訪問 : CMD進入natapp目錄下, 執行命令natapp -authtoken yourauthtoken , 得到外網訪問的域名

2) Tomcat啟動專案

3) 進入微信公眾號測試管理平臺, 修改介面配置資訊URL為: 新域名/weChatServlet , 待簽名校驗通過,就可以測試

4) 進入測試公眾號, 傳送訊息進行測試

7.被動傳送使用者訊息

業務邏輯(一) — 傳送文字訊息

經過上述的三步,我們開發前的準備工作已經完成了,接下來要做的就是接收微信伺服器傳送的訊息並做出響應

從微信公眾平臺介面訊息指南中可以瞭解到,當用戶向公眾帳號發訊息時,微信伺服器會將訊息通過POST方式提交給我們在介面配置資訊中填寫的URL,而我們就需要在URL所指向的請求處理類WxServlet的doPost方法中接收訊息、處理訊息和響應訊息。

1)編寫一個用於處理訊息的工具類

這個工具類主要是解析訊息, 構建訊息

package weChatServlet;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MessageUtil {
    /**
     * 解析微信發來的請求(XML)
     * @param request
     * @return map
     * @throws Exception
     */
    public static Map<String,String> parseXml(HttpServletRequest request) throws Exception {
        // 將解析結果儲存在HashMap中
        Map<String,String> map = new HashMap();
        // 從request中取得輸入流
        InputStream inputStream = request.getInputStream();
        System.out.println("獲取輸入流");
        // 讀取輸入流
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        // 得到xml根元素
        Element root = document.getRootElement();
        // 得到根元素的所有子節點
        List<Element> elementList = root.elements();

        // 遍歷所有子節點
        for (Element e : elementList) {
            System.out.println(e.getName() + "|" + e.getText());
            map.put(e.getName(), e.getText());
        }

        // 釋放資源
        inputStream.close();
        inputStream = null;
        return map;
    }

    /**
     * 根據訊息型別 構造返回訊息
     */
    public static String buildXml(Map<String,String> map) {
        String result;
        String msgType = map.get("MsgType").toString();
        System.out.println("MsgType:" + msgType);
        if(msgType.toUpperCase().equals("TEXT")){
            result = buildTextMessage(map, "Cherry的小小窩, 請問客官想要點啥?");
        }else{
            String fromUserName = map.get("FromUserName");
            // 開發者微訊號
            String toUserName = map.get("ToUserName");
            result = String
                    .format(
                            "<xml>" +
                                    "<ToUserName><![CDATA[%s]]></ToUserName>" +
                                    "<FromUserName><![CDATA[%s]]></FromUserName>" +
                                    "<CreateTime>%s</CreateTime>" +
                                    "<MsgType><![CDATA[text]]></MsgType>" +
                                    "<Content><![CDATA[%s]]></Content>" +
                                    "</xml>",
                            fromUserName, toUserName, getUtcTime(),
                            "請回復如下關鍵詞:\n文字\n圖片\n語音\n視訊\n音樂\n圖文");
        }

        return result;
    }

    /**
     * 構造文字訊息
     *
     * @param map
     * @param content
     * @return
     */
    private static String buildTextMessage(Map<String,String> map, String content) {
        //傳送方帳號
        String fromUserName = map.get("FromUserName");
        // 開發者微訊號
        String toUserName = map.get("ToUserName");
        /**
         * 文字訊息XML資料格式
         */
        return String.format(
                "<xml>" +
                        "<ToUserName><![CDATA[%s]]></ToUserName>" +
                        "<FromUserName><![CDATA[%s]]></FromUserName>" +
                        "<CreateTime>%s</CreateTime>" +
                        "<MsgType><![CDATA[text]]></MsgType>" +
                        "<Content><![CDATA[%s]]></Content>" + "</xml>",
                fromUserName, toUserName, getUtcTime(), content);
    }

    private static String getUtcTime() {
        Date dt = new Date();// 如果不需要格式,可直接用dt,dt就是當前系統時間
        DateFormat df = new SimpleDateFormat("yyyyMMddhhmm");// 設定顯示格式
        String nowTime = df.format(dt);
        long dd = (long) 0;
        try {
            dd = df.parse(nowTime).getTime();
        } catch (Exception e) {

        }
        return String.valueOf(dd);
    }

}
2)在WxServlet的doPost方法中處理請求

 WxServlet的doPost方法的程式碼如下:

package weChatServlet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

public class WeChatAccounts extends HttpServlet {
    static Logger logger = LoggerFactory.getLogger(WeChatAccounts.class);

    /*
    * 自定義token, 用作生成簽名,從而驗證安全性
    * */
    private final String TOKEN = "cherry";

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // TODO 接收、處理、響應由微信伺服器轉發的使用者傳送給公眾帳號的訊息
        // 將請求、響應的編碼均設定為UTF-8(防止中文亂碼)
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        System.out.println("請求進入");
        String result = "";
        try {
            Map<String,String> map = MessageUtil.parseXml(req);

            System.out.println("開始構造訊息");
            result = MessageUtil.buildXml(map);
            System.out.println(result);

            if(result.equals("")){
                result = "未正確響應";
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("發生異常:"+ e.getMessage());
        }
        resp.getWriter().println(result);
    }
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("-----開始校驗簽名-----");
     ..............................

    }
}

將WxStudy部署到Tomcat伺服器,啟動伺服器,記得使用ngrok將本地Tomcat伺服器的8080埠對映到外網,保證介面配置資訊的URL地址:http://xdp.ngrok.natapp.cn/WxServlet可以正常與微信伺服器通訊

  登入到我們的測試公眾號的管理後臺,然後用微信掃描一下測試號的二維碼,如下圖所示:

image.png

關注成功後,我們開發好的公眾號應用會先給使用者發一條提示使用者操作的文字訊息,微信使用者根據提示操作輸入”文字”,我們的公眾號應用接收到使用者請求後就給使用者回覆了一條我們自己構建好的文字訊息,如下圖所示:

image.png

我們公眾號獲取到的輸入流如下:

第一次關注時獲取的輸入流, 訊息型別為事件event, 訂閱
ToUserName|gh_fbcf752402d4
FromUserName|oEG9V1PeFHnsQviezVY9D4IDcyAk
CreateTime|1514461156
MsgType|event
Event|subscribe
EventKey|

第一次傳送訊息內容為"文字"獲取的輸入流, 訊息型別是text
ToUserName|gh_fbcf752402d4
FromUserName|oEG9V1PeFHnsQviezVY9D4IDcyAk
CreateTime|1514461351
MsgType|text
Content|文字
MsgId|6504561974052876654

我們公眾號應用響應給微信使用者的文字訊息的XML資料如下:

關注之後返回的訊息是:
MsgType:event
<xml>
    <ToUserName><![CDATA[oEG9V1PeFHnsQviezVY9D4IDcyAk]]></ToUserName>
    <FromUserName><![CDATA[gh_fbcf752402d4]]></FromUserName>                                    <CreateTime>1514417940000</