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"),
            };
        }
    }
}
View Code

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 }
View Code

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:

{
    "appKey":"dinglqafhlqtb8zreom5",
    "appSecret":"SE93VaqKzjSoCoyhiPmigPrrZO7J5fR0-efFlsJXg7OrZPiqH6C-LlGX0gy4QpD1"
}
返回
{
    "expireIn": 7200,
    "accessToken": "9015791f471e34aa89804286dee0a1eb"
}
发起审批实例
https://oapi.dingtalk.com/topapi/processinstance/create?access_token=9015791f471e34aa89804286dee0a1eb
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"
}

posted @ 2022-03-16 16:30  德平Zeng  阅读(1097)  评论(0编辑  收藏  举报