企业微信服务商创建第三方应用配置数据回调url和指令回调url的java代码实现

发布于:2025-07-31 ⋅ 阅读:(18) ⋅ 点赞:(0)

关键区别说明(指令回调 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!!!!!!!!!!!

网站公告

今日签到

点亮在社区的每一天
去签到