将内网地址映射到外网

配置文件的写法:

server_addr: "server.ngrok.cc:4443"
auth_token: "abfdc23163c424a7956d0543fe53b4e2" 
tunnels:
  sunny:
   subdomain: "liweismile"
   proto:
    http: 8088

其中,token 是登录平台以后可以查询到的;subdomain 是登录平台以后自己添加的。
启动命令:ngrok.exe -config ngrok.cfg start sunny

映射的新网址:
http://liweismile.ngrok.cc

好几个月没有做微信开发了,发现 ngrok 这个工具已经不好用了。所以就换用 nat123 。使用 nat123 要注意一点:免费版本的只支持 80 端口的映射。如图:
这里写图片描述

所以,我们要将自己的应用配置成 80 端口可以访问的应用就可以了。

首先要明白一点,我们的应用要和微信服务器交互,一定要让我们的应用在公网上能够访问,这就须要我们的映射工具了。

工具:ngrok
使用方法非常简单。
命令行:

在 cmd 命令中先切换到 ngrok 所在的位置再进行如下操作

ngrok -config ngrok.cfg -subdomain example 8080

说明:example:自己任意设置;8080:tomcat的端口号。

我的实践:

ngrok -config ngrok.cfg -subdomain liwei 8080

命令行工具非常直观地显示出映射关系:
这里写图片描述

开发模式接入

第一步:填写服务器配置

所谓开发模式的接入的场景是这样的:当我们在微信窗口中输入一些信息的时候,微信服务器会将我们在窗口里输入的消息转发到我们映射在公网的应用上,这样,我们就可以实现自己的逻辑。
这里写图片描述

这一部分详细信息,可以查看【公众平台开发者文档】【开发者必读】【接入指南】。

第二步:验证服务器地址的有效性

以下截图来源于文档。

这里写图片描述

Java 示例代码:

// 第 1 步,我们测试接入,这一部分请参考微信的 《接入指南》
    // 由于我的项目是一个 springboot 项目,因此我启动服务器以后是不用输入项目根路径的
    // 在这里,我们要将 http://liwei.tunnel.mobi/weixin 这个地址映射出去
    // 端口映射工具有花生壳、ngrok(tunnel)
    @RequestMapping(value = "/weixin", method = RequestMethod.GET)
    public void weixin(HttpServletRequest request, HttpServletResponse response) throws IOException {
        PrintWriter out = response.getWriter();
        String signature = request.getParameter("signature");
        String timestamp = request.getParameter("timestamp");
        String nonce = request.getParameter("nonce");
        String echostr = request.getParameter("echostr");
        logger.debug("参数-微信加密签名 signature:" + signature);
        logger.debug("参数-时间戳 timestamp:" + timestamp);
        logger.debug("参数-随机数 nonce:" + nonce);
        logger.debug("参数-随机字符串 echostr:" + echostr);
        // 这里须要校验这些参数是否合法,即是否来自微信公众号
        boolean r = CheckUtil.checkSignature(signature, timestamp, nonce);
        if (r) {
            System.out.println("接入验证通过。");
            // 如果验证通过,将随机字符串返回
            out.print(echostr);
        }
    }

注意事项:我们这里使用了 SpringMVC 框架开发,在这个控制器方法里,我们一定要使用 void 作为返回值,并且将返回给服务器的信息使用 PrintWriter 这个类输出回去。

这里我们还使用到一个校验工具类,代码如下:

public class CheckUtil {

    private static Logger logger = Logger.getLogger(CheckUtil.class);

    /**
     * 该 token 值要和微信公众号那个网页里面的 token 一致,否则校验会出错
     */
    public static final String TOKEN = "liwei";

    public static boolean checkSignature(String signature, String timestamp,
            String nonce) {

        String[] arr = new String[] { TOKEN, timestamp, nonce };
        logger.debug("排序之前的数组:" + arr);
        // 按自然顺序排序
        Arrays.sort(arr);
        logger.debug("排序以后的数组:" + arr);

        // 生成字符串
        StringBuffer content = new StringBuffer();
        for (int i = 0; i < arr.length; i++) {
            content.append(arr[i]);
        }

        // sha1 加密
        String temp = getSha1(content.toString());
        return temp.equals(signature);
    }

    private static String getSha1(String str) {
        if (str == null || str.length() == 0) {
            return null;
        }
        char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                'a', 'b', 'c', 'd', 'e', 'f' };
        try {
            MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
            mdTemp.update(str.getBytes("UTF-8"));
            byte[] md = mdTemp.digest();
            int j = md.length;
            char[] buf = new char[j * 2];
            int k = 0;
            for (int i = 0; i < j; i++) {
                byte byte0 = md[i];
                buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
                buf[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(buf);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

}

然后,我们启动服务器,更改一下开发者中心的配置项,微信服务器就会向我们的应用发送一个 GET 请求,在我们上面的代码中,我设置了日志,会看到日志信息。

这里写图片描述

接收并且处理文本消息

我们写一个和配置的 URL 相同的控制器方法,注意,这里是 post 请求。请看下面的示例代码:

基本思路:我们接受的是一个 xml 格式的文本消息,然后我们还要返回一个 xml 格式的文本消息给微信服务器。

注意:和上面一样,我们写的控制器方法返回值是 void ,向微信服务器输出的信息使用 PrintWriter。

// 第 2 步,我们接收一个文本消息
    // 步骤是这样的:当我们向公众号发送一个消息的时候,微信公众号的服务器,会将
    // 我们发送的数据以 post 的方式转发到我们自己的服务器上
    // 数据交换的格式是 XML
    @RequestMapping(value = "/weixin", method = RequestMethod.POST)
    public void receivingText(HttpServletRequest request, HttpServletResponse response)
            throws IOException, DocumentException {
        // 将请求、响应的编码均设置为UTF-8(防止中文乱码)
        // request.setCharacterEncoding("UTF-8");
        // response.setCharacterEncoding("UTF-8");
        Map<String, String> map = MessageUtil.xmlToMap(request);
        String ToUserName = map.get("ToUserName");
        String FromUserName = map.get("FromUserName");
        String CreateTime = map.get("CreateTime");
        String MsgType = map.get("MsgType");
        String Content = map.get("Content");
        // 以上是接收消息

        /**
         * 下面我们要封装一个 TextMessage 对象 以 XML 字符串的方式转发到用户的手机界面
         */
        TextMessage text = null;
        if ("text".endsWith(MsgType)) {
            text = new TextMessage();
            text.setToUserName(FromUserName);
            text.setFromUserName(ToUserName);
            text.setCreateTime(CreateTime);
            text.setMsgType(MsgType);
            text.setContent("您发送的消息是:" + Content + ",该消息由威威猫的服务器处理。");
        }
        String strXML = MessageUtil.textMessageToXML(text);
        /*
         * String strXML = "<xml><ToUserName><![CDATA[" + FromUserName
         * +"]]></ToUserName><FromUserName><![CDATA[" + ToUserName +"]]>" +
         * "</FromUserName>" + "<CreateTime>" + CreateTime +
         * "</CreateTime><MsgType><![CDATA[" + MsgType +
         * "]]></MsgType><Content><![CDATA[" + Content +"]]></Content></xml>";
         * System.out.println(strXML);
         */

        logger.debug(strXML);
        // 响应消息
        PrintWriter out = response.getWriter();
        out.print(strXML);
    }

这里使用到一个工具类:

public class MessageUtil {

    /**
     * 将 XML 转换为 Map
     * 
     * @param request
     * @return
     * @throws IOException
     * @throws DocumentException
     */
    @SuppressWarnings("unchecked")
    public static Map<String, String> xmlToMap(HttpServletRequest request)
            throws IOException, DocumentException {
        Map<String, String> map = new HashMap<>();
        SAXReader reader = new SAXReader();
        InputStream ins = request.getInputStream();
        Document doc = reader.read(ins);
        Element root = doc.getRootElement();
        List<Element> list = root.elements();
        for (Element e : list) {
            map.put(e.getName(), e.getText());
        }
        ins.close();
        return map;
    }

    /**
     * 将文本消息(TextMessage 对象)转换为 xml 格式
     * 
     * @param textMessage
     * @return
     */
    public static String textMessageToXML(TextMessage textMessage) {
        XStream xtream = new XStream();
        xtream.alias("xml", textMessage.getClass());
        String xmlStr = xtream.toXML(textMessage);
        return xmlStr;
    }
}

实现效果和控制台:

这里写图片描述

这里写图片描述


在这篇文章的最后,我们再介绍一种加密的写法:

思路和第 1 种一样,先把 TOKEN, timestamp, nonce 进行数组排序,然后 sha1 加密,将加密以后的字符串和微信传给我们的 signature 进行字符串的比较。

public static boolean checkSignature2(String signature, String timestamp, String nonce) {
        String[] arr = new String[] { TOKEN, timestamp, nonce };
        Arrays.sort(arr);
        StringBuffer sb = new StringBuffer();
        for (String str : arr) {
            sb.append(str);
        }
        String sha1Str = sha1(sb.toString());
        System.out.println("我们自己的加密:" + sha1Str);
        System.out.println("微信帮我们加密的字符串:" + signature);

        return sha1Str.endsWith(signature)  ;
    }

我们今天这里关注 2 点:
(1) sha1 加密工具类的 API;
(2) String.format 将十进制转换为十六进制;

    public static String sha1(String str) {
        StringBuffer sb = new StringBuffer();
        try {
            MessageDigest md = MessageDigest.getInstance("sha1");
            md.update(str.getBytes());
            byte[] msg = md.digest();
            // 将 十进制转换为 十六进制
            for (byte b : msg) {
                sb.append(String.format("%02x", b));
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }