C#本地应用向钉钉推送信息并在钉钉上创建审批流完成审批
1.创建应用(https://open.dingtalk.com/document/isv/create-isvapp)
2.VS2019创建WEB API项目并部署到公网可以访问的IIS上
Nuget安装System.Text.Json;
using System.Net.Http; using System.Text; using System.Web.Http; namespace Multek.DingTalkService.WebApi.Controllers { [RoutePrefix("api/AppEvent")] public class AppEventController : ApiController { //public IHttpActionResult Post() //{ // AppService app = new AppService(); // var r = app.DingtalkCallback("", "66caf4181a353e39b7c186e923ecaf5c", "MAiNC53WwilSzJYfvyNluAa6UHtV7PBMoeGnWOeBgXt", "1467996111"); // var obj = new { success = r }; // return Json(obj); //} [HttpGet] public object GetAll() { return Ok<string>("Success"); } [HttpPost] public HttpResponseMessage PostData(int id) { return Request.CreateResponse(); } [Route("Order/SaveData")] [HttpPost] public object SaveData() { var a = "Ben"; return Ok<string>("Success"); } [Route("Order/DingdingCallback")] [HttpPost] public HttpResponseMessage DingdingCallback(string signature, string timestamp, string nonce) { JeffSoft.Logger.Error("DingdingCallback Success:" + signature + "," + timestamp + ""); //第一部分 //这两句代码是为了接收body体中传入的加密json串 Request.Content.ReadAsStreamAsync().Result.Seek(0, System.IO.SeekOrigin.Begin); string content = Request.Content.ReadAsStringAsync().Result; //反序列化json串拿去加密字符串 JToken json = JToken.Parse(content); string ever = json["encrypt"].ToString(); //实例化钉钉解密类构造参数为对应的 应用中的token、aes_key、AppKey值 DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor("896caad7fcfd398abc6b333c6156c769", "MAiNC53WwilSzJYfvyNluAa6UHtV7PBMoeGnWOeBgXt", "dinglqafhlqtb8zreom5"); //定义字符串接收解密后的值 string text = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, ever); JeffSoft.Logger.Error("dingTalkEncryptor.getDecryptMsg:" + text + ""); JToken jToken = JToken.Parse(text); //取出事件类型字段 string EventType = jToken["EventType"].ToString(); JeffSoft.Logger.Error("DingdingCallback Success,EventType:" + EventType + ""); //第二部分 // { //判断事件类型是否是日程事件 if ("bpms_instance_change" == EventType) { var DDtitle = jToken["title"].ToString(); var DDresult= jToken["result"].ToString(); var mes = string.Format("bpms_instance_change Success,title:{0},result:{1}", DDtitle, DDresult); JeffSoft.Logger.Error(mes); } //第三部分 //返回加密字符串 var msg = dingTalkEncryptor.getEncryptedMap("success"); var msg_signature = msg["msg_signature"]; var encrypt = msg["encrypt"]; var timeStamp = msg["timeStamp"]; var nonce1 = msg["nonce"]; var v = new { msg_signature = msg["msg_signature"], encrypt = msg["encrypt"], timeStamp = msg["timeStamp"], nonce = msg["nonce"], }; //var obj = new { success = r }; // return Json(obj); var data = JsonConvert.SerializeObject(v); //返回json数 return new HttpResponseMessage() { Content = new StringContent(data, Encoding.UTF8, "application/json"), }; } } }
DingTalkEncryptor.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Text; 6 using System.Security.Cryptography; 7 using System.Text.Json; 8 9 namespace DingDingWebAPI.Controllers 10 { 11 /** 12 * 钉钉开放平台加解密方法 13 */ 14 public class DingTalkEncryptor 15 { 16 //private static readonly Charset CHARSET = Charset.forName("utf-8"); 17 //private static readonly Base64 base64 = new Base64(); 18 private byte[] aesKey; 19 private String token; 20 private String corpId; 21 /**ask getPaddingBytes key固定长度**/ 22 private static readonly int AES_ENCODE_KEY_LENGTH = 43; 23 /**加密随机字符串字节长度**/ 24 private static readonly int RANDOM_LENGTH = 16; 25 26 /** 27 * 构造函数 28 * @param token 钉钉开放平台上,开发者设置的token 29 * @param encodingAesKey 钉钉开放台上,开发者设置的EncodingAESKey 30 * @param corpId 企业自建应用-事件订阅, 使用appKey 31 * 企业自建应用-注册回调地址, 使用corpId 32 * 第三方企业应用, 使用suiteKey 33 * 34 * @throws DingTalkEncryptException 执行失败,请查看该异常的错误码和具体的错误信息 35 */ 36 public DingTalkEncryptor(String token, String encodingAesKey, String corpId) 37 { 38 if (null == encodingAesKey || encodingAesKey.Length != AES_ENCODE_KEY_LENGTH) 39 { 40 throw new DingTalkEncryptException(DingTalkEncryptException.AES_KEY_ILLEGAL); 41 } 42 this.token = token; 43 this.corpId = corpId; 44 aesKey = Convert.FromBase64String(encodingAesKey + "="); 45 } 46 47 /** 48 * 将和钉钉开放平台同步的消息体加密,返回加密Map 49 */ 50 public Dictionary<String, String> getEncryptedMap(String plaintext) 51 { 52 53 var time = DateTime.Now.Millisecond; 54 return getEncryptedMap(plaintext, time); 55 } 56 57 /** 58 * 将和钉钉开放平台同步的消息体加密,返回加密Map 59 * @param plaintext 传递的消息体明文 60 * @param timeStamp 时间戳 61 * @param nonce 随机字符串 62 * @return 63 * @throws DingTalkEncryptException 64 */ 65 public Dictionary<String, String> getEncryptedMap(String plaintext, long timeStamp) 66 { 67 if (null == plaintext) 68 { 69 throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL); 70 } 71 var nonce = Utils.getRandomStr(RANDOM_LENGTH); 72 if (null == nonce) 73 { 74 throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_NONCE_ILLEGAL); 75 } 76 77 String encrypt = this.encrypt(nonce, plaintext); 78 String signature = getSignature(token, timeStamp.ToString(), nonce, encrypt); 79 Dictionary<String, String> resultMap = new Dictionary<String, String>(); 80 resultMap["msg_signature"] = signature; 81 resultMap["encrypt"] = encrypt; 82 resultMap["timeStamp"] = timeStamp.ToString(); 83 resultMap["nonce"] = nonce; 84 return resultMap; 85 } 86 87 /** 88 * 密文解密 89 * @param msgSignature 签名串 90 * @param timeStamp 时间戳 91 * @param nonce 随机串 92 * @param encryptMsg 密文 93 * @return 解密后的原文 94 * @throws DingTalkEncryptException 95 */ 96 public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg) 97 { 98 //校验签名 99 String signature = getSignature(token, timeStamp, nonce, encryptMsg); 100 if (!signature.Equals(msgSignature)) 101 { 102 throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR); 103 } 104 // 解密 105 String result = decrypt(encryptMsg); 106 return result; 107 } 108 109 110 /* 111 * 对明文加密. 112 * @param text 需要加密的明文 113 * @return 加密后base64编码的字符串 114 */ 115 private String encrypt(String random, String plaintext) 116 { 117 try 118 { 119 byte[] randomBytes = System.Text.Encoding.UTF8.GetBytes(random);// random.getBytes(CHARSET); 120 byte[] plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plaintext);// plaintext.getBytes(CHARSET); 121 byte[] lengthByte = Utils.int2Bytes(plainTextBytes.Length); 122 byte[] corpidBytes = System.Text.Encoding.UTF8.GetBytes(corpId);// corpId.getBytes(CHARSET); 123 //MemoryStream byteStream = new MemoryStream(); 124 var bytestmp = new List<byte>(); 125 bytestmp.AddRange(randomBytes); 126 bytestmp.AddRange(lengthByte); 127 bytestmp.AddRange(plainTextBytes); 128 bytestmp.AddRange(corpidBytes); 129 byte[] padBytes = PKCS7Padding.getPaddingBytes(bytestmp.Count); 130 bytestmp.AddRange(padBytes); 131 byte[] unencrypted = bytestmp.ToArray(); 132 133 RijndaelManaged rDel = new RijndaelManaged(); 134 rDel.Mode = CipherMode.CBC; 135 rDel.Padding = PaddingMode.Zeros; 136 rDel.Key = aesKey; 137 rDel.IV = aesKey.ToList().Take(16).ToArray(); 138 ICryptoTransform cTransform = rDel.CreateEncryptor(); 139 byte[] resultArray = cTransform.TransformFinalBlock(unencrypted, 0, unencrypted.Length); 140 return Convert.ToBase64String(resultArray, 0, resultArray.Length); 141 142 143 //Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); 144 //SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); 145 //IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16); 146 //cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); 147 //byte[] encrypted = cipher.doFinal(unencrypted); 148 //String result = base64.encodeToString(encrypted); 149 //return result; 150 } 151 catch (Exception e) 152 { 153 throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR); 154 } 155 } 156 157 /* 158 * 对密文进行解密. 159 * @param text 需要解密的密文 160 * @return 解密得到的明文 161 */ 162 private String decrypt(String text) 163 { 164 byte[] originalArr; 165 try 166 { 167 byte[] toEncryptArray = Convert.FromBase64String(text); 168 RijndaelManaged rDel = new RijndaelManaged(); 169 rDel.Mode = CipherMode.CBC; 170 rDel.Padding = PaddingMode.Zeros; 171 rDel.Key = aesKey; 172 rDel.IV = aesKey.ToList().Take(16).ToArray(); 173 ICryptoTransform cTransform = rDel.CreateDecryptor(); 174 originalArr = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length); 175 //return System.Text.UTF8Encoding.UTF8.GetString(resultArray); 176 177 //// 设置解密模式为AES的CBC模式 178 //Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); 179 //SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); 180 //IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); 181 //cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); 182 //// 使用BASE64对密文进行解码 183 //byte[] encrypted = Base64.decodeBase64(text); 184 //// 解密 185 //originalArr = cipher.doFinal(encrypted); 186 } 187 catch (Exception e) 188 { 189 throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_ERROR); 190 } 191 192 String plainText; 193 String fromCorpid; 194 try 195 { 196 // 去除补位字符 197 byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr); 198 Console.Out.WriteLine("bytes size:" + bytes.Length); 199 200 // 分离16位随机字符串,网络字节序和corpId 201 byte[] networkOrder = bytes.Skip(16).Take(4).ToArray();// Arrays.copyOfRange(bytes, 16, 20); 202 for (int i = 0; i < 4; i++) 203 { 204 Console.Out.WriteLine("networkOrder size:" + (int)networkOrder[i]); 205 } 206 207 Console.Out.WriteLine("bytes plainText:" + networkOrder.Length + " " + JsonSerializer.Serialize(networkOrder)); 208 int plainTextLegth = Utils.bytes2int(networkOrder); 209 Console.Out.WriteLine("bytes size:" + plainTextLegth); 210 211 plainText = System.Text.UTF8Encoding.UTF8.GetString(bytes.Skip(20).Take(plainTextLegth).ToArray()); // new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET); 212 fromCorpid = System.Text.UTF8Encoding.UTF8.GetString(bytes.Skip(20 + plainTextLegth).ToArray()); //new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET); 213 Console.Out.WriteLine("bytes plainText:" + plainText); 214 215 } 216 catch (Exception e) 217 { 218 throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR); 219 } 220 Console.Out.WriteLine(fromCorpid + "=====" + corpId); 221 222 223 // corpid不相同的情况 224 if (!fromCorpid.Equals(corpId)) 225 { 226 throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR); 227 } 228 return plainText; 229 } 230 231 /** 232 * 数字签名 233 * @param token isv token 234 * @param timestamp 时间戳 235 * @param nonce 随机串 236 * @param encrypt 加密文本 237 * @return 238 * @throws DingTalkEncryptException 239 */ 240 public String getSignature(String token, String timestamp, String nonce, String encrypt) 241 { 242 try 243 { 244 Console.Out.WriteLine(encrypt); 245 246 String[] array = new String[] { token, timestamp, nonce, encrypt }; 247 Array.Sort(array, StringComparer.Ordinal); 248 //var tmparray = array.ToList(); 249 //tmparray.Sort(new JavaStringComper()); 250 //array = tmparray.ToArray(); 251 Console.Out.WriteLine("array:" + JsonSerializer.Serialize(array)); 252 StringBuilder sb = new StringBuilder(); 253 for (int i = 0; i < 4; i++) 254 { 255 sb.Append(array[i]); 256 } 257 String str = sb.ToString(); 258 Console.Out.WriteLine(str); 259 //MessageDigest md = MessageDigest.getInstance("SHA-1"); 260 //md.update(str.getBytes()); 261 //byte[] digest = md.digest(); 262 System.Security.Cryptography.SHA1 hash = System.Security.Cryptography.SHA1.Create(); 263 System.Text.Encoding encoder = System.Text.Encoding.ASCII; 264 byte[] combined = encoder.GetBytes(str); 265 ////byte 转换 266 //sbyte[] myByte = new sbyte[] 267 //byte[] mySByte = new byte[myByte.Length]; 268 269 270 271 //for (int i = 0; i < myByte.Length; i++) 272 273 //{ 274 275 // if (myByte[i] > 127) 276 277 // mySByte[i] = (sbyte)(myByte[i] - 256); 278 279 // else 280 281 // mySByte[i] = (sbyte)myByte[i]; 282 283 //} 284 285 byte[] digest = hash.ComputeHash(combined); 286 StringBuilder hexstr = new StringBuilder(); 287 String shaHex = ""; 288 for (int i = 0; i < digest.Length; i++) 289 { 290 shaHex = ((int)digest[i]).ToString("x");// Integer.toHexString(digest[i] & 0xFF); 291 if (shaHex.Length < 2) 292 { 293 hexstr.Append(0); 294 } 295 hexstr.Append(shaHex); 296 } 297 return hexstr.ToString(); 298 } 299 catch (Exception e) 300 { 301 throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR); 302 } 303 } 304 } 305 306 307 /** 308 * 钉钉开放平台加解密异常类 309 */ 310 public class DingTalkEncryptException : Exception 311 { 312 /**成功**/ 313 public static readonly int SUCCESS = 0; 314 /**加密明文文本非法**/ 315 public readonly static int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001; 316 /**加密时间戳参数非法**/ 317 public readonly static int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002; 318 /**加密随机字符串参数非法**/ 319 public readonly static int ENCRYPTION_NONCE_ILLEGAL = 900003; 320 /**不合法的aeskey**/ 321 public readonly static int AES_KEY_ILLEGAL = 900004; 322 /**签名不匹配**/ 323 public readonly static int SIGNATURE_NOT_MATCH = 900005; 324 /**计算签名错误**/ 325 public readonly static int COMPUTE_SIGNATURE_ERROR = 900006; 326 /**计算加密文字错误**/ 327 public readonly static int COMPUTE_ENCRYPT_TEXT_ERROR = 900007; 328 /**计算解密文字错误**/ 329 public readonly static int COMPUTE_DECRYPT_TEXT_ERROR = 900008; 330 /**计算解密文字长度不匹配**/ 331 public readonly static int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009; 332 /**计算解密文字corpid不匹配**/ 333 public readonly static int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010; 334 335 private static Dictionary<int, String> msgMap = new Dictionary<int, String>(); 336 static DingTalkEncryptException() 337 { 338 msgMap[SUCCESS] = "成功"; 339 msgMap[ENCRYPTION_PLAINTEXT_ILLEGAL] = "加密明文文本非法"; 340 msgMap[ENCRYPTION_TIMESTAMP_ILLEGAL] = "加密时间戳参数非法"; 341 msgMap[ENCRYPTION_NONCE_ILLEGAL] = "加密随机字符串参数非法"; 342 msgMap[SIGNATURE_NOT_MATCH] = "签名不匹配"; 343 msgMap[COMPUTE_SIGNATURE_ERROR] = "签名计算失败"; 344 msgMap[AES_KEY_ILLEGAL] = "不合法的aes key"; 345 msgMap[COMPUTE_ENCRYPT_TEXT_ERROR] = "计算加密文字错误"; 346 msgMap[COMPUTE_DECRYPT_TEXT_ERROR] = "计算解密文字错误"; 347 msgMap[COMPUTE_DECRYPT_TEXT_LENGTH_ERROR] = "计算解密文字长度不匹配"; 348 msgMap[COMPUTE_DECRYPT_TEXT_CORPID_ERROR] = "计算解密文字corpid不匹配"; 349 } 350 351 private int code; 352 public DingTalkEncryptException(int exceptionCode) : base(msgMap[exceptionCode]) 353 { 354 this.code = exceptionCode; 355 } 356 } 357 358 /* 359 * PKCS7算法的加密填充 360 */ 361 public class PKCS7Padding 362 { 363 //private readonly static Charset CHARSET = Charset.forName("utf-8"); 364 private readonly static int BLOCK_SIZE = 32; 365 366 /** 367 * 填充mode字节 368 * @param count 369 * @return 370 */ 371 public static byte[] getPaddingBytes(int count) 372 { 373 int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); 374 if (amountToPad == 0) 375 { 376 amountToPad = BLOCK_SIZE; 377 } 378 char padChr = chr(amountToPad); 379 String tmp = string.Empty; ; 380 for (int index = 0; index < amountToPad; index++) 381 { 382 tmp += padChr; 383 } 384 return System.Text.Encoding.UTF8.GetBytes(tmp); 385 } 386 387 /** 388 * 移除mode填充字节 389 * @param decrypted 390 * @return 391 */ 392 public static byte[] removePaddingBytes(byte[] decrypted) 393 { 394 int pad = (int)decrypted[decrypted.Length - 1]; 395 if (pad < 1 || pad > BLOCK_SIZE) 396 { 397 pad = 0; 398 } 399 //Array.Copy() 400 var output = new byte[decrypted.Length - pad]; 401 Array.Copy(decrypted, output, decrypted.Length - pad); 402 return output; 403 } 404 405 private static char chr(int a) 406 { 407 byte target = (byte)(a & 0xFF); 408 return (char)target; 409 } 410 411 } 412 413 /** 414 * 加解密工具类 415 */ 416 public class Utils 417 { 418 /** 419 * 420 * @return 421 */ 422 public static String getRandomStr(int count) 423 { 424 String baset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 425 Random random = new Random(); 426 StringBuilder sb = new StringBuilder(); 427 for (int i = 0; i < count; i++) 428 { 429 int number = random.Next(baset.Length); 430 sb.Append(baset[number]); 431 } 432 return sb.ToString(); 433 } 434 435 436 /* 437 * int转byte数组,高位在前 438 */ 439 public static byte[] int2Bytes(int count) 440 { 441 byte[] byteArr = new byte[4]; 442 byteArr[3] = (byte)(count & 0xFF); 443 byteArr[2] = (byte)(count >> 8 & 0xFF); 444 byteArr[1] = (byte)(count >> 16 & 0xFF); 445 byteArr[0] = (byte)(count >> 24 & 0xFF); 446 return byteArr; 447 } 448 449 /** 450 * 高位在前bytes数组转int 451 * @param byteArr 452 * @return 453 */ 454 public static int bytes2int(byte[] byteArr) 455 { 456 int count = 0; 457 for (int i = 0; i < 4; ++i) 458 { 459 count <<= 8; 460 count |= byteArr[i] & 255; 461 } 462 return count; 463 } 464 } 465 466 public class JavaStringComper : IComparer<string> 467 { 468 public int Compare(string x, string y) 469 { 470 return String.Compare(x, y); 471 } 472 } 473 474 475 }
3.配置事件订阅(https://open.dingtalk.com/document/orgapp-server/configure-event-subcription)
4.进入钉钉管理后台(https://oa.dingtalk.com/)创建审批表单模板(https://open.dingtalk.com/document/isvapp-server/create-or-modify-an-approval-form-template)
https://oa.dingtalk.com/ 工作台--》应用程序--》OA审批--》进入--》创建
5.发起审批实例(https://open.dingtalk.com/document/orgapp-server/initiate-approval)
得到得到accessToken(https://api.dingtalk.com/v1.0/oauth2/accessToken)
post:
{
"form_component_values": [
{
"name": "单号",
"value": "B1000006"
},
{
"name": "设备编号",
"value": "AOI10002"
},
{
"name": "维修员",
"value": "Ben"
},
],
"agent_id": 1496597138,
"process_code": "PROC-9B39B0F8-C8EA-4401-87A8-4FCBCA0FE13F", //对接审批流的process_code
//"cc_position": "FINISH",
//"approvers": "6526336,8181347",
//"cc_list": "6526336,8181347",
"dept_id": -1,
"approvers_v2": [ //设置审批人,会签、或签设置的审批人必须大于等于2个人
{
"task_action_type": "OR",
"user_ids": [
"8181347" ,
"6526336"
]
}
],
"originator_user_id": "8181347"
}