目录

  • 前言
  • 技术栈及工具
  • 一、后台配置
  • 应用回调配置
  • 通用开发参数回调配置
  • 二、构建spring-boot项目
  • 新建项目
  • 项目目录结构
  • 导入加解密包
  • 写验证类
  • 1、回调验证
  • 2、获取suite_ticket及auth_code
  • 总结


前言

  1. 我们在新建好一个网页应用后,需要填一些基础配置。其中最重要的就是一些回调配置,回调路径指向我们自己的服务器,需要正确接收响应微信服务器的请求。
  2. 微信接口开发无非就是通过一些带token的http请求来实现相关功能,而获取这些token往往需要先获取一些重要参数,这里有两个参数需要先获取:suite_ticketauth_code
  3. 对微信开发不熟,或者没读我之前文章的同学一定要搞清一个概念:在三方应用开发中,应用开发方统称三方服务商,对应id我会称 【服务商corpId】;应用使用方称作 企业,对应id叫 【企业corpId】。请不要搞混了

技术栈及工具

  • 开发框架:spring-boot
  • 开发工具: idea

一、后台配置

应用回调配置

首先我们进入新建的应用:

企业微信回调api java 企业微信回调地址_微信


其中最主要的配置就是这个回调配置:

企业微信回调api java 企业微信回调地址_微信_02

共有四个配置项,分别是:

  • 两个回调URL
  • Token(密钥,不可泄漏)
  • EncodingAESKey(加密消息内容的码)

这里的URL就是我们服务器的响应路径,微信服务器会根据这里的URL向我们服务器发送请求,我们服务器验证方面需要做两件事:

  • 分辨出是否为企业微信来源
  • 分辨出推送消息的内容是否被篡改

企业微信回调api java 企业微信回调地址_企业微信回调api java_03

填写好回调URL,并选择自动生成Token和EncodingAESKey

简单一句话概括就是,在某些时点(具体什么时点或访问哪个回调请看下面 “写VerifyController类” 内容),微信服务器会向我们服务器发送验证请求,我们服务器需要通过Token、EncodingAESKey和服务商CorpID计算Signature与GET请求参数中的Signature作对比,如果一样就验证通过,返回"success"。

通用开发参数回调配置

还有一个重要参数需要设置,就是 通用开发参数 里的 系统事件接收URL

企业微信回调api java 企业微信回调地址_java_04

企业 安装应用时,微信服务器会向这个 系统事件接收URL 发送验证请求,我们需要做的响应跟上面的应用回调验证基本一样。 所以我将两者URL、Token、EncodingAESKey设为一样,通过同一个接口来处理这两种请求。

由于后台还未搭建,这些设置都保存不了,接下来我们先搭建后台。

二、构建spring-boot项目

Spring Boot的出现简直就是像我这样的后端渣渣的福音,我们可以很快速的构建一个服务系统。所以我很愉快的选择了它!

还有很重要的一点,本系列文重在打通企业微信的一些功能,以及验证一些逻辑。代码尽量简洁。所以我暂时不会遵循实战项目的设计逻辑,也缺少一些排错处理。请读者知悉!

新建项目

新建一个项目:cwp(company wechat project之意)

并新建一些必要的包和类,具体目录结构如下:

企业微信回调api java 企业微信回调地址_java_05

项目目录结构

  • 新建controller目录:这里暂时不考虑打通数据库,接收请求和数据处理全在controller完成。
  • 新建confg目录:存放Constant.java——公用参数。
  • 新建 data.properties :通过controller获得的数据存在此文件中。
  • 新建 util 目录:存放 PropertiesUtil.java ——此类作用是动态向data.properties写入内容。存放 WxUtil.java——此类存放xml和map互转等方法。

导入加解密包

还记的之前说过的企业微信给的加密计算方法实例吗?

微信准备了xml和json两种解密方式。这里选择Java的xml方式。将验证包中的类全部考入 wechataes包 中:

企业微信回调api java 企业微信回调地址_java_06


同时pom.xml中需要引入commons.codec包

<!-- commons-codec-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>

写验证类

此类有两个方法:

  • doGetValid : 接收验证请求,用于验证通用开发参数系统事件接收URL、数据回调URL、指令回调URL。当企业微信后台录入回调URL点击保存时,微信服务器会立即发送一条GET请求到对应URL,该函数就对URL的signature进行验证。
  • doPostValid: 用于获取 suite_ticket ,以及安装应用时传递过来的 auth_code ,还有用户从企业微信打开应用时也会调用此函数。解密包的第三个参数需做区分:当刷新ticket和安装应用时传递 【SuitID】 ;当打开应用时传递 【CorpID

再来捋一下,数据回调和指令回调除了保存配置时被访问,还有哪些情况会被微信服务器访问:

  • 数据回调URL: 当每次从企业微信打开应用时。【doPostValid 中的解密参数传递CorpID】
  • 指令回调URL: 微信服务器推送suite_ticket以及安装应用时推送auth_code时。【doPostValid 中的解密参数传递SuitID】

1、回调验证

doGetValid方法如下:

package com.tan.cwp.controller;

import com.tan.cwp.config.Constant;
import com.tan.cwp.wechataes.WXBizMsgCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@RestController
@RequestMapping("/verify")
public class VerifyController {
    Logger logger = LoggerFactory.getLogger(VerifyController.class);

    /*
     * 验证通用开发参数及应用回调
     */
    @RequestMapping(value = "callback_verify.do" ,method = RequestMethod.GET)
    public void doGetValid(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 微信加密签名
        String msg_signature = request.getParameter("msg_signature");
        // 时间戳
        String timestamp = request.getParameter("timestamp");
        // 随机数
        String nonce = request.getParameter("nonce");
        // 随机字符串
        // 如果是刷新,需返回原echostr
        String echostr = request.getParameter("echostr");

        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Constant.TOKEN, Constant.EncodingAESKey, Constant.CorpID);

        String sEchoStr=""; //需要返回的明文
        PrintWriter out;
        try {
            sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp,
                    nonce, echostr);
            logger.info("verifyurl echostr: " + sEchoStr);

            // 验证URL成功,将sEchoStr返回
            out = response.getWriter();
            out.print(sEchoStr);
        } catch (Exception e) {
            //验证URL失败,错误原因请查看异常
            e.printStackTrace();
        }
    }
}

在后台录入好接口URL保存时,微信后台即会向该接口发送GET请求,并携带四个参数:msg_signature、timestamp、nonce、echostr。

通过 request.getParameter(xxx)获取到他们。

还需要从企业微信后台获取三个固定参数用于计算singnature:TOKEN、EncodingAESKey、CorpID

我把固定参数都放在 配置类Constant 中:

package com.tan.cwp.config;

public class Constant {
    // 服务商相关
    /**
     * 服务商CorpID
     */
    public static final String CorpID = "xxxx";
    /**
     * 服务商身份的调用凭证
     */
    public static final String ProviderSecret = "xxxx";

    // 应用相关
    /**
     * 应用的唯一身份标识
     */
    public static final String SuiteID = "xxxx";
    /**
     * 应用的调用身份密钥
     */
    public static final String SuiteSecret = "xxxx";

    // 回调相关
    /**
     * 回调/通用开发参数Token, 两者解密算法一样,所以为方便设为一样
     */
    public static final String TOKEN = "xxxx";

    /**
     * 回调/通用开发参数EncodingAESKey, 两者解密算法一样,所以为方便设为一样
     */
    public static final String EncodingAESKey = "xxxx";

}

有了这些重要参数就可以通过企业微信准备的包进行验证了。运行项目,并点击申请验证:

企业微信回调api java 企业微信回调地址_微信_07


都成功返回 echostr,并显示已验证:

企业微信回调api java 企业微信回调地址_服务器_08


企业微信回调api java 企业微信回调地址_服务器_09


通用参数也同样验证成功:

企业微信回调api java 企业微信回调地址_微信_10

2、获取suite_ticket及auth_code

  • suite_ticket: 是用于获取 第三方应用凭证(suite_access_token) 的重要参数,由企业微信后台每过十分钟推送一次给 “指令回调URL”
  • auth_code: 是用于获取企业临时授权码的重要参数,由企业安装应用成功时返回

两者获取方式完全相同,微信服务器会通过POST请求向我们服务器传递:msg_signature,timestamp,nonce,echostr四个参数 ,同样通过 request.getParameter(xxx)获取。

同时还会传递一个加密过的xml格式的请求体,通过输入流方式获取该参数,并通过企业微信的验证包进行解密。

具体解密过程查看 doPostValid 方法:

package com.tan.cwp.controller;

import com.tan.cwp.config.Constant;
import com.tan.cwp.util.PropertiesUtil;
import com.tan.cwp.util.WxUtil;
import com.tan.cwp.wechataes.WXBizMsgCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Map;

@RestController
@RequestMapping("/verify")
public class VerifyController {
    Logger logger = LoggerFactory.getLogger(VerifyController.class);

    /**
     * 验证通用开发参数
     */
    @RequestMapping(value = "callback_verify.do" ,method = RequestMethod.GET)
    public void doGetValid(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 微信加密签名
        String msg_signature = request.getParameter("msg_signature");
        // 时间戳
        String timestamp = request.getParameter("timestamp");
        // 随机数
        String nonce = request.getParameter("nonce");
        // 随机字符串
        // 如果是刷新,需返回原echostr
        String echostr = request.getParameter("echostr");

        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Constant.TOKEN, Constant.EncodingAESKey, Constant.CorpID);

        String sEchoStr=""; //需要返回的明文
        PrintWriter out;
        try {
            sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp,
                    nonce, echostr);

            // 验证URL成功,将sEchoStr返回
            out = response.getWriter();
            out.print(sEchoStr);
        } catch (Exception e) {
            //验证URL失败,错误原因请查看异常
            e.printStackTrace();
        }
    }

    /**
     * 刷新 ticket
     */
    @RequestMapping(value = "callback_verify.do" ,method = RequestMethod.POST)
    public String doPostValid(HttpServletRequest request) throws Exception {

        // 微信加密签名
        String msg_signature = request.getParameter("msg_signature");
        // 时间戳
        String timestamp = request.getParameter("timestamp");
        // 随机数
        String nonce = request.getParameter("nonce");

        String type = request.getParameter("type");

        String id = "";

        // 访问应用和企业回调传不同的ID
        if(type.equals("data")){
            id = Constant.CorpID;
        } else {
            id = Constant.SuiteID;
        }



        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Constant.TOKEN, Constant.EncodingAESKey, id);

        String postData="";   // 密文,对应POST请求的数据
        //1.获取加密的请求消息:使用输入流获得加密请求消息postData
        ServletInputStream in = request.getInputStream();
        BufferedReader reader =new BufferedReader(new InputStreamReader(in));

        String tempStr="";   //作为输出字符串的临时串,用于判断是否读取完毕
        while(null!=(tempStr=reader.readLine())){
            postData+=tempStr;
        }

        String suiteXml=wxcpt.DecryptMsg( msg_signature, timestamp, nonce, postData);
        logger.info("suiteXml: " + suiteXml);

        Map suiteMap = WxUtil.transferXmlToMap(suiteXml);
        if(suiteMap.get("SuiteTicket") != null) {
            PropertiesUtil.setProperty("suite_ticket", (String) suiteMap.get("SuiteTicket"));
        } else if(suiteMap.get("AuthCode") != null){
            PropertiesUtil.setProperty("auth_code", (String) suiteMap.get("AuthCode"));
        }

        String success = "success";
        return success;
    }


}

注意这段:

企业微信回调api java 企业微信回调地址_服务器_11


我们在数据回调URL加个参数用于判断是用corpid还是suiteid:

企业微信回调api java 企业微信回调地址_java_12


最终解密过后都会获取到一个字符串类型的xml,我们用 WxUtil类 中的 parseXml方法 将string格式转为map格式:

package com.tan.cwp.util;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class WxUtil {
    /**
     * 将 Map 转化为 XML
     *
     * @param map
     * @return
     */
    public static String transferMapToXml(SortedMap<String, Object> map) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        for (String key : map.keySet()) {
            sb.append("<").append(key).append(">")
                    .append(map.get(key))
                    .append("</").append(key).append(">");
        }
        return sb.append("</xml>").toString();
    }

    /**
     * 将 XML 转化为 map
     *
     * @param strxml
     * @return
     * @throws IOException
     */
    public static Map transferXmlToMap(String strxml) throws IOException {
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
        if (null == strxml || "".equals(strxml)) {
            return null;
        }
        Map m = new HashMap();
        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
        SAXBuilder builder = new SAXBuilder();
        Document doc = null;
        try {
            doc = builder.build(in);
        } catch (JDOMException e) {
            throw new IOException(e.getMessage()); // 统一转化为 IO 异常输出
        }
        // 解析 DOM
        Element root = doc.getRootElement();
        List list = root.getChildren();
        Iterator it = list.iterator();
        while (it.hasNext()) {
            Element e = (Element) it.next();
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
            if (children.isEmpty()) {
                v = e.getTextNormalize();
            } else {
                v = getChildrenText(children);
            }
            m.put(k, v);
        }
        //关闭流
        in.close();
        return m;
    }

    // 辅助 transferXmlToMap 方法递归提取子节点数据
    private static String getChildrenText(List<Element> children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator<Element> it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List<Element> list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        return sb.toString();
    }
}

里面用到了dom4J,需要在pom.xml中引入:

<!--XML 解析包-->
        <dependency>
            <groupId>org.jdom</groupId>
            <artifactId>jdom2</artifactId>
            <version>2.0.6</version>
        </dependency>

由于并不急着打通数据库,这里通过 PropertiesUtil类 动态的读取和写入获取到的参数到 data.properties中:

package com.tan.cwp.util;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

public class PropertiesUtil {
    private static Logger logger = LoggerFactory.getLogger(PropertiesUtil.class);

    private static Properties props;
    private static String fileName = "data.properties";
    private static String path ="D:/Nobug/WeChat/backend/cwp/src/main/resources/data.properties";
    static {
        props = new Properties();
        try {
            props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
        } catch (IOException e) {
            logger.error("配置文件读取异常",e);
        }
    }
    
    // 读取参数
    public static String getProperty(String key){
        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            return null;
        }
        return value.trim();
    }

    // 写入参数
    public static String setProperty(String key, String value) throws IOException {
        props.setProperty(key, value);
        FileOutputStream file = new FileOutputStream(path);
        props.store(file,"refresh");
        return null;
    }

}

pom.xml需要引入commons-lang3

<!--StringUtils-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>

记住要先在data.properties中放好suite_ticket和auth_code:

企业微信回调api java 企业微信回调地址_java_13


正确获取到参数后,还需要返回success给微信服务器(注意要将"success"字符串复制给一个变量返回,这是一个坑!)再次运行项目,我们先获取suite_ticket:

企业微信回调api java 企业微信回调地址_企业微信回调api java_14


写入成功:

企业微信回调api java 企业微信回调地址_企业微信回调api java_15


注意!!以下安装测试需先通过授权才能安装。授权方法参考下一章[企业微信三方开发(二)的授权配置一节](),建议把下一章全部调通再回过头来测试安装应用!我们再试下安装测试:

企业微信回调api java 企业微信回调地址_微信_16


企业微信回调api java 企业微信回调地址_服务器_17

通过企业微信扫码安装后,显示安装成功:

企业微信回调api java 企业微信回调地址_微信_18


AuthCode也写入:

企业微信回调api java 企业微信回调地址_企业微信回调api java_19

总结

在第三方应用开发提供的接口中,主要围绕三种类型的access_token进行开发:

  • provider_access_token 服务商的token
  • suite_access_token 第三方应用的token
  • access_token 授权企业的token

通过今天的配置和获取到SuiteTicketAuthCode后,接下来要做的就是通过一系列HTTP请求来愉快获取这些token!

2020/10/7重要更正:

企业微信回调api java 企业微信回调地址_java_20


今天在测试数据回调时发现数据回调后台提示corpid验证失败。原来逻辑是实例化WXBizMsgCrypt类时传递的corpid为服务商的id。通过测试发现要用安装企业的id才行,赶紧回看文档果然有变化。等于说企业微信服务器在访问数据回调url时都会携带上访问的企业id,后台在处理时将corpid取出用作参数就行了!

企业微信回调api java 企业微信回调地址_服务器_21


首先将数据回调URL加上 &corpid=$CORPID$ ,微信服务器会自动将 $CORPID$ 的内容替换为该企业的corpid,我们后台直接获取该参数就行了。

企业微信回调api java 企业微信回调地址_服务器_22