TMaize Blog

微信公众号开发

最近又接触到了公众号开发,虽然用了新的框架,但是最基本的原理还是要知道的

本文主要对 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);
    }
}

01

至此我们的域名已经通过微信的验证

获取 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();
    }
}

最终效果如下

02

参考

微信公众平台技术文档