DingTalk:: 通信錄單向同步實現示例

概述

最近項目中須要實現對接釘釘,並實現單向通信錄同步(釘釘服務器 -> 對接平臺)本文經過一個簡單的案例快速實現相關的DEMO (本文主要實現與釘釘對接)。html

釘釘API: https://open-doc.dingtalk.com...

流程示意圖

準備工做

在使用回調接口前,須要作如下準備工做:
1) 提供一個接收消息的RESTful接口。
2) 調用釘釘API,主動註冊回調通知。
3) 由於涉及到消息的加密解密,默認的JDK存在一些限制,先要替換相關jar:java

在官方網站下載JCE無限制權限策略文件
JDK6的下載地址: http://www.oracle.com/technet...
JDK7的下載地址: http://www.oracle.com/technet...
JDK8的下載地址: http://www.oracle.com/technet...
下載後解壓,能夠看到local_policy.jar和US_export_policy.jar以及readme.txt。
若是安裝的是JRE,將兩個jar文件放到%JRE_HOME% libsecurity目錄下覆蓋原來的文件,
若是安裝的是JDK,將兩個jar文件放到%JDK_HOME%jrelibsecurity目錄下覆蓋原來文件。

4) 內網穿透映射本地RESTful接口到公網,推薦使用Ngrok: http://ngrok.ciqiuwl.cn/git

具體實現

1. 提供回調接口

package com.wuwenze.dingtalk.rest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.mzlion.core.lang.Assert;
import com.wuwenze.dingtalk.api.DingTalkConst;
import com.wuwenze.dingtalk.encrpty.DingTalkEncryptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.Serializable;
import java.util.Map;

/**
 * @author wwz
 * @version 1 (2018/7/26)
 * @since Java7
 */
@Slf4j
@RestController
public class DingTalkCallbackRest {

    @PostMapping("/dingtalk/receive")
    public Map<String, ? extends Serializable> receive(//
          String signature, String timestamp, String nonce,@RequestBody String requestBody) {
        Assert.notNull(signature, "signature is null.");
        Assert.notNull(timestamp, "timestamp is null.");
        Assert.notNull(nonce, "nonce is null.");
        Assert.notNull(requestBody, "requestBody is null.");

        log.info("#receive 接收密文:{}", requestBody);
        DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(//
                DingTalkConst.CALLBACK_TOKEN, DingTalkConst.CALLBACK_AES_KEY, DingTalkConst.CORP_ID);
        JSONObject jsonEncrypt = JSON.parseObject(requestBody);
        String encryptMessage = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, jsonEncrypt.getString("encrypt"));
        log.info("#receive 密文解密後:{}", encryptMessage);

        // TODO: 異步處理報文,解析相關信息

        // 返回加密後的success (快速響應)
        return dingTalkEncryptor.getEncryptedMsg("success", Long.parseLong(timestamp), nonce);
    }
}

接口寫好以後,還須要將接口暴露在公網,如此釘釘服務器才能進行調用,下爲內網穿透示意圖:
titlegithub

釘釘爲咱們開發者提供了一個Ngrok服務,在https://github.com/open-dingt...,按照操做文章指引配置便可。web

我在這邊使用的是其餘的Ngrok服務,官網地址是http://ngrok.ciqiuwl.cn/,配置後啓動以下圖所示:
title
將本地的http://127.0.0.1:8080映射到http://wuwz.ngrok.xiaomiqiu.cn,最終提供給釘釘的回調接口地址即爲:http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receivespring

以上準備工做完後成,就能夠將接口啓動起來,繼續後續的操做。json

2. 主動註冊回調接口

寫一個測試方法,將 http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive註冊到釘釘,後續釘釘相關的消息都會推送到此處。
package com.wuwenze.dingtalk;

import com.wuwenze.dingtalk.api.DingTalkApi;
import com.wuwenze.dingtalk.api.DingTalkConst;
import com.wuwenze.dingtalk.enums.DingTalkCallbackTag;

/**
 * @author wwz
 * @version 1 (2018/7/27)
 * @since Java7
 */
public class TestRegisterCallback {

    public static void main(String[] args) {
        // 獲取Token
        String accessToken = DingTalkApi.getAccessTokenCache();

        // 先刪除以前註冊的回調接口
        DingTalkApi.removeCallback(accessToken);

        // 註冊新的回調接口
        String callbackToken = DingTalkConst.CALLBACK_TOKEN;
        String callbackAesKey = DingTalkConst.CALLBACK_AES_KEY;
        String callbackUrl = "http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive";
        DingTalkCallbackTag[] callbackTags = {
                DingTalkCallbackTag.USER_ADD_ORG, // 增長用戶
                DingTalkCallbackTag.USER_MODIFY_ORG, // 修改用戶
                DingTalkCallbackTag.USER_LEAVE_ORG // 用戶離職
        };
        DingTalkApi.registerCallback(accessToken, callbackToken, callbackAesKey, callbackUrl, callbackTags);
    }
}

執行代碼,若是一切不出意外的話,就註冊成功了(註冊的過程當中需保證callbackUrl能夠正常訪問,由於首次會向該接口發送一條check_url事件,驗證其合法性)api

// 獲取Token
13:44:41.342 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"access_token":"9990578f789c3fb1a9d974c268df5029","errcode":0.0,"errmsg":"ok","expires_in":7200.0}
// 先刪除以前註冊的回調接口
13:44:41.438 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"errcode":0.0,"errmsg":"ok"}
// 註冊新的回調接口
13:44:41.888 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"errcode":0.0,"errmsg":"ok"}
13:44:41.893 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - #registerCallback 註冊回調接口 -> url: http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive, tags: tag: user_add_org, describe: 通信錄用戶增長 + tag: user_modify_org, describe: 通信錄用戶更改 + tag: user_leave_org, describe: 通信錄用戶離職

另外再來觀察一下回調接口是否收到checkUrL消息:數組

2018-07-27 13:44:41.823  INFO 2392 --- [nio-8080-exec-1] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 接收密文:{"encrypt":"JfRo/wn+E1agXgk1uN5UQP/WDv0RvWnw8TgXC/ucatBxYm54OSUcGn5uTGCVMaGIN6Lv24ZOujH/uixB39AKxjXWgzdJQ1Eq4HD0EIJFG+QY8mjcCltvhX0QfhisFlll"}
2018-07-27 13:44:41.823  INFO 2392 --- [nio-8080-exec-1] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 密文解密後:{"EventType":"check_url"}

3. 測試註冊的通信錄事件

在上一步中,註冊了 USER_ADD_ORG (增長用戶)、 USER_MODIFY_ORG (修改用戶)、 USER_LEAVE_ORG (用戶離職|刪除)三個事件

打開釘釘後臺管理,在通信錄中新增一個用戶:
title
保存成功後,在回調接口中則立刻收到了該事件的通知消息:服務器

2018-07-27 13:49:55.985  INFO 2392 --- [nio-8080-exec-3] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 接收密文:{"encrypt":"g6RsagVKTVUS2Gg7B1JSn81uJPgCpPKoaRN4kps4cMpp6CuqW1QahaDP8TcnwDP2fYyG0gwLFvF5cOWbn+lKX2kq4UYe5m08BB/FWw8lALV/4LYu7RI6OARCFDTsllBTs4W6/OUv+9AyYlWGmwK2ZYnXoFyiK4DqFt6jenp45NCXwvSgssjn8RsD/3E7kfw5DL/mfr4L3hkaBysmkU2ohaFFEqBO1r63cj+mONLsD8Dvr2lAsefBoMdZ2JV5sIIePuKhz08G6KnJDvkAqcm59naV6AIbDLouWrBK7upCP7Q="}
2018-07-27 13:49:55.985  INFO 2392 --- [nio-8080-exec-3] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 密文解密後:{"TimeStamp":"1532670599144","CorpId":"dingb9875d6606f892ed35c2f4657eb6378f","UserId":["202844352662984130"],"EventType":"user_add_org"}

4. 後續同步邏輯

在上面的例子中新增用戶後,收到的報文解密後的信息爲只包含事件類型和用戶ID,因此後面還須要主動調用釘釘獲取用戶詳情的接口,再作具體的同步邏輯,這裏就再也不往下寫了,貼一下相關的API接口吧:
https://open-doc.dingtalk.com...
title

相關API工具封裝

下面羅列了以上示例中用到的工具類封裝,再也不具體講解,直接貼代碼

DingTalkConst

常量池
public class DingTalkConst {
    public final static String CORP_ID = "dingb9875d6606f892ed35c2f4657eb6378f";
    public final static Object CORP_SECRET = "到釘釘查看";
    public final static String CALLBACK_TOKEN = "token"; // 回調Token
    public final static String CALLBACK_AES_KEY = "xxxxx7p5qnb6zs3xxxxxlkfmxqfkv23d40yd0xxxxxx"; // 回調祕鑰,43個隨機字符
}

DingTalkCallbackTag

可供註冊的回調事件類型枚舉
public enum DingTalkCallbackTag {
    USER_ADD_ORG("通信錄用戶增長"),
    USER_MODIFY_ORG("通信錄用戶更改"),
    USER_LEAVE_ORG("通信錄用戶離職"),
    ORG_ADMIN_ADD("通信錄用戶被設爲管理員"),
    ORG_ADMIN_REMOVE("通信錄用戶被取消設置管理員"),
    ORG_DEPT_CREATE("通信錄企業部門建立"),
    ORG_DEPT_MODIFY("通信錄企業部門修改"),
    ORG_DEPT_REMOVE("通信錄企業部門刪除"),
    ORG_REMOVE("企業被解散"),
    ORG_CHANGE("企業信息發生變動"),
    LABEL_USER_CHANGE("員工角色信息發生變動"),
    LABEL_CONF_ADD("增長角色或者角色組"),
    LABEL_CONF_DEL("刪除角色或者角色組"),
    LABEL_CONF_MODIFY("修改角色或者角色組");

    private String describe;
    DingTalkCallbackTag(String describe) {
        this.describe = describe;
    }

    public String getDescribe() {
        return describe;
    }

    public void setDescribe(String describe) {
        this.describe = describe;
    }

    @Override
    public String toString() {
        return super.toString().toLowerCase();
    }

    public String toInfoString() {
        return String.format("tag: %s, describe: %s", this.toString(), this.getDescribe());
    }
}

DingTalkEncryptor

釘釘消息加密解密工做類
package com.wuwenze.dingtalk.encrpty;

import com.google.common.collect.ImmutableMap;
import com.mzlion.core.binary.Base64;
import com.mzlion.core.lang.Assert;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;

/**
 * @author wwz
 * @version 1 (2018/7/26)
 * @since Java7
 */
public class DingTalkEncryptor {
    private static final Charset CHARSET = Charset.forName("UTF-8");
    private byte[] aesKey;
    private String token;
    private String corpId;

    /**
     * ask getPaddingBytes key固定長度
     **/
    private static final Integer AES_ENCODE_KEY_LENGTH = 43;
    /**
     * 加密隨機字符串字節長度
     **/
    private static final Integer RANDOM_LENGTH = 16;

    /**
     * 構造函數
     *
     * @param token          釘釘開放平臺上,開發者設置的token
     * @param encodingAesKey 釘釘開放臺上,開發者設置的EncodingAESKey
     * @param corpId         ISV進行配置的時候應該傳對應套件的SUITE_KEY(第一次建立時傳的是默認的CREATE_SUITE_KEY),普通企業是Corpid
     */
    public DingTalkEncryptor(String token, String encodingAesKey, String corpId) {
        if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
            throw new IllegalArgumentException("encodingAesKey is null");
        }
        this.token = token;
        this.corpId = corpId;
        this.aesKey = Base64.decode(encodingAesKey + "=");
    }

    /**
     * 將和釘釘開放平臺同步的消息體加密,返回加密Map
     *
     * @param message 傳遞的消息體明文
     * @param timeStamp 時間戳
     * @param nonce     隨機字符串
     * @return
     */
    public Map<String, ? extends Serializable> getEncryptedMsg(String message, Long timeStamp, String nonce) {
        Assert.notNull(message, "plaintext is null");
        Assert.notNull(timeStamp, "timeStamp is null");
        Assert.notNull(nonce, "nonce is null");
        String encrypt = encrypt(getRandomStr(RANDOM_LENGTH), message);
        String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
        return ImmutableMap.of(
                "msg_signature", signature, //
                "encrypt", encrypt, //
                "timeStamp", timeStamp,//
                "nonce", nonce);
    }

    /**
     * 密文解密
     *
     * @param msgSignature 簽名串
     * @param timeStamp    時間戳
     * @param nonce        隨機串
     * @param encryptMsg   密文
     * @return 解密後的原文
     */
    public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg) {
        // 校驗簽名
        String signature = getSignature(token, timeStamp, nonce, encryptMsg);
        if (!signature.equals(msgSignature)) {
            throw new RuntimeException("校驗簽名失敗。");
        }
        // 解密
        return decrypt(encryptMsg);
    }

    private String encrypt(String random, String plaintext) {
        try {
            byte[] randomBytes = random.getBytes(CHARSET);
            byte[] plainTextBytes = plaintext.getBytes(CHARSET);
            byte[] lengthByte = int2Bytes(plainTextBytes.length);
            byte[] corpidBytes = corpId.getBytes(CHARSET);
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            byteStream.write(randomBytes);
            byteStream.write(lengthByte);
            byteStream.write(plainTextBytes);
            byteStream.write(corpidBytes);
            byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size());
            byteStream.write(padBytes);
            byte[] unencrypted = byteStream.toByteArray();
            byteStream.close();
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
            byte[] encrypted = cipher.doFinal(unencrypted);
            return Base64.encode(encrypted);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 對密文進行解密.
     * @param text 須要解密的密文
     * @return 解密獲得的明文
     */
    private String decrypt(String text) {
        byte[] originalArr;
        try {
            // 設置解密模式爲AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            // 使用BASE64對密文進行解碼, 解密
            originalArr = cipher.doFinal(Base64.decode(text));
        } catch (Exception e) {
            throw new RuntimeException("計算解密文本錯誤");
        }

        String plainText;
        String fromCorpid;
        try {
            // 去除補位字符
            byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
            // 分離16位隨機字符串,網絡字節序和corpId
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
            int plainTextLegth = bytes2int(networkOrder);
            plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET);
            fromCorpid = new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET);
        } catch (Exception e) {
            throw new RuntimeException("計算解密文本長度錯誤");
        }

        // corpid不相同的狀況
        if (!fromCorpid.equals(corpId)) {
            throw new RuntimeException("計算文本密碼錯誤");
        }
        return plainText;
    }

    /**
     * 數字簽名
     * @param token     isv token
     * @param timestamp 時間戳
     * @param nonce     隨機串
     * @param encrypt   加密文本
     * @return
     */
    public String getSignature(String token, String timestamp, String nonce, String encrypt) {
        try {
            String[] array = new String[]{token, timestamp, nonce, encrypt};
            Arrays.sort(array);
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuffer hexstr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
           throw new RuntimeException(e);
        }
    }

    public static String getRandomStr(int count) {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < count; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }


    /**
     * int轉byte數組,高位在前
     */
    public static byte[] int2Bytes(int count) {
        byte[] byteArr = new byte[4];
        byteArr[3] = (byte) (count & 0xFF);
        byteArr[2] = (byte) (count >> 8 & 0xFF);
        byteArr[1] = (byte) (count >> 16 & 0xFF);
        byteArr[0] = (byte) (count >> 24 & 0xFF);
        return byteArr;
    }

    /**
     * 高位在前bytes數組轉int
     * @param byteArr
     * @return
     */
    public static int bytes2int(byte[] byteArr) {
        int count = 0;
        for (int i = 0; i < 4; i++) {
            count <<= 8;
            count |= byteArr[i] & 0xff;
        }
        return count;
    }
}

PKCS7Padding

package com.wuwenze.dingtalk.encrpty;

import java.nio.charset.Charset;
import java.util.Arrays;

/**
 * @author wwz
 * @version 1 (2018/7/10)
 * @since Java7
 */
public class PKCS7Padding {
    private final static Charset CHARSET    = Charset.forName("utf-8");
    private final static int     BLOCK_SIZE = 32;

    /**
     * 填充mode字節
     * @param count
     * @return
     */
    public static byte[] getPaddingBytes(int count) {
        int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
        if (amountToPad == 0) {
            amountToPad = BLOCK_SIZE;
        }
        char padChr = chr(amountToPad);
        String tmp = new String();
        for (int index = 0; index < amountToPad; index++) {
            tmp += padChr;
        }
        return tmp.getBytes(CHARSET);
    }

    /**
     * 移除mode填充字節
     * @param decrypted
     * @return
     */
    public static byte[] removePaddingBytes(byte[] decrypted) {
        int pad = (int) decrypted[decrypted.length - 1];
        if (pad < 1 || pad > BLOCK_SIZE) {
            pad = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
    }

    private static char chr(int a) {
        byte target = (byte) (a & 0xFF);
        return (char) target;
    }
}

DingTalkApi

釘釘開放API簡易封裝 (僅供測試)
package com.wuwenze.dingtalk.api;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.mzlion.core.lang.Assert;
import com.mzlion.easyokhttp.HttpClient;
import com.wuwenze.dingtalk.enums.DingTalkCallbackTag;
import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;


/**
 * @author wwz
 * @version 1 (2018/7/26)
 * @since Java7
 */
@Slf4j
public class DingTalkApi {

    private final static LoadingCache<String, String> mTokenCache = //
            CacheBuilder.newBuilder()//
                    .maximumSize(100)//
                    .expireAfterAccess(7200, TimeUnit.SECONDS)//
                    .build(new CacheLoader<String, String>() {
                        @Override
                        public String load(String key) throws Exception {
                            // key:corpId#corpSecret
                            String[] params = key.split("#");
                            if (params.length != 2) {
                                throw new RuntimeException("#loadTokenCache error");
                            }
                            return getAccessToken(params[0], params[1]);
                        }
                    });



    public static String getAccessToken(String corpId, String corpSecret) {
        String url = String.format("https://oapi.dingtalk.com/gettoken?corpid=%s&corpsecret=%s",corpId, corpSecret);
        JSONObject jsonObject = HttpClient.get(url).asBean(JSONObject.class);
        assertDingTalkJSONObject(jsonObject);
        return jsonObject.getString("access_token");
    }

    public static String getAccessTokenCache() {
        try {
            return mTokenCache.get(DingTalkConst.CORP_ID + "#" + DingTalkConst.CORP_SECRET);
        } catch (ExecutionException e) {
            return null;
        }
    }

    public static void registerCallback(String accessToken, String callbackToken, String callbackAesKey, String url, DingTalkCallbackTag ... tags) {
        Assert.notNull(accessToken, "accessToken is null");
        Assert.notNull(callbackToken, "callbackToken is null");
        Assert.notNull(callbackAesKey, "callbackAesKey is null");
        Assert.notNull(url, "url is null");
        if (tags.length < 1) {
            throw new IllegalArgumentException("至少指定一個回調事件類型。");
        }
        String[] callbackTagArray = new String[tags.length];
        for (int i = 0; i < tags.length; i++) {
            callbackTagArray[i] = tags[i].toString();
        }
        ImmutableMap<String, Serializable> params = ImmutableMap.of(//
                "call_back_tag", callbackTagArray,//
                "token", callbackToken,//
                "aes_key", callbackAesKey, //
                "url", url//
        );
        String apiUrl =  "https://oapi.dingtalk.com/call_back/register_call_back?access_token=" + accessToken;
        assertDingTalkJSONObject(//
                HttpClient.textBody(apiUrl).json(JSON.toJSONString(params)).asBean(JSONObject.class)
        );
        log.info("#registerCallback 註冊回調接口 -> url: {}, tags: {}", url, showTagsInfo(tags));
    }

    private static String showTagsInfo(DingTalkCallbackTag ... tags) {
        StringBuffer stringBuffer = new StringBuffer();
        for (DingTalkCallbackTag tag : tags) {
            stringBuffer.append(tag.toInfoString()).append(" + ");
        }
        return stringBuffer.toString();
    }

    public static void removeCallback(String accessToken) {
        String apiUrl = "https://oapi.dingtalk.com/call_back/delete_call_back?access_token=" + accessToken;
        assertDingTalkJSONObject(//
                HttpClient.get(apiUrl).asBean(JSONObject.class)
        );
    }

    public static void removeCallback() {
        removeCallback(getAccessTokenCache());
    }

    public static void registerCallback(String url, DingTalkCallbackTag ... tags) {
        registerCallback(getAccessTokenCache(), DingTalkConst.CALLBACK_TOKEN, DingTalkConst.CALLBACK_AES_KEY, url, tags);
    }

    private static void assertDingTalkJSONObject(JSONObject jsonObject) {
        log.info(jsonObject.toJSONString());
        int errcode = jsonObject.getIntValue("errcode");
        if (errcode != 0) {
            throw new RuntimeException(jsonObject.getString("errmsg"));
        }
    }
}
相關文章
相關標籤/搜索