├── README.md ├── pom.xml ├── robot-demo ├── pom.xml └── src │ └── main │ ├── java │ └── org │ │ └── mvnsearch │ │ └── wx │ │ └── web │ │ └── controllers │ │ └── PortalController.java │ ├── resources │ ├── appContext-robot.xml │ ├── struts.properties │ ├── struts.xml │ └── struts2packages │ │ └── portal-package.xml │ └── webapp │ ├── WEB-INF │ ├── pages │ │ ├── weixin.jspx │ │ └── welcome.jsp │ ├── web.xml │ └── weixin-router.xml │ └── index.action └── robot-sdk ├── pom.xml └── src ├── main └── java │ └── org │ └── mvnsearch │ └── wx │ ├── WeixinMessage.java │ ├── WeixinUtils.java │ ├── rewrite │ ├── Conf.java │ ├── NormalRewrittenUrl.java │ ├── NormalRule.java │ ├── RewrittenUrl.java │ ├── Rule.java │ ├── RuleChain.java │ └── UrlRewriter.java │ └── servlet │ ├── WeixinMessageContext.java │ └── WexinRobotServlet.java └── test ├── java └── org │ └── mvnsearch │ └── wx │ ├── ConfTest.java │ └── WeixinMessageTest.java └── resources ├── weixin-robot-flow.puml └── weixin-router.xml /README.md: -------------------------------------------------------------------------------- 1 | Weixin robot Java 2 | ======================================== 3 | 微信公共平台自动回复机器人的Java SDK,你可以使用SDK简单快速构建微信机器人。 4 | 微信Robot Java借鉴了url rewrite的思想,url rewrite是根据url进行路由,而微信Robot则是根据消息类型和内容进行路由。 5 | 整理的流程如下: 6 | 7 | * WexinRobotServlet负责认证和微信消息接收 8 | * 接收后进行XML解析,构建出 WeixinMessage对象,并注入到request的attribute中,名字为wxMsg 9 | * 根据配置的路由参数,对微信消息进行匹配 10 | * 根据匹配的结果进行URL路由转发 11 | * 最后由处理的组件完成xml格式反馈的输出 12 | 13 | 14 | ### 如何使用 15 | 首先我们要在pom.xml中添加: 16 | 17 | 18 | org.mvnsearch.wx 19 | wx-robot-sdk 20 | 1.0.0 21 | 22 | 23 | 接下来就是修改web.xml添加Servlet负责接收微信公共平台发过来的消息: 24 | 25 | 26 | WexinRobotServlet 27 | org.mvnsearch.wx.servlet.WexinRobotServlet 28 | 29 | token 30 | yourTokenHere 31 | 32 | 33 | confFile 34 | /WEB-INF/weixin-router.xml 35 | 36 | 37 | 38 | 39 | 40 | WexinRobotServlet 41 | /servlet/weixinrobot 42 | 43 | 44 | 其中token是指你在微信公共平台上的token,这里和其一致。接下来我们还需要设置servlet mapping,添加url-pattern, 45 | 然后将最终的url,如上述配置的 http://www.foobar.com/servlet/weixinrobot 作为微信公共平台接口配置中的URL。 46 | 接下来是在WEB-INF目录下创建一个weixin-router.xml的文件,完成信息的路由。 47 | 48 | 接收到的消息如何路由到指定的组件处理,如Struts的Controller等,这里我们需要创建一个router文件,这里还是根据 49 | 消息的类型和内容进行转发,内容如下: 50 | 51 | 52 | 53 | 54 | 55 | 56 | text 57 | [\d]{11} 58 | /weixin2.wx 59 | 60 | 61 | 62 | text 63 | /weixin.wx 64 | 65 | 66 | 67 | 68 | 上述的例子就是如果内容是手机号,交与/weixin2.wx这个URL处理。当然我们设置了一个默认的选项,如果text类型的消息没有任何 69 | 匹配,则由/weixin.wx处理。 70 | 71 | 收到微信消息后,要给出消息反馈。微信公共平台的消息反馈是一个xml格式的数据,在Java的系统中,我们使用jspx输出接口,样例如下: 72 | 73 | 74 | 75 | ${wxMsg.sender} 76 | ${wxMsg.receiver} 77 | ${wxMsg.createdTime} 78 | text 79 | ${content} 80 | 0 81 | 82 | 83 | ### 编码 84 | 85 | * 在代码中获取解析好的微信消息 WeixinMessage wxMsg = (WeixinMessage)request.getAttribute("wxMsg"); 86 | 87 | ### FAQ 88 | 89 | * 如何调试: 由于微信服务器只能通知互联网IP和80端口,开发时你可以在你家中的ADSL拨号路由器上设置一下80转发, 90 | 然后微信服务器的消息通知就可以路由到你的笔记本上,方便你测试和开发。查看你的互联网Ip请访问 http://ip.mvnsearch.org 91 | * 参考样例清访问robot-demo Maven module 92 | 93 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.mvnsearch.wx 6 | weixin-robot 7 | pom 8 | 1.0.0-SNAPSHOT 9 | 10 | robot-sdk 11 | robot-demo 12 | 13 | Weixin Robot Java Project 14 | 15 | 微信公共平台自动回复机器人Java SDK 16 | 17 | 2013 18 | 19 | MvnSearch 20 | http://www.mvnsearch.org 21 | 22 | 23 | UTF-8 24 | 25 | 26 | 27 | linux_china 28 | Jacky Chan 29 | libing.chen@gmail.com 30 | http://weibo.com/linux2china 31 | 32 | 开发人员 33 | 34 | 35 | 36 | 37 | scm:git@github.com:linux-china/weixin-robot-java.git 38 | scm:git@github.com:linux-china/weixin-robot-java.git 39 | https://github.com/linux-china/weixin-robot-java 40 | 41 | 42 | 43 | 44 | junit 45 | junit 46 | 3.8.2 47 | test 48 | 49 | 50 | -------------------------------------------------------------------------------- /robot-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | weixin-robot 7 | org.mvnsearch.wx 8 | 1.0.0-SNAPSHOT 9 | 10 | robot-demo 11 | 1.0.0-SNAPSHOT 12 | war 13 | robot demo Maven Webapp 14 | 15 | 2.3.15.1 16 | 3.2.2.RELEASE 17 | 18 | 19 | 20 | org.mvnsearch.wx 21 | wx-robot-sdk 22 | 1.0.0-SNAPSHOT 23 | 24 | 25 | 26 | javax.servlet 27 | servlet-api 28 | 3.0-alpha-1 29 | provided 30 | 31 | 32 | javax.servlet.jsp 33 | jsp-api 34 | 2.2 35 | provided 36 | 37 | 38 | 39 | org.springframework 40 | spring-beans 41 | ${spring.version} 42 | 43 | 44 | org.springframework 45 | spring-core 46 | ${spring.version} 47 | 48 | 49 | org.springframework 50 | spring-context 51 | ${spring.version} 52 | 53 | 54 | org.springframework 55 | spring-context-support 56 | ${spring.version} 57 | 58 | 59 | org.springframework 60 | spring-aspects 61 | ${spring.version} 62 | 63 | 64 | org.springframework 65 | spring-web 66 | ${spring.version} 67 | 68 | 69 | org.springframework 70 | spring-aop 71 | ${spring.version} 72 | 73 | 74 | 75 | org.apache.struts 76 | struts2-core 77 | ${struts2.version} 78 | 79 | 80 | org.apache.struts 81 | struts2-spring-plugin 82 | ${struts2.version} 83 | 84 | 85 | org.springframework 86 | spring-beans 87 | 88 | 89 | org.springframework 90 | spring-context 91 | 92 | 93 | org.springframework 94 | spring-core 95 | 96 | 97 | org.springframework 98 | spring-web 99 | 100 | 101 | 102 | 103 | junit 104 | junit 105 | 3.8.1 106 | test 107 | 108 | 109 | 110 | robot-demo 111 | 112 | 113 | -------------------------------------------------------------------------------- /robot-demo/src/main/java/org/mvnsearch/wx/web/controllers/PortalController.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.web.controllers; 2 | 3 | import com.opensymphony.xwork2.ActionSupport; 4 | import org.apache.struts2.interceptor.ServletRequestAware; 5 | import org.mvnsearch.wx.WeixinMessage; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import java.util.Date; 9 | 10 | /** 11 | * portal controller 12 | * 13 | * @author linux_china 14 | */ 15 | public class PortalController extends ActionSupport implements ServletRequestAware { 16 | /** 17 | * http servlet request 18 | */ 19 | HttpServletRequest request; 20 | 21 | /** 22 | * inject http servlet request 23 | * 24 | * @param httpServletRequest http servlet request 25 | */ 26 | public void setServletRequest(HttpServletRequest httpServletRequest) { 27 | this.request = httpServletRequest; 28 | } 29 | 30 | /** 31 | * project front page 32 | * 33 | * @return welcome page 34 | */ 35 | public String index() { 36 | return "index"; 37 | } 38 | 39 | /** 40 | * 显示微信消息 41 | * 42 | * @return weixing response 43 | */ 44 | public String weixin() { 45 | WeixinMessage wxMsg = (WeixinMessage) request.getAttribute("wxMsg"); 46 | if (wxMsg == null) { 47 | wxMsg = new WeixinMessage(); 48 | wxMsg.setSender("jacky"); 49 | wxMsg.setReceiver("leijuan"); 50 | wxMsg.setContent("txt"); 51 | wxMsg.setCreatedTime(new Date()); 52 | request.setAttribute("wxMsg", wxMsg); 53 | } 54 | request.setAttribute("content", "echo:" + wxMsg.getContent()); 55 | return "weixin"; 56 | } 57 | 58 | /** 59 | * 显示微信消息2 60 | * 61 | * @return weixing response 62 | */ 63 | public String weixin2() { 64 | request.setAttribute("content", "weixin2"); 65 | return "weixin"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /robot-demo/src/main/resources/appContext-robot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /robot-demo/src/main/resources/struts.properties: -------------------------------------------------------------------------------- 1 | struts.i18n.reload = true 2 | struts.devMode = true 3 | struts.configuration.xml.reload = true 4 | struts.url.http.port = 80 5 | struts.serve.static = true 6 | struts.serve.static.browserCache = false 7 | struts.objectFactory = spring 8 | struts.objectFactory.spring.autoWire = name 9 | struts.objectFactory.spring.useClassCache = true 10 | struts.locale = zh_CN 11 | struts.i18n.encoding = UTF-8 12 | struts.ui.theme = xhtml 13 | struts.enable.DynamicMethodInvocation = true 14 | struts.custom.i18n.resources = global 15 | struts.action.extension = action,do,jsn,wx 16 | struts.multipart.maxSize = 5242880 -------------------------------------------------------------------------------- /robot-demo/src/main/resources/struts.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /robot-demo/src/main/resources/struts2packages/portal-package.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | /WEB-INF/pages/welcome.jsp 11 | /WEB-INF/pages/weixin.jspx 12 | 13 | 14 | -------------------------------------------------------------------------------- /robot-demo/src/main/webapp/WEB-INF/pages/weixin.jspx: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${wxMsg.sender} 4 | ${wxMsg.receiver} 5 | ${wxMsg.createdTime} 6 | text 7 | ${content} 8 | 0 9 | 10 | -------------------------------------------------------------------------------- /robot-demo/src/main/webapp/WEB-INF/pages/welcome.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Weixin Robot 6 | 7 | 8 |

Weixin Robot Demo

9 | 10 | 11 | -------------------------------------------------------------------------------- /robot-demo/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | Weixin Robot Demo 9 | 10 | 11 | contextConfigLocation 12 | 13 | classpath:/appContext-robot.xml 14 | 15 | 16 | 17 | 18 | 19 | org.springframework.web.context.ContextLoaderListener 20 | 21 | 22 | 23 | WexinRobotServlet 24 | org.mvnsearch.wx.servlet.WexinRobotServlet 25 | 26 | token 27 | nananjingdu 28 | 29 | 30 | confFile 31 | /WEB-INF/weixin-router.xml 32 | 33 | 34 | 35 | 36 | 37 | WexinRobotServlet 38 | /servlet/weixinrobot 39 | 40 | 41 | 42 | struts 43 | org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter 44 | 45 | 46 | 47 | struts 48 | *.action 49 | REQUEST 50 | FORWARD 51 | 52 | 53 | struts 54 | *.wx 55 | REQUEST 56 | FORWARD 57 | 58 | 59 | index.action 60 | 61 | 62 | 63 | 64 | *.jsp 65 | UTF-8 66 | true 67 | 68 | 69 | *.jspx 70 | UTF-8 71 | true 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /robot-demo/src/main/webapp/WEB-INF/weixin-router.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | text 7 | [\d]{11} 8 | /weixin2.wx 9 | 10 | 11 | 12 | text 13 | /weixin.wx 14 | 15 | 16 | -------------------------------------------------------------------------------- /robot-demo/src/main/webapp/index.action: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-china/weixin-robot-java/985b1f68c3b21fb2ae3607aac2554dd6380848d0/robot-demo/src/main/webapp/index.action -------------------------------------------------------------------------------- /robot-sdk/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.mvnsearch.wx 7 | wx-robot-sdk 8 | 1.0.0-SNAPSHOT 9 | 10 | Weixin Robot Java SDK 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | linux_china 18 | Jacky Chan 19 | libing.chen@gmail.com 20 | http://weibo.com/linux2china 21 | 22 | 开发人员 23 | 24 | 25 | 26 | 27 | scm:git@github.com:linux-china/weixin-robot-java.git 28 | scm:git@github.com:linux-china/weixin-robot-java.git 29 | https://github.com/linux-china/weixin-robot-java 30 | 31 | 32 | 33 | 34 | javax.servlet 35 | servlet-api 36 | 2.5 37 | provided 38 | 39 | 40 | org.jdom 41 | jdom 42 | 1.1.3 43 | 44 | 45 | commons-codec 46 | commons-codec 47 | 1.7 48 | 49 | 50 | com.intellij 51 | annotations 52 | 12.0 53 | 54 | 55 | junit 56 | junit 57 | 3.8.1 58 | test 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/WeixinMessage.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * weixin message 9 | * 10 | * @author linux_china 11 | */ 12 | public class WeixinMessage { 13 | /** 14 | * text message 15 | */ 16 | public static final String type_text = "text"; 17 | /** 18 | * image message 19 | */ 20 | public static final String type_image = "image"; 21 | /** 22 | * location message 23 | */ 24 | public static final String type_location = "location"; 25 | /** 26 | * link message 27 | */ 28 | public static final String type_link = "link"; 29 | /** 30 | * news message 31 | */ 32 | public static final String type_news = "news"; 33 | /** 34 | * music message 35 | */ 36 | public static final String type_music = "music"; 37 | 38 | /** 39 | * subscribe message 40 | */ 41 | public static final String type_subscribe = "subscribe"; 42 | /** 43 | * unsubscribe message 44 | */ 45 | public static final String type_unsubscribe = "unsubscribe"; 46 | /** 47 | * click message 48 | */ 49 | public static final String type_click = "click"; 50 | /** 51 | * message id 52 | */ 53 | private String id; 54 | /** 55 | * sender,微信号 56 | */ 57 | private String sender; 58 | /** 59 | * receiver,微信号 60 | */ 61 | private String receiver; 62 | /** 63 | * created timestamp 64 | */ 65 | private Date createdTime; 66 | /** 67 | * type: text, image, location, link 68 | */ 69 | private String type; 70 | /** 71 | * content, such as text, image url, link etc 72 | */ 73 | private String content; 74 | 75 | public String getId() { 76 | return id; 77 | } 78 | 79 | public void setId(String id) { 80 | this.id = id; 81 | } 82 | 83 | public String getSender() { 84 | return sender; 85 | } 86 | 87 | public void setSender(String sender) { 88 | this.sender = sender; 89 | } 90 | 91 | public String getReceiver() { 92 | return receiver; 93 | } 94 | 95 | public void setReceiver(String receiver) { 96 | this.receiver = receiver; 97 | } 98 | 99 | public Date getCreatedTime() { 100 | return createdTime; 101 | } 102 | 103 | public void setCreatedTime(Date createdTime) { 104 | this.createdTime = createdTime; 105 | } 106 | 107 | public Long getCreatedSeconds() { 108 | return this.createdTime.getTime() / 1000; 109 | } 110 | 111 | public String getType() { 112 | return type; 113 | } 114 | 115 | public void setType(String type) { 116 | this.type = type; 117 | } 118 | 119 | public String getContent() { 120 | return content; 121 | } 122 | 123 | public void setContent(String content) { 124 | this.content = content; 125 | } 126 | 127 | /** 128 | * fill message 129 | * 130 | * @param type type 131 | * @param content content 132 | */ 133 | public void fill(String type, String content) { 134 | this.type = type; 135 | this.content = content; 136 | } 137 | 138 | /** 139 | * get reply message 140 | * 141 | * @return reply message 142 | */ 143 | @NotNull 144 | public WeixinMessage getReply() { 145 | WeixinMessage reply = new WeixinMessage(); 146 | reply.setReceiver(this.sender); 147 | reply.setSender(this.receiver); 148 | reply.setCreatedTime(new Date()); 149 | return reply; 150 | } 151 | 152 | /** 153 | * get response xml 154 | * 155 | * @param type type 156 | * @param content content 157 | * @return response xml 158 | */ 159 | public String getResponseXml(String type, String content) { 160 | String template = "\n" + 161 | " \n" + 162 | " \n" + 163 | " %s\n" + 164 | " \n" + 165 | " \n" + 166 | " 0\n" + 167 | ""; 168 | return String.format(template, sender, receiver, String.valueOf(System.currentTimeMillis() / 1000), type, content); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/WeixinUtils.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx; 2 | 3 | import org.jdom.Document; 4 | import org.jdom.Element; 5 | import org.jdom.input.SAXBuilder; 6 | 7 | import java.io.InputStream; 8 | import java.io.StringReader; 9 | import java.util.Date; 10 | 11 | /** 12 | * weixin utils 13 | * 14 | * @author linux_china 15 | */ 16 | public class WeixinUtils { 17 | 18 | /** 19 | * parse xml to get weixin message 20 | * 21 | * @param xmlText xml text 22 | * @return weixin message 23 | * @throws Exception exception 24 | */ 25 | public static WeixinMessage parseXML(String xmlText) throws Exception { 26 | SAXBuilder saxBuilder = new SAXBuilder(); 27 | Document document = saxBuilder.build(new StringReader(xmlText)); 28 | return constructMsg(document); 29 | } 30 | 31 | /** 32 | * parse xml to get weixin message 33 | * 34 | * @param inputStream xml text 35 | * @return weixin message 36 | * @throws Exception exception 37 | */ 38 | public static WeixinMessage parseXML(InputStream inputStream) throws Exception { 39 | SAXBuilder saxBuilder = new SAXBuilder(); 40 | Document document = saxBuilder.build(inputStream); 41 | return constructMsg(document); 42 | } 43 | 44 | /** 45 | * construct weixin message from xml document 46 | * 47 | * @param document xml document 48 | * @return weixin message 49 | */ 50 | private static WeixinMessage constructMsg(Document document) { 51 | Element rootElement = document.getRootElement(); 52 | WeixinMessage msg = new WeixinMessage(); 53 | msg.setId(rootElement.getChildTextTrim("MsgId")); 54 | msg.setReceiver(rootElement.getChildTextTrim("ToUserName")); 55 | msg.setSender(rootElement.getChildTextTrim("FromUserName")); 56 | msg.setType(rootElement.getChildTextTrim("MsgType")); 57 | msg.setContent(rootElement.getChildTextTrim("Content")); 58 | String createTime = rootElement.getChildTextTrim("CreateTime"); 59 | if (createTime != null) { 60 | msg.setCreatedTime(new Date(Long.valueOf(createTime) * 1000)); 61 | } 62 | //event message 63 | if ("event".equals(msg.getType())) { 64 | String event = rootElement.getChildText("EventKey"); 65 | String eventKey = rootElement.getChildText("EventKey"); 66 | msg.setType(event); 67 | msg.setContent(eventKey); 68 | } 69 | return msg; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/rewrite/Conf.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.rewrite; 2 | 3 | import org.jdom.Document; 4 | import org.jdom.Element; 5 | import org.jdom.input.SAXBuilder; 6 | 7 | import javax.servlet.ServletContext; 8 | import java.io.InputStream; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * weixin robot configuration 14 | * 15 | * @author linux_china 16 | */ 17 | public class Conf { 18 | /** 19 | * rules 20 | */ 21 | private List rules = new ArrayList(); 22 | 23 | /** 24 | * construct method 25 | * 26 | * @param path path 27 | * @throws Exception exception 28 | */ 29 | @SuppressWarnings("unchecked") 30 | public Conf(ServletContext servletContext, String path) throws Exception { 31 | InputStream inputStream; 32 | if (path.startsWith("classpath:")) { 33 | inputStream = this.getClass().getResourceAsStream(path.replace("classpath:", "/")); 34 | } else { 35 | inputStream = servletContext.getResourceAsStream(path); 36 | } 37 | SAXBuilder saxBuilder = new SAXBuilder(); 38 | Document document = saxBuilder.build(inputStream); 39 | List ruleElements = document.getRootElement().getChildren("rule"); 40 | for (Element ruleElement : ruleElements) { 41 | rules.add(new NormalRule(ruleElement.getChildTextTrim("type"), ruleElement.getChildTextTrim("content"), (ruleElement.getChildTextTrim("forward")))); 42 | } 43 | } 44 | 45 | /** 46 | * get forward rules 47 | * 48 | * @return rules 49 | */ 50 | public List getRules() { 51 | return rules; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/rewrite/NormalRewrittenUrl.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.rewrite; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.http.HttpServletRequest; 5 | import javax.servlet.http.HttpServletResponse; 6 | import java.io.IOException; 7 | 8 | /** 9 | * normal rewrite url 10 | * 11 | * @author linux_china 12 | */ 13 | public class NormalRewrittenUrl implements RewrittenUrl { 14 | /** 15 | * target forward url 16 | */ 17 | private String target; 18 | 19 | /** 20 | * construct method 21 | * 22 | * @param target target url 23 | */ 24 | public NormalRewrittenUrl(String target) { 25 | this.target = target; 26 | } 27 | 28 | /** 29 | * get target url 30 | * 31 | * @return target url 32 | */ 33 | public String getTarget() { 34 | return this.target; 35 | } 36 | 37 | /** 38 | * execute rewrite 39 | * 40 | * @param request http servlet request 41 | * @param response http servlet response 42 | * @return rewrite mark 43 | * @throws ServletException servlet exception 44 | * @throws IOException IO exception 45 | */ 46 | public boolean doRewrite(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 47 | request.getRequestDispatcher(getTarget()).forward(request, response); 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/rewrite/NormalRule.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.rewrite; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | import org.mvnsearch.wx.WeixinMessage; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * normal rule 14 | * 15 | * @author linux_china 16 | */ 17 | public class NormalRule implements Rule { 18 | /** 19 | * type 20 | */ 21 | private String type; 22 | /** 23 | * content 24 | */ 25 | private String content; 26 | /** 27 | * regex pattern 28 | */ 29 | private Pattern regexPattern; 30 | /** 31 | * forward 32 | */ 33 | private String forward; 34 | 35 | /** 36 | * construct method 37 | * 38 | * @param type type 39 | * @param content content 40 | * @param forward forward 41 | */ 42 | public NormalRule(String type, String content, String forward) { 43 | this.type = type; 44 | this.content = content; 45 | if (content != null && !content.isEmpty()) { 46 | this.regexPattern = Pattern.compile(content); 47 | } 48 | this.forward = forward; 49 | } 50 | 51 | /** 52 | * match weixin message? 53 | * 54 | * @param wxMsg weixin message 55 | * @param request http servlet request 56 | * @param response http servlet response 57 | * @return rewritten url 58 | * @throws IOException io exception 59 | * @throws ServletException servlet exception 60 | */ 61 | @Nullable 62 | public RewrittenUrl matches(WeixinMessage wxMsg, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 63 | boolean matched = false; 64 | if (type.equals(wxMsg.getType())) { 65 | matched = regexPattern == null || regexPattern.matcher(wxMsg.getContent()).find(); 66 | } 67 | return matched ? new NormalRewrittenUrl(forward) : null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/rewrite/RewrittenUrl.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.rewrite; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.http.HttpServletRequest; 5 | import javax.servlet.http.HttpServletResponse; 6 | import java.io.IOException; 7 | 8 | /** 9 | * rewritten url 10 | * 11 | * @author linux_china 12 | */ 13 | public interface RewrittenUrl { 14 | /** 15 | * get target url 16 | * 17 | * @return target url 18 | */ 19 | public String getTarget(); 20 | 21 | /** 22 | * execute rewrite 23 | * 24 | * @param request http servlet request 25 | * @param response http servlet response 26 | * @return rewrite mark 27 | * @throws ServletException servlet exception 28 | * @throws IOException IO exception 29 | */ 30 | public boolean doRewrite(HttpServletRequest request, 31 | HttpServletResponse response) 32 | throws ServletException, IOException; 33 | } 34 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/rewrite/Rule.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.rewrite; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | import org.mvnsearch.wx.WeixinMessage; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | import java.lang.reflect.InvocationTargetException; 11 | 12 | /** 13 | * rewrite rule 14 | * 15 | * @author linux_china 16 | */ 17 | public interface Rule { 18 | /** 19 | * match weixin message? 20 | * 21 | * @param wxMsg weixin message 22 | * @param request http servlet request 23 | * @param response http servlet response 24 | * @return rewritten url 25 | * @throws IOException io exception 26 | * @throws ServletException servlet exception 27 | */ 28 | @Nullable 29 | public RewrittenUrl matches(WeixinMessage wxMsg, 30 | final HttpServletRequest request, 31 | final HttpServletResponse response) 32 | throws IOException, ServletException; 33 | } 34 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/rewrite/RuleChain.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.rewrite; 2 | 3 | import org.mvnsearch.wx.WeixinMessage; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.util.List; 11 | 12 | /** 13 | * rule chain 14 | * 15 | * @author linux_china 16 | */ 17 | public class RuleChain { 18 | /** 19 | * rules 20 | */ 21 | private List rules; 22 | /** 23 | * target 24 | */ 25 | private RewrittenUrl rewrittenUrl; 26 | 27 | /** 28 | * rule chain construct 29 | * 30 | * @param rules rules 31 | */ 32 | public RuleChain(List rules) { 33 | this.rules = rules; 34 | } 35 | 36 | /** 37 | * process weixin message 38 | * 39 | * @param wxMsg weixin message 40 | * @param request http servlet request 41 | * @param response response 42 | * @throws IOException IO Exception 43 | * @throws ServletException servlet exception 44 | * @throws InvocationTargetException invocation target exception 45 | */ 46 | public void process(WeixinMessage wxMsg, HttpServletRequest request, HttpServletResponse response) 47 | throws IOException, ServletException, InvocationTargetException { 48 | doRules(wxMsg, request, response); 49 | } 50 | 51 | /** 52 | * iterated match in rules 53 | * 54 | * @param wxMsg weixin message 55 | * @param request http servlet request 56 | * @param response http servlet response 57 | * @throws IOException IO Exception 58 | * @throws ServletException servlet exception 59 | */ 60 | public void doRules(WeixinMessage wxMsg, HttpServletRequest request, HttpServletResponse response) 61 | throws IOException, ServletException { 62 | for (Rule rule : rules) { 63 | RewrittenUrl rewrittenUrl = rule.matches(wxMsg, request, response); 64 | if (rewrittenUrl != null) { 65 | this.rewrittenUrl = rewrittenUrl; 66 | return; 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * get final rewritten request 73 | * 74 | * @return rewritten url 75 | */ 76 | public RewrittenUrl getFinalRewrittenRequest() { 77 | return this.rewrittenUrl; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/rewrite/UrlRewriter.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.rewrite; 2 | 3 | import org.mvnsearch.wx.WeixinMessage; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import java.lang.reflect.InvocationTargetException; 10 | 11 | /** 12 | * url rewriter 13 | * 14 | * @author linux_china 15 | */ 16 | public class UrlRewriter { 17 | /** 18 | * rewriter configuration 19 | */ 20 | private Conf conf; 21 | 22 | /** 23 | * construct method 24 | * 25 | * @param conf rewriter configuration 26 | */ 27 | public UrlRewriter(Conf conf) { 28 | this.conf = conf; 29 | } 30 | 31 | 32 | /** 33 | * Helpful for testing but otherwise, don't use. 34 | */ 35 | public RewrittenUrl processRequest(WeixinMessage wxMsg, 36 | final HttpServletRequest request, 37 | final HttpServletResponse response) 38 | throws IOException, ServletException, InvocationTargetException { 39 | RuleChain chain = getNewChain(request); 40 | chain.process(wxMsg, request, response); 41 | return chain.getFinalRewrittenRequest(); 42 | } 43 | 44 | /** 45 | * get new rule chain 46 | * 47 | * @param request http servlet request 48 | * @return rule chain 49 | */ 50 | private RuleChain getNewChain(final HttpServletRequest request) { 51 | return new RuleChain(conf.getRules()); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/servlet/WeixinMessageContext.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.servlet; 2 | 3 | import org.mvnsearch.wx.WeixinMessage; 4 | 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * weixin message context 11 | * 12 | * @author linux_china 13 | */ 14 | @SuppressWarnings("unchecked") 15 | public class WeixinMessageContext { 16 | /** 17 | * empty map 18 | */ 19 | private static Map emptyMap = Collections.emptyMap(); 20 | /** 21 | * thread local variable 22 | */ 23 | private final static ThreadLocal> threadLocal = new ThreadLocal>() { 24 | protected Map initialValue() { 25 | return new HashMap(); 26 | } 27 | }; 28 | 29 | /** 30 | * set query string 31 | * 32 | * @param queryString query string 33 | */ 34 | public static void setQueryString(String queryString) { 35 | if (queryString != null && queryString.contains("=")) { 36 | Map query = new HashMap(); 37 | for (String pair : queryString.split("&")) { 38 | String[] parts = pair.split("=", 2); 39 | if (parts.length == 2 && !parts[1].isEmpty()) { 40 | query.put(parts[0], parts[1]); 41 | } 42 | } 43 | threadLocal.get().put("query", query); 44 | } 45 | } 46 | 47 | /** 48 | * add query pair 49 | * 50 | * @param name name 51 | * @param value value 52 | */ 53 | public void addQuery(String name, String value) { 54 | Map query = (Map) threadLocal.get().get("query"); 55 | if (query == null) { 56 | query = new HashMap(); 57 | threadLocal.get().put("query", query); 58 | } 59 | query.put(name, value); 60 | } 61 | 62 | /** 63 | * get query 64 | * 65 | * @return query 66 | */ 67 | public static Map getQuery() { 68 | Map query = (Map) threadLocal.get().get("query"); 69 | return query == null ? emptyMap : query; 70 | } 71 | 72 | /** 73 | * set weixin message 74 | * 75 | * @param message message 76 | */ 77 | public static void setWeixinMessage(WeixinMessage message) { 78 | threadLocal.get().put("message", message); 79 | } 80 | 81 | /** 82 | * get weixin message 83 | * 84 | * @return weixin message 85 | */ 86 | public static WeixinMessage getWeixinMessage() { 87 | return (WeixinMessage) threadLocal.get().get("message"); 88 | } 89 | 90 | /** 91 | * clear context 92 | */ 93 | public static void clear() { 94 | threadLocal.remove(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /robot-sdk/src/main/java/org/mvnsearch/wx/servlet/WexinRobotServlet.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx.servlet; 2 | 3 | import org.apache.commons.codec.digest.DigestUtils; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.mvnsearch.wx.WeixinMessage; 6 | import org.mvnsearch.wx.WeixinUtils; 7 | import org.mvnsearch.wx.rewrite.Conf; 8 | import org.mvnsearch.wx.rewrite.RewrittenUrl; 9 | import org.mvnsearch.wx.rewrite.UrlRewriter; 10 | 11 | import javax.servlet.ServletConfig; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.http.HttpServlet; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | import java.io.PrintWriter; 18 | import java.util.*; 19 | 20 | /** 21 | * weixin robot servlet 22 | * 23 | * @author linux_china 24 | */ 25 | public class WexinRobotServlet extends HttpServlet { 26 | /** 27 | * token 28 | */ 29 | private String token; 30 | /** 31 | * pass code,用于调试,不执行签名检查 32 | */ 33 | private String passCode; 34 | /** 35 | * url rewriter 36 | */ 37 | private UrlRewriter urlRewriter; 38 | 39 | /** 40 | * init config 41 | * 42 | * @param config servlet config 43 | * @throws ServletException servlet exception 44 | */ 45 | public void init(ServletConfig config) throws ServletException { 46 | this.token = config.getInitParameter("token"); 47 | this.passCode = config.getInitParameter("passCode"); 48 | String confFile = config.getInitParameter("confFile"); 49 | try { 50 | Conf conf = new Conf(config.getServletContext(), confFile); 51 | this.urlRewriter = new UrlRewriter(conf); 52 | } catch (Exception e) { 53 | e.printStackTrace(); 54 | } 55 | } 56 | 57 | /** 58 | * 微信公共平台认证接口 59 | * 60 | * @param request http servlet request 61 | * @param response http servlet response 62 | * @throws ServletException servlet exception 63 | * @throws IOException io exception 64 | */ 65 | public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 66 | String echostr = request.getParameter("echostr"); 67 | response.setContentType("text/plain; charset=UTF-8"); 68 | PrintWriter out = response.getWriter(); 69 | if (checkSignature(request.getQueryString(), getToken())) { 70 | out.print(echostr); 71 | } else { 72 | out.print("false"); 73 | } 74 | out.flush(); 75 | } 76 | 77 | /** 78 | * 微信消息接收接口 79 | * 80 | * @param request http servlet request 81 | * @param response http servlet response 82 | * @throws ServletException servlet exception 83 | * @throws IOException io exception 84 | */ 85 | public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 86 | //validate signature 87 | boolean valid = checkSignature(request.getQueryString(), getToken()); 88 | if (valid) { 89 | try { 90 | WeixinMessage wxMsg = WeixinUtils.parseXML(request.getInputStream()); 91 | doMessage(wxMsg, request, response); 92 | } catch (Exception e) { 93 | response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 94 | } finally { 95 | WeixinMessageContext.clear(); 96 | } 97 | } else { 98 | send400(response); 99 | } 100 | } 101 | 102 | /** 103 | * 处理 weixin message 104 | * 105 | * @param wxMsg weixin message 106 | * @param request http servlet request 107 | * @param response http servlet response 108 | * @throws Exception exception 109 | */ 110 | public void doMessage(WeixinMessage wxMsg, HttpServletRequest request, HttpServletResponse response) throws Exception { 111 | WeixinMessageContext.setWeixinMessage(wxMsg); 112 | WeixinMessageContext.setQueryString(request.getQueryString()); 113 | response.setContentType("text/xml; charset=UTF-8"); 114 | request.setAttribute("wxMsg", wxMsg); 115 | RewrittenUrl rewrittenUrl = urlRewriter.processRequest(wxMsg, request, response); 116 | if (rewrittenUrl != null) { 117 | rewrittenUrl.doRewrite(request, response); 118 | } 119 | } 120 | 121 | /** 122 | * 验证签名 123 | * 124 | * @param queryString query string 125 | * @return valid mark 126 | */ 127 | public boolean checkSignature(String queryString, @Nullable String token) { 128 | if (token == null || token.isEmpty() || queryString == null) return false; 129 | //pass code check 130 | if (passCode != null && queryString.contains(passCode)) return true; 131 | Map params = new HashMap(); 132 | for (String pair : queryString.split("&")) { 133 | String[] temp = pair.split("=", 2); 134 | params.put(temp[0], temp[1]); 135 | } 136 | List elements = new ArrayList(); 137 | elements.add(params.get("timestamp")); 138 | elements.add(params.get("nonce")); 139 | elements.add(token); 140 | Collections.sort(elements); 141 | StringBuilder seed = new StringBuilder(); 142 | for (String link : elements) { 143 | seed.append(link); 144 | } 145 | return DigestUtils.sha1Hex(seed.toString()).equals(params.get("signature")); 146 | } 147 | 148 | /** 149 | * get weixin call back token 150 | * 151 | * @return token 152 | */ 153 | @Nullable 154 | protected String getToken() { 155 | return this.token; 156 | } 157 | 158 | /** 159 | * get query from query string 160 | * 161 | * @param request http servlet request 162 | * @return query object 163 | */ 164 | protected Map getQuery(HttpServletRequest request) { 165 | String queryString = request.getQueryString(); 166 | if (queryString == null || !queryString.contains("=")) { 167 | return Collections.emptyMap(); 168 | } 169 | Map query = new HashMap(); 170 | for (String pair : queryString.split("&")) { 171 | String[] parts = pair.split("=", 2); 172 | if (parts.length == 2 && !parts[1].isEmpty()) { 173 | query.put(parts[0], parts[1]); 174 | } 175 | } 176 | return query; 177 | } 178 | 179 | /** 180 | * send 400(bad request) response 181 | * 182 | * @param response response 183 | */ 184 | public void send400(HttpServletResponse response) { 185 | try { 186 | response.sendError(400); 187 | } catch (Exception ignore) { 188 | 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /robot-sdk/src/test/java/org/mvnsearch/wx/ConfTest.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx; 2 | 3 | import junit.framework.TestCase; 4 | import org.mvnsearch.wx.rewrite.Conf; 5 | import org.mvnsearch.wx.rewrite.Rule; 6 | 7 | /** 8 | * conf test 9 | * 10 | * @author linux_china 11 | */ 12 | public class ConfTest extends TestCase { 13 | 14 | /** 15 | * test to construct conf 16 | * 17 | * @throws Exception exception 18 | */ 19 | public void testConstructConf() throws Exception { 20 | Conf conf = new Conf(null, "classpath:weixin-router.xml"); 21 | for (Rule rule : conf.getRules()) { 22 | System.out.println(rule); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /robot-sdk/src/test/java/org/mvnsearch/wx/WeixinMessageTest.java: -------------------------------------------------------------------------------- 1 | package org.mvnsearch.wx; 2 | 3 | import junit.framework.TestCase; 4 | 5 | /** 6 | * weixin message test case 7 | * 8 | * @author linux_china 9 | */ 10 | public class WeixinMessageTest extends TestCase { 11 | 12 | /** 13 | * test to prse xml 14 | * 15 | * @throws Exception exception 16 | */ 17 | public void testParseXML() throws Exception { 18 | String msgXml = "\n" + 19 | "\n" + 20 | "\n" + 21 | "1365073034\n" + 22 | "\n" + 23 | "\n" + 24 | "5862944037781496076\n" + 25 | ""; 26 | WeixinMessage msg = WeixinUtils.parseXML(msgXml); 27 | System.out.println(msg.getResponseXml("text", "我能帮助你什么?")); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /robot-sdk/src/test/resources/weixin-robot-flow.puml: -------------------------------------------------------------------------------- 1 | @startdot 2 | digraph WeixRobotFlow { 3 | WeixinServer [style=rounded] 4 | WeixinRobotServlet [shape=box] 5 | WebFramework [shape=box] 6 | MessageRouter [shape=diamond] 7 | 8 | WeixinServer -> WeixinRobotServlet [style=bold,label="Message Post"]; 9 | WeixinRobotServlet -> MessageRouter [label="Regex Router"] 10 | MessageRouter -> WebFramework [label="Forward"] 11 | WebFramework -> WeixinServer [label="Response"] 12 | } 13 | @enddot -------------------------------------------------------------------------------- /robot-sdk/src/test/resources/weixin-router.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | text 7 | ^\d{13}$ 8 | /weixin.wx 9 | 10 | 11 | --------------------------------------------------------------------------------