关键区别说明(指令回调 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. 解析XMLMap<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: 8080servlet:context-path: /wxwork:token: 你的Token # 在企业微信后台设置的回调TokenencodingAESKey: 你的EncodingAESKey # 在企业微信后台设置的EncodingAESKeycorpId: 你的CorpID # 企业微信服务商的CorpIDsuiteId: 你的suiteId # 第三方应用id
踩坑:企业微信文档写的太烂了,而且坑也特别多,企业微信指令回调用的不是corpid,而是
suiteId!!!!!!!!!!!