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

前言:


想要对接企业微信,你要有心理准备!你要有心理准备!你要有心理准备!你必须要理清楚整个业务过程以及每个过程需要调用什么接口,你有什么参数,你要调用企业微信接口获得哪些参数,下一步需要做什么。否则根据现在企业微信的情况:文档缺少技术实现案例、审核流程慢,比如创建第三方应用审核一直卡在域名未备案、没有在线技术人员、有问题联系在线客服,在线客服解决不了技术问题会让你在社区发帖子,运气不好你要等很久才能等到有人回帖,还不一定能解决问题、网上的案例又很少、文档停更,但是服务商后台和企业微信后台ui有变更......等等问题会搞的你非常难受。因此,本前言就是告诉你,你要有充分的心理准备。因为这是一个非常蛋疼的周期。以下接口也是经过我经过无数次踩坑、经技术论证后贴出的,搞定了这两个接口,下面的就好弄了。给各位作参考,减少各位的时间成本。

开始:

关键区别说明(指令回调 vs 数据回调)

特性指令回调数据回调
触发场景授权/取消授权等管理事件通讯录变更、应用菜单点击等业务事件
关键字段InfoTypeEvent + ChangeType
典型事件suite_auth, cancel_authchange_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, corpId);
            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_ticket":
                    // suite_ticket 刷新
                    
                    break;

                case "create_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

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值