Wen

Jersey实现微信公众号服务器

搭建微信公众号后台服务器这个话题虽然有点过时,但我在网上却并没找到用Jersey来做的,其实如果你喜欢用Java语言搭建后台服务的话Jersey是个不错的选择,本文就把用Java和Jersey搭建微信公众号后台的过程记录一下。

微信公众平台后台设置

首先,要有一个微信公众平台的帐号,我是注册的一个个人订阅号,功能虽然受限但也够了。打开微信平台后台并登录:mp.weixin.qq.com。在左侧栏的“开发”栏下找到“基本配置”,然后输入配置如下图:

这里的URL就是你将要搭建的服务的入口,本例中do.zhaowen.io指向我DigitalOcean的一个host,service/weixin就是我准备留给微信后端的入口地址。Token是首次验证时要用的密码,要记住,后面要在服务里再次用到。另外加密不是本文重点,这里就选明文传输了。

Jersey 实现首次验证

Jersey 基本配置

请先参考之前的文章《用Intellij和Tomcat实现RESTful服务(Jersey)并远程部署》完成Jersey的环境配置,这里不再重复,只说一点不同的地方。

之前那篇文章中的配置Servlet是在web/WEB-INF/web.xml中做的,其实可以用一个Java类来取代它 (参考Jersey
文档
,实现的效果是一样的,但我个人更喜欢这样配置)。那么web/WEB-INF/web.xml就可以是空的:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
</web-app>

然后新建一个java类,比如叫ApplicationConfig,继承Application,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.Set;
@ApplicationPath("/")
public class ApplicationConfig extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new java.util.HashSet<>();
resources.add(HelloWorld.class); // 把servlet类加进去
return resources;
}
}

首次认证

接下来就可以实现微信后台首次验证了。我已经认证过了,好像就无法再次认证所以这里没有截图了,不过说一下流程和代码。

微信的首次认证过程是,给在上一步指定的URL发送一个Get请求,里面包含四个参数 (引用自微信开发文档):

  • signature:微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
  • timestamp:时间戳
  • nonce:随机数
  • echostr:随机字符串

开发者通过检验signature对请求进行校验(文档提供校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。

既然知道了验证过程,就首先建立一个servlet class来处理微信服务器的请求,然后在其中完成校验过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Path("/weixin")
public class Wechat {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String initialAuthenticate(@Context UriInfo uriInfo) {
// 获取微信服务器请求内容
Map<String, String> map = SignUtil.getQueryMap(uriInfo.getRequestUri().getQuery());
String signature = map.get("signature");
String timestamp = map.get("timestamp");
String nonce = map.get("nonce");
String echostr = map.get("echostr");
// 我们认证该请求是否来自微信服务器
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
return echostr;
}
return null;
}
}

@Path("/weixin")是对应于在微信后台设置的URL地址。这个函数首先获取微信服务器发来的请求内容,把四个参数的值存在一个map中,然后自己校验得知该请求是否来自微信服务器,如果是,则返回echostr参数内容。其中获取参数和校验的实现都在SignUtil这个类里,这里就不贴代码了,具体请下载整个项目的源代码:JerseyDemo

另外记得把这个类添加进刚刚创建的ApplicationConfig中,不然/weixin这个path是会返回404的。

重点来了,这个过程听起来并不复杂,但实现的过程中可能会遇到各种问题,这个时候debug就很重要了,但我在微信官网上找了半天也没找到微信服务器发过来的具体请求是什么,后来搞了半天终于发现它大概是这个样子:

http://host/weixin?signature=abcdefg&echostr=1234567&timestamp=1486104762&nonce=751213921

再配合《用Intellij和Tomcat实现RESTful服务(Jersey)并远程部署》中讲的远程调试,认证成功也不是难事。

Jersey 实现文本返回

我们自己搭建的后台要实现的一个最基本的功能,就是在用户给我们的公众号发信息的时候,我们的公众号能自动回复用户,这个流程是这样的:

1
2
3
4
5
用户微信账户 发送消息 给公众号 -> 微信服务器 -> 我们服务器
|
| 处理后产生response
|
公众号 发送消息给 用户微信账户 <- 微信服务器 <- 我们服务器

接收消息

根据微信开发文档,在接收普通消息的说明中,当用户给公众号发文本消息时,微信服务器发给我们服务器的request是这样的:

1
2
3
4
5
6
7
8
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>

这个格式真的很蛋疼,首先它不是标准的xml,如果是的话可以用Jersey直接把request解析成Java Object,现在还得手动解析。另外它不用json格式,json比xml解析更快。

Anyway,微信会向我们提供的URL发送一个POST请求,请求的Body就是上面的内容,我们需要手动解析出各个参数的值。下面是我的实现:

1
2
3
4
5
6
@POST
@Produces(MediaType.TEXT_PLAIN)
public String reply(@Context UriInfo uriInfo, String rawRequest) {
Map<String, String> map = XmlHelperUtils.toMap(rawRequest);
...
}

XmlHelperUtils.toMap(rawRequest)实现的就是手动解析请求,然后把请求中的各个参数存到一个map中,实现细节请参照源代码。这里再提一下debug中用到的Tool:

发送响应

根据微信开发文档,在被动回复用户消息的说明中,我们服务器发给微信服务器的格式必须是这样的:

1
2
3
4
5
6
7
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
</xml>

基本意思就是,返回上面这个string,只不过把里面的参数值填好,需要注意的是,这里toUser就是我们收到的request里的FromUserName,而fromUser就是request里的ToUserName。下面是一个简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
@POST
@Produces(MediaType.TEXT_PLAIN)
public String reply(@Context UriInfo uriInfo, String rawRequest) {
Map<String, String> map = XmlHelperUtils.toMap(rawRequest);
String response = "<xml>\n" +
"<ToUserName><![CDATA["+map.get("FromUserName")+"]]></ToUserName>\n" +
"<FromUserName><![CDATA["+map.get("ToUserName")+"]]></FromUserName>\n" +
"<CreateTime>"+Long.toString(System.currentTimeMillis() / 1000)+"</CreateTime>\n" +
"<MsgType><![CDATA[text]]></MsgType>\n" +
"<Content><![CDATA["+"Hello Wechat!"+"]]></Content>\n" +
"</xml>";
return response;
}

如果一切顺利的话,部署后向微信公众号发送任何消息,都可以收到微信公众号返回的"Hello Wechat!"

有时会遇到代码部署后无效的情况,不要慌,一般follow以下步骤都可以解决:

  • 打开本地的Intellij,删除项目中的out文件夹
  • 登录远程服务器,删除之前部署的文件夹,比如webapps下的文件夹
  • 重新在本地用Intellij编译并部署至远程服务器

项目源代码:JerseyDemo
转载请注明出处:http://zhaowen.io/post/wechat_backend_jersey/