企业微信推送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";
这些参数都是可以通过企业微信管理后台拿到的,比如
然后是企业微信参数解析类
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位随机字符串,网络字节序和receiveIdbyte[] 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);// 获取到的suiteTicketString suiteTicket = jsonObject.getStr("SuiteTicket");System.out.println("suiteTicket:" + suiteTicket);} catch (Exception e) {e.printStackTrace();}// 该接口返回successreturn "success";}
可以看到,GET和POST接口的URI是一致的,不同的是参数和返回内容。验证接口是GET请求,加密内容是URL里获取的,接口直接返回解密内容就行。推送suite_ticket接口是POST请求,加密内容是在请求体里的,接口返回“success”表示成功。
推送suite_ticket接口回调成功以后,每半小时推送一次,注意保存suite_ticket后续接口调用。