有一段時間沒寫文章了,今天記錄一下當時是怎麼搭建的微信公衆號後臺吧。
一.基本步驟
1.申請賬號。https://mp.weixin.qq.com/
2.搭建自己的後臺服務。
公衆號的基本邏輯是,當用戶發送信息到你公衆賬號的時候,騰訊服務器收到消息之後推送一個消息到你的服務器,然後你的服務器作出相應,發送一個信息到騰訊服務器,然後騰訊服務器再發送信息給用戶。
你的服務器需要做的是就是實現接受到騰訊收到的信息時候如何解析,以及處理不同消息的邏輯,還有安全驗證。
3.如何解析信息。
先貼代碼,一個struts的action用來控制微信消息的處理,一個filter用來做身份驗證.
Action類:
package com.marsyoung.action;
import java.io.IOException;
import java.util.List;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.apache.struts2.ServletActionContext;
import org.json.JSONException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import com.marsyoung.service.WeChatService;
@Controller
@Scope("prototype")
public class WeChatAction extends BaseAction {
/*
* 校驗參數,目前只用在了weChatServerInterceptor中。
* */
String signature;// 微信加密簽名,signature結合了開發者填寫的token參數和請求中的timestamp參數、nonce參數。
String timestamp;// 時間戳
String nonce;// 隨機數
String echostr;// 隨機字符串
@Autowired
private WeChatService weChatService;
/**
* 申請消息接口
*
* @return
* @throws IOException
* @throws JSONException
* @throws MessagingException
*/
public String weChatInterface() throws IOException, JSONException, MessagingException {
if(echostr!=null){
resp=echostr;
return SUCCESS;
}
HttpServletRequest request = ServletActionContext.getRequest(); // 獲取客戶端發過來的HTTP請求
List<String> requestContent=IOUtils.readLines(request.getInputStream(), "UTF-8");
StringBuffer contentStr = new StringBuffer();
for(String s:requestContent){
contentStr.append(s);
}
log.info("收到Post來的數據:"+contentStr);
resp=weChatService.centralProcessor(contentStr.toString());
return SUCCESS;
}
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getNonce() {
return nonce;
}
public void setNonce(String nonce) {
this.nonce = nonce;
}
public String getEchostr() {
return echostr;
}
public void setEchostr(String echostr) {
this.echostr = echostr;
}
}
父類BaseAction,此處用到的只有resp這個string:
package com.marsyoung.action;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.ServletActionContext;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.marsyoung.domain.Pagination;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.Preparable;
/**
* BaseAction是各個類的父類,定義了個各類共用的參數和方法
* **/
public abstract class BaseAction implements Action, Preparable {
protected final Log log = LogFactory.getLog(getClass());
protected int userID; // 用戶ID
protected int id; // 各種實體的id
protected int pagerOffset = 0; // 實際上對應於請求參數pager.offset,該參數表示該頁第一條記錄在總記錄中的偏移量
protected Pagination pagination; // 分頁對象
protected String resp; // 返回給瀏覽器的JSON字符串
protected String basePath; // jsp頁面指定相對路徑用
protected JSONObject respJO;
protected JSONArray respJA;
public String execute() throws Exception {
return null;
}
public void prepare() {
// 設置pagerOffset的值爲請求參數pager.offset的值
HttpServletRequest request = ServletActionContext.getRequest();
if (request.getParameter("pager.offset") != null) {// pager.offset,taglib標籤自帶的屬性
pagerOffset = Integer.parseInt(request.getParameter("pager.offset"));
}
basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ request.getContextPath() + "/";
}
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID = userID;
}
public String getResp() {
return resp;
}
public void setResp(String resp) {
this.resp = resp;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPagerOffset() {
return pagerOffset;
}
public void setPagerOffset(int pagerOffset) {
this.pagerOffset = pagerOffset;
}
public Pagination getPagination() {
return pagination;
}
public void setPagination(Pagination pagination) {
this.pagination = pagination;
}
public String getBasePath() {
return basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
public JSONObject getRespJO() {
return respJO;
}
public void setRespJO(JSONObject respJO) {
this.respJO = respJO;
}
public JSONArray getRespJA() {
return respJA;
}
public void setRespJA(JSONArray respJA) {
this.respJA = respJA;
}
}
struts.xml配置,我只貼和wechat相關的部分了:
<!-- 基類package,定義了所有action共用的攔截器棧 -->
<package name="blog-default" extends="struts-default" abstract="true">
<!-- 攔截器配置 -->
<interceptors>
<!-- 自定義的異常和執行時間攔截器,會把異常信息和執行時間過長的action的信息記錄到日誌裏 -->
<interceptor name="exceptionAndExecuteTimeInterceptor" class="com.marsyoung.filter.ExceptionAndExecuteTimeInterceptor">
</interceptor>
<!-- 定義默認攔截器棧 -->
<interceptor-stack name="blog-stack">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="exceptionAndExecuteTimeInterceptor"/>
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="blog-stack"/>
<!-- 全局 results配置 -->
<global-results>
<result name="success">/global/json.jsp</result>
<result name="exception">/global/404.jsp</result>
<result name="input">/global/json.jsp</result>
<result name="notLogin">/global/not_login.jsp</result>
<result name="client-abort-exception">/global/ignored.jsp</result>
</global-results>
</package>
<package name="weChat-default" extends="blog-default" abstract="true">
<!-- 攔截器配置 -->
<interceptors>
<!-- 自定義的異常和執行時間攔截器,會把異常信息和執行時間過長的action的信息記錄到日誌裏 -->
<interceptor name="weChatServerInterceptor" class="com.marsyoung.filter.WeChatServerInterceptor">
</interceptor>
<interceptor-stack name="weChat-stack">
<interceptor-ref name="blog-stack"/>
<interceptor-ref name="weChatServerInterceptor"/>
</interceptor-stack>
<!-- 定義默認攔截器棧 -->
</interceptors>
<default-interceptor-ref name="weChat-stack"></default-interceptor-ref>
<!-- 全局 results配置 -->
</package>
<package name="weChat" extends="weChat-default" namespace="/weChat">
<!-- 攔截器配置 -->
<action name="*" class="weChatAction" method="{1}"/>
</package>
可以看到,在配置中有兩個filter,代碼如下:
package com.marsyoung.filter;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.ServletActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
public class ExceptionAndExecuteTimeInterceptor extends AbstractInterceptor {
private static final long serialVersionUID = -6442157043443401725L;
private static final Log log = LogFactory
.getLog(ExceptionAndExecuteTimeInterceptor.class);
private static final String EQUAL_SIGN = "=";
private static final String PLUS_SIGN = "+";
private static final String AND = "&";
@Override
public String intercept(ActionInvocation invocation) throws Exception {
/*
* 獲取該http請求的一些信息,下面的日誌會使用到
*/
HttpServletRequest request = ServletActionContext.getRequest(); // 獲取客戶端發過來的HTTP請求
String remoteHost = request.getHeader("x-real-ip"); // 獲取客戶端的主機名
if (remoteHost == null) {
remoteHost = "“沒有獲取到客戶端IP”";
}
String requestURL = request.getRequestURL().toString(); // 獲取客戶端請求的URL
@SuppressWarnings("unchecked")
Map<String, String[]> paramsMap = request.getParameterMap(); // 獲取所有的請求參數
/*
* 獲取所有參數的名值對信息的字符串表示,存儲在變量paramsStr中
*/
StringBuilder paramsStrSb = new StringBuilder();
if (paramsMap != null && paramsMap.size() > 0) {
Set<Entry<String, String[]>> paramsSet = paramsMap.entrySet();
for (Entry<String, String[]> param : paramsSet) {
StringBuilder paramStrSb = new StringBuilder();
String paramName = param.getKey(); // 參數的名字
String[] paramValues = param.getValue(); // 參數的值
if (paramValues.length == 1) { // 參數只有一個值,絕大多數情況
paramStrSb.append(paramName).append(EQUAL_SIGN)
.append(paramValues[0]);
} else {
paramStrSb.append(paramName).append(EQUAL_SIGN);
for (String paramValue : paramValues) {
paramStrSb.append(paramValue);
paramStrSb.append(PLUS_SIGN);
}
paramStrSb.deleteCharAt(paramStrSb.length() - 1);
}
paramsStrSb.append(paramStrSb).append(AND);
}
paramsStrSb.deleteCharAt(paramsStrSb.length() - 1);
}
String paramsStr = paramsStrSb.toString();
log.info("收到來自" + remoteHost + "的請求,URL:" + requestURL + ",參數:"
+ paramsStr);
/*
* 如果Action的執行過程中拋出異常,則記錄到日誌裏; 或者Action執行成功,但執行時間過長,也記錄到日誌裏
*/
String result = null;
long start = System.currentTimeMillis();
try {
// 執行該攔截器的下一個攔截器,或者如果沒有下一個攔截器,直接執行Action的execute方法
result = invocation.invoke();
} catch (Exception e) {
String msg = "拋出了異常!" + remoteHost + "的請求,URL:" + requestURL
+ ",參數:" + paramsStr;
log.error(msg, e);
return "exception";
}
long end = System.currentTimeMillis();
// 如果該Action的執行時間超過了500毫秒,則日誌記錄下來
final int MAX_TIME = 500;
long executeTimeMillis = end - start;
if (executeTimeMillis >= MAX_TIME) {
log.info("Action執行時間過長!執行" + remoteHost + "的請求,URL:" + requestURL
+ ",參數:" + paramsStr + ",共用時" + executeTimeMillis + "毫秒");
}
// 記錄返回的JSON字符串
if (request.getAttribute("resp") != null) {
String jsonStr = (String) request.getAttribute("resp");
log.debug("請求的URL爲:" + requestURL + ",參數爲:" + paramsStr
+ ",該請求返回的JSON字符串是:" + jsonStr);
}
return result;
}
}
package com.marsyoung.filter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.struts2.ServletActionContext;
import com.marsyoung.constants.WeiXinConstants;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
/**
* 微信驗證過濾器
*
* @author Mars
*
*/
public class WeChatServerInterceptor extends AbstractInterceptor{
private static final long serialVersionUID = -3421357173884989787L;
@Override
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest(); // 獲取客戶端發過來的HTTP請求
String signature;// 微信加密簽名,signature結合了開發者填寫的token參數和請求中的timestamp參數、nonce參數。
String timestamp;// 時間戳
String nonce;// 隨機數
signature=request.getParameter("signature");
timestamp=request.getParameter("timestamp");
nonce=request.getParameter("nonce");
if(signature==null||timestamp==null||nonce==null){
request.setAttribute("resp", "缺少參數");
return "input";
}
//1. 將token、timestamp、nonce三個參數進行字典序排序
List<String> paramsList=Arrays.asList(WeiXinConstants.WeiXin_Token,timestamp,nonce);
//2. 將三個參數字符串拼接成一個字符串進行sha1加密
Collections.sort(paramsList);
String paramsStr="";
for(String s:paramsList){
paramsStr=paramsStr+s;
}
String createSignature=new String(DigestUtils.shaHex(paramsStr.getBytes("UTF-8")));
//3. 開發者獲得加密後的字符串可與signature對比,標識該請求來源於微信
if(createSignature.equals(signature)){
//request.setAttribute("resp", echostr);這個值得設置應該放到Action中去。
return invocation.invoke();
}else{
request.setAttribute("resp", "stupid。");
return "input";
}
}
}
下面的這個filter是實現微信公衆賬號上關於安全驗證的代碼,WeiXinConstants中存儲的是我的安全驗證的一些常量,就不貼了。
二.遇上的問題
1.關於struts如何接卸post數據?
一般來說,只要有對應的參數,我們在struts中,配置對應名稱的變量的get和set方法就可以獲取到對應的數據。但是微信post過來的數據是沒有參數名的,那麼如何解決?我用的還是比較原始的方法,直接從request中把對應的流讀出來,然後解析出對應的內容。不知道struts是否有封裝對應的邏輯。。
2.關於md5加密。
關於加密,有兩種思路,一種是自己寫個類實現對應的加密,一種是引用一些現有的包。我選擇的後者,引入了common-codec包。一句話搞定。(關於加密的一些其它介紹,見另外一篇文章 使用Commons-codec包加密)