正文

公司要求对接钉钉,之前没对接过,相当于从0开始,记录一下对接的过程

1、访问钉钉官网

因为公司没对接过,所以我自己注册了一个公司(公司名随便填,其他的按要求填就好)

java 发送钉钉待办 java对接钉钉_钉钉


注意:如果你本身就是最高级的管理员,那么你不需要申请成为开发者,因为你就是BOSS

java 发送钉钉待办 java对接钉钉_java 发送钉钉待办_02


如果你不是最高级,那么,你就要申请成为开发者,权限叫你的同事配上去

2、创建一个应用

java 发送钉钉待办 java对接钉钉_jvm_03

3、点击应用详情

java 发送钉钉待办 java对接钉钉_java_04

4、选择事件与回调

java 发送钉钉待办 java对接钉钉_ci_05

5、做内网穿透

工具自己上网找一个,要达到的效果是,可以通过外网访问项目。例如:访问接口:localhost:80/接口名,外网的访问方式是:http:/外网映射:外网端口/接口名

具体的映射参考工具的教程

6、测试代码

DingCallbackCrypto(这个类是从阿里的git拿下来的,里面还有其他语言的,这里是java代码)

import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.security.Security;
import java.lang.reflect.Field;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import com.alibaba.fastjson.JSON;

import org.apache.commons.codec.binary.Base64;
/**
 * @Description DingDing解密
 * @Author JunHao Huang
 * @Date 2023/6/1 10:25
 */
public class DingCallbackCrypto {
	private static final Charset CHARSET = Charset.forName("utf-8");
	private static final Base64 base64 = new Base64();
	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         企业自建应用-事件订阅, 使用appKey
	 *                       企业自建应用-注册回调地址, 使用corpId
	 *                       第三方企业应用, 使用suiteKey
	 *
	 * @throws DingTalkEncryptException 执行失败,请查看该异常的错误码和具体的错误信息
	 */
	public DingCallbackCrypto(String token, String encodingAesKey, String corpId) throws DingTalkEncryptException {
		if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
			throw new DingTalkEncryptException(DingTalkEncryptException.AES_KEY_ILLEGAL);
		}
		this.token = token;
		this.corpId = corpId;
		aesKey = Base64.decodeBase64(encodingAesKey + "=");
	}

	public Map<String, String> getEncryptedMap(String plaintext) throws DingTalkEncryptException {
		return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16));
	}

	/**
	 * 将和钉钉开放平台同步的消息体加密,返回加密Map
	 *
	 * @param plaintext 传递的消息体明文
	 * @param timeStamp 时间戳
	 * @param nonce     随机字符串
	 * @return
	 * @throws DingTalkEncryptException
	 */
	public Map<String, String> getEncryptedMap(String plaintext, Long timeStamp, String nonce)
		throws DingTalkEncryptException {
		if (null == plaintext) {
			throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL);
		}
		if (null == timeStamp) {
			throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_TIMESTAMP_ILLEGAL);
		}
		if (null == nonce) {
			throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_NONCE_ILLEGAL);
		}
		// 加密
		String encrypt = encrypt(Utils.getRandomStr(RANDOM_LENGTH), plaintext);
		String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
		Map<String, String> resultMap = new HashMap<String, String>();
		resultMap.put("msg_signature", signature);
		resultMap.put("encrypt", encrypt);
		resultMap.put("timeStamp", String.valueOf(timeStamp));
		resultMap.put("nonce", nonce);
		return resultMap;
	}

	/**
	 * 密文解密
	 *
	 * @param msgSignature 签名串
	 * @param timeStamp    时间戳
	 * @param nonce        随机串
	 * @param encryptMsg   密文
	 * @return 解密后的原文
	 * @throws DingTalkEncryptException
	 */
	public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg)
		throws DingTalkEncryptException {
		//校验签名
		String signature = getSignature(token, timeStamp, nonce, encryptMsg);
		if (!signature.equals(msgSignature)) {
			throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
		}
		// 解密
		String result = decrypt(encryptMsg);
		return result;
	}

	/*
	 * 对明文加密.
	 * @param text 需要加密的明文
	 * @return 加密后base64编码的字符串
	 */
	private String encrypt(String random, String plaintext) throws DingTalkEncryptException {
		try {
			byte[] randomBytes = random.getBytes(CHARSET);
			byte[] plainTextBytes = plaintext.getBytes(CHARSET);
			byte[] lengthByte = Utils.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);
			String result = base64.encodeToString(encrypted);
			return result;
		} catch (Exception e) {
			throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR);
		}
	}

	/*
	 * 对密文进行解密.
	 * @param text 需要解密的密文
	 * @return 解密得到的明文
	 */
	private String decrypt(String text) throws DingTalkEncryptException {
		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对密文进行解码
			byte[] encrypted = Base64.decodeBase64(text);
			// 解密
			originalArr = cipher.doFinal(encrypted);
		} catch (Exception e) {
			throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_ERROR);
		}

		String plainText;
		String fromCorpid;
		try {
			// 去除补位字符
			byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
			// 分离16位随机字符串,网络字节序和corpId
			byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
			int plainTextLegth = Utils.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 DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR);
		}

		// corpid不相同的情况
		if (!fromCorpid.equals(corpId)) {
			throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
		}
		return plainText;
	}

	/**
	 * 数字签名
	 *
	 * @param token     isv token
	 * @param timestamp 时间戳
	 * @param nonce     随机串
	 * @param encrypt   加密文本
	 * @return
	 * @throws DingTalkEncryptException
	 */
	public String getSignature(String token, String timestamp, String nonce, String encrypt)
		throws DingTalkEncryptException {
		try {
			String[] array = new String[] {token, timestamp, nonce, encrypt};
			Arrays.sort(array);
			System.out.println(JSON.toJSONString(array));
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < 4; i++) {
				sb.append(array[i]);
			}
			String str = sb.toString();
			System.out.println(str);
			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 DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
		}
	}

	public static class Utils {
		public Utils() {
		}

		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();
		}

		public static byte[] int2Bytes(int count) {
			byte[] byteArr = new byte[] {(byte)(count >> 24 & 255), (byte)(count >> 16 & 255), (byte)(count >> 8 & 255),
				(byte)(count & 255)};
			return byteArr;
		}

		public static int bytes2int(byte[] byteArr) {
			int count = 0;

			for (int i = 0; i < 4; ++i) {
				count <<= 8;
				count |= byteArr[i] & 255;
			}

			return count;
		}
	}

	public static class PKCS7Padding {
		private static final Charset CHARSET = Charset.forName("utf-8");
		private static final int BLOCK_SIZE = 32;

		public PKCS7Padding() {
		}

		public static byte[] getPaddingBytes(int count) {
			int amountToPad = 32 - count % 32;
			if (amountToPad == 0) {
				amountToPad = 32;
			}

			char padChr = chr(amountToPad);
			String tmp = new String();

			for (int index = 0; index < amountToPad; ++index) {
				tmp = tmp + padChr;
			}

			return tmp.getBytes(CHARSET);
		}

		public static byte[] removePaddingBytes(byte[] decrypted) {
			int pad = decrypted[decrypted.length - 1];
			if (pad < 1 || pad > 32) {
				pad = 0;
			}

			return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
		}

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

	public static class DingTalkEncryptException extends Exception {
		public static final int SUCCESS = 0;
		public static final int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001;
		public static final int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002;
		public static final int ENCRYPTION_NONCE_ILLEGAL = 900003;
		public static final int AES_KEY_ILLEGAL = 900004;
		public static final int SIGNATURE_NOT_MATCH = 900005;
		public static final int COMPUTE_SIGNATURE_ERROR = 900006;
		public static final int COMPUTE_ENCRYPT_TEXT_ERROR = 900007;
		public static final int COMPUTE_DECRYPT_TEXT_ERROR = 900008;
		public static final int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009;
		public static final int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010;
		private static Map<Integer, String> msgMap = new HashMap();
		private Integer code;

		static {
			msgMap.put(0, "成功");
			msgMap.put(900001, "加密明文文本非法");
			msgMap.put(900002, "加密时间戳参数非法");
			msgMap.put(900003, "加密随机字符串参数非法");
			msgMap.put(900005, "签名不匹配");
			msgMap.put(900006, "签名计算失败");
			msgMap.put(900004, "不合法的aes key");
			msgMap.put(900007, "计算加密文字错误");
			msgMap.put(900008, "计算解密文字错误");
			msgMap.put(900009, "计算解密文字长度不匹配");
			msgMap.put(900010, "计算解密文字corpid不匹配");
		}

		public Integer getCode() {
			return this.code;
		}

		public DingTalkEncryptException(Integer exceptionCode) {
			super((String)msgMap.get(exceptionCode));
			this.code = exceptionCode;
		}
	}
	static {
		try {
			Security.setProperty("crypto.policy", "limited");
			RemoveCryptographyRestrictions();
		} catch (Exception var1) {
		}

	}
	private static void RemoveCryptographyRestrictions() throws Exception {
		Class<?> jceSecurity = getClazz("javax.crypto.JceSecurity");
		Class<?> cryptoPermissions = getClazz("javax.crypto.CryptoPermissions");
		Class<?> cryptoAllPermission = getClazz("javax.crypto.CryptoAllPermission");
		if (jceSecurity != null) {
			setFinalStaticValue(jceSecurity, "isRestricted", false);
			PermissionCollection defaultPolicy = (PermissionCollection)getFieldValue(jceSecurity, "defaultPolicy", (Object)null, PermissionCollection.class);
			if (cryptoPermissions != null) {
				Map<?, ?> map = (Map)getFieldValue(cryptoPermissions, "perms", defaultPolicy, Map.class);
				map.clear();
			}

			if (cryptoAllPermission != null) {
				Permission permission = (Permission)getFieldValue(cryptoAllPermission, "INSTANCE", (Object)null, Permission.class);
				defaultPolicy.add(permission);
			}
		}

	}
	private static Class<?> getClazz(String className) {
		Class clazz = null;

		try {
			clazz = Class.forName(className);
		} catch (Exception var3) {
		}

		return clazz;
	}
	private static void setFinalStaticValue(Class<?> srcClazz, String fieldName, Object newValue) throws Exception {
		Field field = srcClazz.getDeclaredField(fieldName);
		field.setAccessible(true);
		Field modifiersField = Field.class.getDeclaredField("modifiers");
		modifiersField.setAccessible(true);
		modifiersField.setInt(field, field.getModifiers() & -17);
		field.set((Object)null, newValue);
	}
	private static <T> T getFieldValue(Class<?> srcClazz, String fieldName, Object owner, Class<T> dstClazz) throws Exception {
		Field field = srcClazz.getDeclaredField(fieldName);
		field.setAccessible(true);
		return dstClazz.cast(field.get(owner));
	}
}

DingDingController

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * @Description 钉钉控制类
 * @Author JunHao Huang
 * @Date 2023/5/31 18:21
 */
@RestController
@RequestMapping("dingding")
public class DingDingController {

	/**
	 * 钉钉事件订阅token
	 */
	@Value("${dingtalk.app.token}")
	private String token;

	/**
	 * 钉钉事件订阅aes_key
	 */
	@Value("${dingtalk.app.encodingAesKey}")
	private String encodingAesKey;

	/**
	 * 钉钉程序对应的AppKey
	 */
	@Value("${dingtalk.app.appKey}")
	private String appKey;

	@PostMapping("/callBack")
	@ApiOperationSupport(order = 1)
	@ApiOperation(value = "测试回调", notes = "测试回调")
	@SneakyThrows
	public Map<String, String> callBack(
		@RequestParam(value = "msg_signature", required = false) String msg_signature,
		@RequestParam(value = "timestamp", required = false) String timeStamp,
		@RequestParam(value = "nonce", required = false) String nonce,
		@RequestBody(required = false) JSONObject json) {
			// 1. 从http请求中获取加解密参数

			// 2. 使用加解密类型
			// Constant.OWNER_KEY 说明:
			// 1、开发者后台配置的订阅事件为应用级事件推送,此时OWNER_KEY为应用的APP_KEY。
			// 2、调用订阅事件接口订阅的事件为企业级事件推送,
			//      此时OWNER_KEY为:企业的appkey(企业内部应用)或SUITE_KEY(三方应用)
			DingCallbackCrypto callbackCrypto = new DingCallbackCrypto(token, encodingAesKey, appKey);
			String encryptMsg = json.getString("encrypt");
			String decryptMsg = callbackCrypto.getDecryptMsg(msg_signature, timeStamp, nonce, encryptMsg);

			// 3. 反序列化回调事件json数据
			JSONObject eventJson = JSON.parseObject(decryptMsg);
			String eventType = eventJson.getString("EventType");

			// 4. 根据EventType分类处理
			if ("check_url".equals(eventType)) {
				// 测试回调url的正确性
			} else if ("label_conf_modify".equals(eventType)) {
			// 修改角色或者角色组
			JSONArray postLabelList = eventJson.getJSONArray("PostLabelList");
			Long timeStamp1 = eventJson.getLong("TimeStamp");
			System.out.println(timeStamp);
			System.out.println(DateUtil.formatDateTime(new Date(Long.parseLong(timeStamp))));
			System.out.println(timeStamp1);
			System.out.println(DateUtil.formatDateTime(new Date(timeStamp1)));
			for (Object o : postLabelList) {
				JSONObject jsonObject = JSON.parseObject((String)o);
				PostLabel postLabel = JSONObject.toJavaObject(jsonObject, PostLabel.class);
				//以修改时间为准,而不是回调时间
				//用钉钉的修改时间比较数据库当条记录的返回的修改时间,如果数据库存储的修改时间大于本次修改时间,则不做任何操作
				System.out.println("修改角色或者角色组:"+postLabel.getName()+",修改时间"+ DateUtil.formatDateTime(new Date(timeStamp1)));
			}
		} else {
			// 添加其他已注册的
		}

			// 5. 返回success的加密数据
			Map<String, String> successMap = callbackCrypto.getEncryptedMap("success");
			return successMap;

	}
}

PostLabel

import lombok.Data;

/**
 * @Description 钉钉角色或者组
 * @Author JunHao Huang
 * @Date 2023/6/1 14:16
 */
@Data
public class PostLabel {
	private Boolean hidden;
	private String name;
	private Long id;
}

配置文件

dingtalk:
  app:
    token: 在事件与回调菜单,应用随机生成的token
    encodingAesKey: 在事件与回调菜单,应用随机生成的aes_key
    appKey: 应用信息菜单,对应的AppKey

结尾

java 发送钉钉待办 java对接钉钉_java 发送钉钉待办_06