微信公眾號開發
最近又接觸到了公眾號開發,雖然用了新的框架,但是最基本的原理還是要知道的
本文主要對 2016-08-30-java-微信開發基本配置.md 做了補充
服務端配置
由於微信公眾平臺填寫的 URL 必須以http://或https://開頭,分別支援80埠和443埠,而我們的專案是跑在tomcat的8080上的,所以先用nginx為tomcat走一個簡單的反向代理
專案 tomcat 訪問地址:wx.tmaize.net:8080/wx/
需要配置成 wx.tmaize.net 的訪問方式,nginx 規則如下
# 虛擬主機wx.tmaize.net server { listen80; server_namewx.tmaize.net; location / { proxy_pass http://127.0.0.1:8080/wx/; } location /wx { proxy_pass http://127.0.0.1:8080; } }
至此服務端及域名就搭建完成了
微信公眾平臺 URL 驗證
在微信公眾平臺填寫服務端資訊時會對 URL 進行驗證,後端根據請求做出相應的回覆以此來通過驗證
注意:URL 是 Get 請求,其傳送訊息是 Post 請求,因此收到 GET 請求時候,需要對一系列引數進行簽名,返回正確的結果給微信伺服器
package net.tmaize.wx.controller; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/api/wx/msg") public class WeixinMsgController extends HttpServlet { private static final long serialVersionUID = 5975555563943880069L; // 微信公眾號上面設定的 private static final String token = "tmaize_dev"; // 伺服器驗證資訊 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); String signature = req.getParameter("signature"); String timestamp = req.getParameter("timestamp"); String nonce = req.getParameter("nonce"); String echostr = req.getParameter("echostr"); if (signature != null && signature != null && signature != null && signature != null) { ArrayList<String> array = new ArrayList<String>(); array.add(signature); array.add(timestamp); array.add(nonce); // 1.排序 String[] strArray = { token, timestamp, nonce }; Arrays.sort(strArray); StringBuilder sbuilder = new StringBuilder(); for (String str : strArray) { sbuilder.append(str); } String sortString = sbuilder.toString(); // 2.加密 MessageDigest digest = null; try { digest = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException e) { resp.getWriter().println("出錯啦"); } digest.update(sortString.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); } String mytoken = hexString.toString(); // 3.校驗簽名,如果檢驗成功輸出echostr,微信伺服器接收到此輸出,才會確認檢驗完成。 if (mytoken.equals(signature)) { resp.getWriter().println(echostr); } else { resp.getWriter().println("驗證失敗"); } } else { resp.getWriter().println("該GET請求不是來自微信"); } } // 接收到訊息 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // TODO Auto-generated method stub super.doPost(req, resp); } }
至此我們的域名已經通過微信的驗證
獲取 access_token
access_token 是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需使用 access_token。access_token 的有效期目前為 2 個小時,不然會過期無法使用,同時在兩小時內再次獲取也會使上次的 access_token 失效
通過 http 請求方式: GET 呼叫介面來獲取 access_token
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
公眾號拿到 appId 和 appSecret,請妥善儲存
注意還要設定 IP 白名單,之後白名單內的 IP 才可以呼叫獲取 access_token 的介面,應該是處於安全考慮防止被惡意呼叫重新整理 access_token 導致線上服務不可用
// 請求失敗,IP不在白名單 {"errcode":40164,"errmsg":"invalid ip x.x.x.x, not in whitelist hint: [Y907903054]"} // 請求成功{"access_token":"14_vF7jf5VPYqHWdRqiNH43kaA51_IfFvs7LrZHmcREfFBuOYW4p35kJPyR32qu1Z_0ear_daAdV-zvFXCYNX_Hxfr2XtU7fRcK-hgV8NRJ2zg4iOd5MUURHj2NnGYSdBhH5Dtl7LVnwh6DlknQLYIbAGAUHR","expires_in":7200}
程式碼如下,這裡為了合成一個程式碼塊,把業務都合在一起了,實際開發應該抽取成公共方法的
package net.tmaize.wx.controller; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet(value = "/api/wx/access_token", loadOnStartup = 0) public class AccessTokenCron extends HttpServlet { private static final long serialVersionUID = 4597002680960142831L; // 微信後臺獲取 private static final String appId = "********"; private static final String appSecret = "********"; // 共享 public static String accessToken = ""; //定時任務隨著專案啟動而啟動 @Override public void init() throws ServletException { new Thread(new Runnable() { @Override public void run() { try { while (true) { Pattern pattern = Pattern.compile("access_token\":\"(.+?)\""); Matcher matcher = pattern.matcher(getToken(appId, appSecret)); if (matcher.find()) { accessToken = matcher.group(1); } else { accessToken = ""; } Thread.sleep(1000 * 60 * 100); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().println(accessToken); } public static String getToken(String appId, String appSecret) { URL url = null; HttpURLConnection coon = null; StringBuffer stringBuffer = new StringBuffer(); try { url = new URL("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret); coon = (HttpURLConnection) url.openConnection(); coon.setInstanceFollowRedirects(false); coon.setUseCaches(false); coon.setAllowUserInteraction(false); coon.setRequestMethod("GET"); coon.connect(); BufferedReader URLinput = new BufferedReader(new InputStreamReader(coon.getInputStream())); String line; while ((line = URLinput.readLine()) != null) { stringBuffer.append(line); } } catch (Exception e) { stringBuffer.append("{}"); } finally { if (coon != null) { coon.disconnect(); } } return stringBuffer.toString(); } }
至此成功獲取到 accessToken
收發訊息
收發訊息都是使用 XML 做資料交換,同時還支援報文加密功能,為了方便,在為微信公眾平臺設定訊息加解密方式為明文模式
收發訊息對應的是 POST 請求,也就是我們 所以把邏輯寫在 WeixinMsgController 的 doPost 方法內
這裡做一個收到訊息返回一模一樣訊息的 Demo
以下程式碼依賴 dom4j
// 接收到訊息 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { InputStream inputStream = req.getInputStream(); SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); @SuppressWarnings("unchecked") List<Element> elementList = document.getRootElement().elements(); // xml轉物件,demo而已, 實際不要這樣寫,抽取成公用方法 TextMessage textMessage = new TextMessage(); for (Element e : elementList) { String value = e.getText(); switch (e.getName()) { case "ToUserName": textMessage.setToUserName(value); break; case "FromUserName": textMessage.setFromUserName(value); break; case "CreateTime": textMessage.setCreateTime(Long.valueOf(value)); break; case "MsgType": textMessage.setMsgType(value); break; case "Content": textMessage.setContent(value); break; case "MsgId": textMessage.setMsgId(Long.valueOf(value)); break; default: break; } } // 交換收件人發件人 String temp = textMessage.getFromUserName(); textMessage.setFromUserName(textMessage.getToUserName()); textMessage.setToUserName(temp); // 物件轉XML Document document2 = DocumentHelper.createDocument(); Element root = document2.addElement("xml"); Field[] field = textMessage.getClass().getDeclaredFields(); for (int i = 0; i < field.length; i++) { String name = field[i].getName(); if (!name.equals("serialVersionUID")) { // 首字母大寫 name = name.substring(0, 1).toUpperCase() + name.substring(1); Method m = textMessage.getClass().getMethod("get" + name); Element propertie = root.addElement(name); propertie.setText(m.invoke(textMessage).toString()); } } resp.getWriter().print(document2.asXML()); } catch (Exception e) { e.printStackTrace(); } }
最終效果如下
參考
ofollow,noindex" target="_blank">微信公眾平臺技術文件