最近又接触到了公众号开发,虽然用了新的框架,但是最基本的原理还是要知道的
本文主要对 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 {
listen 80;
server_name wx.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();
}
}
最终效果如下