企业微信推送suite_ticket对接,由于微信文档不详细,很多地方还有错误,所以对接的时候很是痛苦。通过查阅各种文档,加上整合demo才最终对接成功,拿到了suite_ticket。

推送suite_ticket的文档说是一个POST接口,其实还有一个验证的GET接口,而且需要URL一致的。比如我的接口都是“/suite/receive”。

下面接对接通过代码展示,首先是对接参数:

private final String sToken = "Token";
    private final String sCorpID = "CorpID";
    private final String suiteID = "SuiteID";
    private final String sEncodingAESKey = "EncodingAESKey";

这些参数都是可以通过企业微信管理后台拿到的,比如

zabbix对接企业微信 企业微信 对接_zabbix对接企业微信


然后是企业微信参数解析类

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;

/**
 * 描述:微信参数解密工具
 *
 * @author 罗锅
 * @date 2021/9/2 11:44
 */
public class WXBizMsgCrypt {
    byte[] aesKey;
    String token;
    String receiveId;

    /**
     * 构造函数
     *
     * @param token          企业微信后台,开发者设置的token
     * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
     * @param receiveId,     不同场景含义不同,详见文档
     */
    public WXBizMsgCrypt(String token, String encodingAesKey, String receiveId) {
        this.token = token;
        this.receiveId = receiveId;
        aesKey = Base64.getDecoder().decode(encodingAesKey + "=");
    }

    /**
     * 验证并获取解密后数据
     *
     * @param msgSignature 签名串,对应URL参数的msg_signature
     * @param timeStamp    时间戳,对应URL参数的timestamp
     * @param nonce        随机串,对应URL参数的nonce
     * @param echoStr      随机串,对应URL参数的echostr
     * @return 解密之后的echoString
     * @throws Exception 执行失败,请查看该异常的错误码和具体的错误信息
     */
    public String verifyAndGetData(String msgSignature, String timeStamp, String nonce, String echoStr)
            throws Exception {
        String signature = getSignature(token, timeStamp, nonce, echoStr);

        if (!signature.equals(msgSignature)) {
            throw new Exception("参数验签不通过");
        }

        return decrypt(echoStr);
    }

    /**
     * 获取参数签名
     *
     * @param token
     * @param timestamp
     * @param nonce
     * @param encrypt
     * @return
     * @throws Exception
     */
    private String getSignature(String token, String timestamp, String nonce, String encrypt) throws Exception {
        String[] array = new String[]{token, timestamp, nonce, encrypt};
        StringBuilder sb = new StringBuilder();
        // 字符串排序
        Arrays.sort(array);
        for (String s : array) {
            sb.append(s);
        }
        String str = sb.toString();
        // SHA1签名生成
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(str.getBytes());
        byte[] digest = md.digest();

        StringBuilder hexStringBuilder = new StringBuilder();
        String shaHex;
        for (byte b : digest) {
            shaHex = Integer.toHexString(b & 0xFF);
            if (shaHex.length() < 2) {
                hexStringBuilder.append(0);
            }
            hexStringBuilder.append(shaHex);
        }
        return hexStringBuilder.toString();
    }

    /**
     * 对密文进行解密.
     *
     * @param text 需要解密的密文
     * @return 解密得到的明文
     * @throws Exception aes解密失败
     */
    private String decrypt(String text) throws Exception {
        // 设置解密模式为AES的CBC模式
        Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
        SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);

        // 使用BASE64对密文进行解码
        byte[] encrypted = Base64.getDecoder().decode(text);
        // 解密
        byte[] original = cipher.doFinal(encrypted);
        // 去除补位字符
        byte[] bytes = decode(original);

        // 分离16位随机字符串,网络字节序和receiveId
        byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
        int xmlLength = recoverNetworkBytesOrder(networkOrder);
        String xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), StandardCharsets.UTF_8);
        String fromReceiveId = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
                StandardCharsets.UTF_8);

        // receiveId不相同的情况
        if (!fromReceiveId.equals(receiveId)) {
            throw new Exception("receiveId不相同");
        }
        return xmlContent;
    }

    /**
     * 还原4个字节的网络字节序
     *
     * @param orderBytes
     * @return
     */
    int recoverNetworkBytesOrder(byte[] orderBytes) {
        int sourceNumber = 0;
        int length = 4;
        for (int i = 0; i < length; i++) {
            sourceNumber <<= 8;
            sourceNumber |= orderBytes[i] & 0xff;
        }
        return sourceNumber;
    }

    /**
     * 删除解密后明文的补位字符
     *
     * @param decrypted 解密后的明文
     * @return 删除补位字符后的明文
     */
    private byte[] decode(byte[] decrypted) {
        int pad = decrypted[decrypted.length - 1];
        int min = 1;
        int max = 32;
        if (pad < min || pad > max) {
            pad = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
    }
}

然后是验证接口,推送suite_ticket是这样的,要先用GET请求验证接口,验证通过了以后才可以保存并推送suite_ticket。

@GetMapping("/suite/receive")
    public String suiteReceive(@RequestParam(value = "msg_signature") String msgSignature,
            @RequestParam(value = "timestamp") String timestamp, @RequestParam(value = "nonce") String nonce,
            @RequestParam(value = "echostr") String data) {
        System.out.println("GET################################");
        System.out.println("msgSignature:" + msgSignature);
        System.out.println("timestamp:" + timestamp);
        System.out.println("nonce:" + nonce);
        System.out.println("data:" + data);
        try {
            WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);
            String sEchoStr = wxcpt.verifyAndGetData(msgSignature, timestamp, nonce, data);

            System.out.println("aesDecode:" + sEchoStr);
            
            // 将解密后获取的参数直接返回就行
            return sEchoStr;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

验证通过后,就可以刷新Ticket了。

@PostMapping("/suite/receive")
    public String suiteReceivePost(HttpServletRequest request,
            @RequestParam(value = "msg_signature") String msgSignature,
            @RequestParam(value = "timestamp") String timestamp, @RequestParam(value = "nonce") String nonce) {
        System.out.println("POST################################");
        System.out.println("msgSignature:" + msgSignature);
        System.out.println("timestamp:" + timestamp);
        System.out.println("nonce:" + nonce);
        String result = null;
        try {
            String xmlString = getXMLString(request);
            System.out.println("data:" + xmlString);
            String encryptData = XML.toJSONObject(xmlString).getJSONObject("xml").getStr("Encrypt");
            System.out.println("encryptData:" + encryptData);

            WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(sToken, sEncodingAESKey, suiteID);
            String sEchoStr = wxcpt.verifyAndGetData(msgSignature, timestamp, nonce, encryptData);
            ;
            System.out.println("sEchoStr:" + sEchoStr);
            JSONObject jsonObject = XML.toJSONObject(sEchoStr).getJSONObject("xml");
            System.out.println("jsonObject:" + jsonObject.toString());
            String infoType = jsonObject.getStr("InfoType");
            System.out.println("infoType:" + infoType);
            
            // 获取到的suiteTicket
            String suiteTicket = jsonObject.getStr("SuiteTicket");
            System.out.println("suiteTicket:" + suiteTicket);

        } catch (Exception e) {
            e.printStackTrace();
        }

        // 该接口返回success
        return "success";
    }

可以看到,GET和POST接口的URI是一致的,不同的是参数和返回内容。验证接口是GET请求,加密内容是URL里获取的,接口直接返回解密内容就行。推送suite_ticket接口是POST请求,加密内容是在请求体里的,接口返回“success”表示成功。

推送suite_ticket接口回调成功以后,每半小时推送一次,注意保存suite_ticket后续接口调用。