企业微信推送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位随机字符串,网络字节序和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后续接口调用。