关键区别说明(指令回调 vs 数据回调)
特性 | 指令回调 | 数据回调 |
---|---|---|
触发场景 | 授权/取消授权等管理事件 | 通讯录变更、应用菜单点击等业务事件 |
关键字段 | InfoType |
Event + ChangeType |
典型事件 | suite_auth, cancel_auth | change_contact, suite_ticket |
响应要求 | 必须返回加密的"success" | 必须返回加密的"success" |
xml:
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 企业微信官方加解密库 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-cp</artifactId>
<version>4.5.0</version>
</dependency>
<!-- XML处理 -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
controller
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.example.testchat.aes.WXBizMsgCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@RestController
@RequestMapping("/callback")
public class WxWorkCallbackController {
private static final Logger logger = LoggerFactory.getLogger(WxWorkCallbackController.class);
@Value("${qiyewx.token}")
private String token;
@Value("${qiyewx.encodingAESKey}")
private String encodingAESKey;
@Value("${qiyewx.corpid}")
private String corpid;
@Value("${qiyewx.suiteId}")
private String suiteId;
/**
* 数据回调验证接口 (GET请求)
*/
@GetMapping("/data")
public String validateDataCallback(
@RequestParam("msg_signature") String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
logger.info("收到数据回调验证请求: signature={}, timestamp={}, nonce={}, echostr={}",
msgSignature, timestamp, nonce, echostr);
try {
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(token, encodingAESKey, corpid);
String plainText = wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr);
logger.info("验证成功,明文: {}", plainText);
return plainText;
} catch (Exception e) {
logger.error("验证失败", e);
return "fail";
}
}
/**
* 专门处理suite_ticket推送(数据回调)
*/
@PostMapping(value = "/data", produces = "text/plain;charset=UTF-8")
public String handleDataCallback(
@RequestParam("msg_signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestBody String encryptedMsg) {
try {
// 1. 解密消息
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(token, encodingAESKey, corpid);
String plainText = wxcpt.DecryptMsg(signature, timestamp, nonce, encryptedMsg);
// 2. 解析XML
Map<String, String> message = parseXml(plainText);
if ("suite_ticket".equals(message.get("Event"))) {
String suiteTicket = message.get("SuiteTicket");
String suiteId = message.get("SuiteId");
// 3. 保存ticket(示例代码)
saveSuiteTicket(suiteId, suiteTicket);
logger.info("成功更新suite_ticket: {}", suiteTicket);
}
// 4. 关键点:返回加密的success!!!
String encryptedSuccess = wxcpt.EncryptMsg("success", timestamp, nonce);
return encryptedSuccess;
} catch (Exception e) {
logger.error("处理suite_ticket失败", e);
return "fail";
}
}
private void saveSuiteTicket(String suiteId, String suiteTicket) {
// 实现你的存储逻辑,例如:
// redisTemplate.opsForValue().set("wxwork:ticket:"+suiteId, suiteTicket, 20*60);
System.out.println("suiteId: " + suiteId + "suiteTicket:" + suiteTicket);
}
/**
* 指令回调验证接口(GET请求)
* 企业微信首次配置时会触发此验证
*/
@GetMapping("/cmd")
public String validateCmdCallback(
@RequestParam("msg_signature") String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
logger.info("[指令回调] 验证请求 - signature:{}, timestamp:{}, nonce:{}, echostr:{}",
msgSignature, timestamp, nonce, echostr);
try {
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(token, encodingAESKey, suiteId);
String plainText = wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr);
logger.info("[指令回调] 验证成功,明文: {}", plainText);
// return plainText; // 必须返回解密后的明文
return "success";
} catch (Exception e) {
logger.error("[指令回调] 验证失败", e);
return "fail";
}
}
/**
* 指令回调处理接口(POST请求)
* 接收:授权成功、取消授权、变更授权等指令
*/
@PostMapping(value = "/cmd", produces = "application/xml;charset=UTF-8")
public String handleCmdCallback(
@RequestParam("msg_signature") String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestBody String encryptedMsg) {
logger.info("[指令回调] 收到消息 - signature:{}, timestamp:{}, nonce:{}",
msgSignature, timestamp, nonce);
try {
// 1. 解密消息
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(token, encodingAESKey, suiteId);
String plainText = wxcpt.DecryptMsg(msgSignature, timestamp, nonce, encryptedMsg);
logger.info("[指令回调] 解密后消息: {}", plainText);
// 2. 解析XML(复用数据回调的解析方法)
Map<String, String> message = parseXml(plainText);
String infoType = message.get("InfoType");
String authCorpId = message.get("AuthCorpId");
String SuiteTicket = message.get("SuiteTicket");
saveSuiteTicket(authCorpId, SuiteTicket);
// 3. 处理不同类型的指令
switch (infoType) {
case "suite_auth":
// 授权成功事件(含临时授权码)
String authCode = message.get("AuthCode");
logger.info("[指令回调] 企业授权成功: corpId={}, authCode={}", authCorpId, authCode);
// TODO: 调用企业微信API换取永久授权码
break;
case "change_auth":
// 授权变更事件(如权限集变更)
String state = message.get("State");
logger.info("[指令回调] 授权变更: corpId={}, state={}", authCorpId, state);
break;
case "cancel_auth":
// 取消授权事件
logger.info("[指令回调] 取消授权: corpId={}", authCorpId);
// TODO: 清理该企业相关数据
break;
default:
logger.warn("[指令回调] 未知指令类型: {}", infoType);
}
// 4. 必须返回加密的success
// return wxcpt.EncryptMsg("success", timestamp, nonce);
return "success";
} catch (Exception e) {
logger.error("[指令回调] 处理失败", e);
return "fail";
}
}
/**
* 解析XML到Map
*/
private Map<String, String> parseXml(String xml) throws DocumentException {
Map<String, String> result = new HashMap<>();
Document document = DocumentHelper.parseText(xml);
Element root = document.getRootElement();
for (Iterator<Element> it = root.elementIterator(); it.hasNext(); ) {
Element element = it.next();
result.put(element.getName(), element.getText());
}
return result;
}
}
yml
server:
port: 8080
servlet:
context-path: /
wxwork:
token: 你的Token # 在企业微信后台设置的回调Token
encodingAESKey: 你的EncodingAESKey # 在企业微信后台设置的EncodingAESKey
corpId: 你的CorpID # 企业微信服务商的CorpID
suiteId: 你的suiteId # 第三方应用id
踩坑:企业微信文档写的太烂了,而且坑也特别多,企业微信指令回调用的不是corpid,而是
suiteId!!!!!!!!!!!