有一段时间没写文章了,今天记录一下当时是怎么搭建的微信公众号后台吧。
一.基本步骤
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包加密)