C#实现钉钉企业应用鉴权

一、写在前面

    鉴权:钉钉提供了一些Native能力的JSAPI,这些API有很多是手机的基础能力,对这些api的调用不需要进行鉴权(即不需要进行dd.config),只需要保证在dd.ready里面调用即可。对于一些钉钉业务相关、安全相关的API调用,需要开发者先进行鉴权后再调用。官方文档地址:企业应用鉴权


    C#实现钉钉企业应用鉴权,主要分为以下步骤:

    1、鉴权-获取access_token:

        1. 在企业管理后台:https://oa.dingtalk.com/上注册企业完成之后,在企业应用-工作台设置页面里面,可以获取到企业的corpId和CorpSecret。
        2.通过调用获取access_token的接口获取企业的access_token。

    2、鉴权-获取jsticket:  

        企业应用如果配置了IP白名单,则请求域名的地址必须在IP白名单里面,且2小时之内通过接口重新请求的jsticket值都会变。如果企业没有配置IP白名单,则2小时之内通过接口重新请求的jsticket值不会变,只是jsticket值的生命周期重新延长2小时。通过调用获取jsticket的接口获取企业的jsticket。

    3、鉴权-获取签名参数:

        在前端进行免登鉴权之前,我们要先拿到一些免登鉴权的参数,主要有'url','nonceStr','agentId','timeStamp','corpId'

参数 说明 企业
url 当前网页的URL,不包含#及其后面部分 //
nonceStr 随机串,自己定义 //
agentId 应用的标识 编辑企业应用可以看到
timeStamp 时间戳 当前时间,但是前端和服务端进行校验时候的值要一致
corpId 企业ID

企业ID,在//open-dev.dingtalk.com/上企业视图下开发者账号设置里面可以看到

    4、鉴权-计算签名信息

        在服务端通过sign(ticket, nonceStr, timeStamp, url)计算前端校验需要使用的签名信息。

二、代码实现

    1、通用类:

    public class DDHelper
    {
        private static String dd_host = ConfigurationManager.AppSettings["DDHost"];//https://oapi.dingtalk.com/
        private static String dd_corpid = ConfigurationManager.AppSettings["DD_corpid"];
        private static String dd_corpsecret = ConfigurationManager.AppSettings["DD_corpsecret"];
        private static String dd_accesstoken = string.Empty;
        private static DateTime dd_accesstokentime;
        public static String appSecret;
        /// <summary>
        /// 发起请求
        /// </summary>
        /// <param name="url">地址</param>
        /// <param name="data">数据</param>
        /// <param name="reqtype">请求类型</param>
        /// <returns></returns>
        private String Request(string url, string data, string reqtype)
        {
            if (url.IndexOf('?') == -1 && url != "gettoken")
                url += ("?access_token=" + dd_accesstoken);
            else if (url.IndexOf('?') > -1 && url.IndexOf("gettoken") == -1)
                url += ("&access_token=" + dd_accesstoken);
            HttpWebRequest web = (HttpWebRequest)HttpWebRequest.Create(dd_host + url);
            web.ContentType = "application/json";
            web.Method = reqtype;
            if (data.Length > 0 && reqtype.Trim().ToUpper() == "POST")
            {
                byte[] postBytes = Encoding.UTF8.GetBytes(data);
                web.ContentLength = postBytes.Length;
                using (Stream reqStream = web.GetRequestStream())
                {
                    reqStream.Write(postBytes, 0, postBytes.Length);
                }
            }
            string html = string.Empty;
            using (HttpWebResponse response = (HttpWebResponse)web.GetResponse())
            {
                Stream responseStream = response.GetResponseStream();
                StreamReader streamReader = new StreamReader(responseStream, Encoding.UTF8);
                html = streamReader.ReadToEnd();
            }
            return html;
        }
        /// <summary>
        /// 给TOP请求签名。
        /// </summary>
        /// <param name="parameters">所有字符型的TOP请求参数</param>
        /// <param name="secret">签名密钥</param>
        /// <param name="signMethod">签名方法,可选值:md5, hmac</param>
        /// <returns>签名</returns>
        public static string SignTopRequest(IDictionary<string, string> parameters, string secret, string signMethod)
        {
            // 第一步:把字典按Key的字母顺序排序
            IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters, StringComparer.Ordinal);
            IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();

            // 第二步:把所有参数名和参数值串在一起
            StringBuilder query = new StringBuilder();
            if (Constants.SIGN_METHOD_MD5.Equals(signMethod))
            {
                query.Append(secret);
            }
            while (dem.MoveNext())
            {
                string key = dem.Current.Key;
                string value = dem.Current.Value;
                if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
                {
                    query.Append(key).Append(value);
                }
            }

            // 第三步:使用MD5/HMAC加密
            byte[] bytes;
            if (Constants.SIGN_METHOD_HMAC.Equals(signMethod))
            {
                HMACMD5 hmac = new HMACMD5(Encoding.UTF8.GetBytes(secret));
                bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(query.ToString()));
            }
            else
            {
                query.Append(secret);
                MD5 md5 = MD5.Create();
                bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(query.ToString()));
            }

            // 第四步:把二进制转化为大写的十六进制
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < bytes.Length; i++)
            {
                result.Append(bytes[i].ToString("X2"));
            }

            return result.ToString();
        }
        ///<summary>
        ///生成随机字符串 
        ///</summary>
        ///<param name="length">目标字符串的长度</param>
        ///<param name="useNum">是否包含数字,1=包含,默认为包含</param>
        ///<param name="useLow">是否包含小写字母,1=包含,默认为包含</param>
        ///<param name="useUpp">是否包含大写字母,1=包含,默认为包含</param>
        ///<param name="useSpe">是否包含特殊字符,1=包含,默认为不包含</param>
        ///<param name="custom">要包含的自定义字符,直接输入要包含的字符列表</param>
        ///<returns>指定长度的随机字符串</returns>
        public static string GetRandomString(int length, bool useNum, bool useLow, bool useUpp, bool useSpe, string custom)
        {
            byte[] b = new byte[4];
            new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(b);
            Random r = new Random(BitConverter.ToInt32(b, 0));
            string s = null, str = custom;
            if (useNum == true) { str += "0123456789"; }
            if (useLow == true) { str += "abcdefghijklmnopqrstuvwxyz"; }
            if (useUpp == true) { str += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }
            if (useSpe == true) { str += "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; }
            for (int i = 0; i < length; i++)
            {
                s += str.Substring(r.Next(0, str.Length - 1), 1);
            }
            return s;
        }
        public class AccessTokenModel
        {
            public string access_token { get; set; }

            public int errcode { get; set; }

            public string errmsg { get; set; }
        }
        public sealed class Constants
        {
            public const string CHARSET_UTF8 = "utf-8";

            public const string DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
            public const string DATE_TIME_MS_FORMAT = "yyyy-MM-dd HH:mm:ss.fff";

            public const string SIGN_METHOD_MD5 = "md5";
            public const string SIGN_METHOD_HMAC = "hmac";
            public const string SIGN_METHOD_HMAC_SHA256 = "hmac-sha256";

            public const string LOG_SPLIT = "^_^";
            public const string LOG_FILE_NAME = "topsdk.log";

            public const string ACCEPT_ENCODING = "Accept-Encoding";
            public const string CONTENT_ENCODING = "Content-Encoding";
            public const string CONTENT_ENCODING_GZIP = "gzip";

            public const string ERROR_RESPONSE = "error_response";
            public const string ERROR_CODE = "code";
            public const string ERROR_MSG = "msg";

            public const string QIMEN_CLOUD_ERROR_RESPONSE = "response";
            public const string QIMEN_CLOUD_ERROR_CODE = "code";
            public const string QIMEN_CLOUD_ERROR_MSG = "message";

            public const string SDK_VERSION = "top-sdk-net-20180118";
            public const string SDK_VERSION_CLUSTER = "top-sdk-net-cluster-20180118";

            public const string APP_KEY = "app_key";
            public const string FORMAT = "format";
            public const string METHOD = "method";
            public const string TIMESTAMP = "timestamp";
            public const string VERSION = "v";
            public const string SIGN = "sign";
            public const string SIGN_METHOD = "sign_method";
            public const string PARTNER_ID = "partner_id";
            public const string SESSION = "session";
            public const string FORMAT_XML = "xml";
            public const string FORMAT_JSON = "json";
            public const string SIMPLIFY = "simplify";
            public const string TARGET_APP_KEY = "target_app_key";

            public const string QM_ROOT_TAG_REQ = "request";
            public const string QM_ROOT_TAG_RSP = "response";
            public const string QM_CUSTOMER_ID = "customerId";
            public const string QM_CONTENT_TYPE = "text/xml;charset=utf-8";
            public const string QM_TARGETAPPKEY = "targetAppkey";

            public const string CTYPE_DEFAULT = "application/octet-stream";
            public const string CTYPE_FORM_DATA = "application/x-www-form-urlencoded";
            public const string CTYPE_FILE_UPLOAD = "multipart/form-data";
            public const string CTYPE_TEXT_XML = "text/xml";
            public const string CTYPE_TEXT_PLAIN = "text/plain";
            public const string CTYPE_APP_JSON = "application/json";
            public const int READ_BUFFER_SIZE = 1024 * 4;
        }
    }

    2、获取access_token。钉钉为AccessToken提供的有效时长为7200s,在有效时间内每次请求都将自动延时,而钉钉提的要求是不允许对AccessToken进行高频率请求。所以我们可以采用缓存的方式,将AccessToken缓存起来,缓存时间少于7200s,即可在每次失效前再次延长。

        /// <summary>
        /// 更新AccessToken
        /// </summary>
        public static void GetAccessToken()
        {
            //从缓存中获取Token,如果缓存中已经过期,再从接口获取;
            object DDToken = CacheHelper.GetCache("dd_accesstoken");
            if (DDToken == null || DDToken == "")
            {
                //获取Token;
                if (dd_accesstokentime == null || (DateTime.Now.Ticks - dd_accesstokentime.Ticks) >= 5000)
                {
                    dd_accesstokentime = DateTime.Now;
                    DDHelper tempDDHelper = new DDHelper();
                    dd_accesstoken = JsonConvert.DeserializeObject<AccessTokenModel>(tempDDHelper.Request("gettoken?corpid=" + dd_corpid + "&corpsecret=" + dd_corpsecret, "", "GET")).access_token;
                }
                //将Token存入缓存
                CacheHelper.AddCache("dd_accesstoken", dd_accesstoken, 115);
            }
            else
                dd_accesstoken = DDToken.ToString();
        }

    3、获取jsticket。

        /// <summary>
        /// 获取免登录令牌
        /// </summary>
        /// <returns></returns>
        public static String GetTicket()
        {
            GetAccessToken();
            DDHelper tempDDHelper = new DDHelper();
            string json = tempDDHelper.Request("get_jsapi_ticket?type=jsapi", "", "GET");
            JObject jo = JsonConvert.DeserializeObject<JObject>(json);
            if (jo["errcode"].Value<Int32>() == 0)
                return jo["ticket"].Value<String>();
            else
            {
                Log4Helper.WriteInfoLog(StoreType.TextFile, json);
            return "";
        }
        }

    4、获取签名参数,计算签名信息。

        /// <summary>
        /// 登录获取签名
        /// </summary>
        /// <param name="url">获取的url</param>
        /// <returns>成功时,返回签名信息</returns> 
        [AnonymousAttribute]
        public ResultObject DDGetSign(string url)
        {
            try
            {
                String jsTicket = DDHelper.GetTicket();
                if (jsTicket != "")
                {
                    String noncestr = DDHelper.GetRandomString(20, true, true, true, false, "");
                    String jsUrl = System.Web.HttpUtility.UrlDecode(url);
                    int timeStamp = Convert.ToInt32((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds);
                    String plainTex = "jsapi_ticket=" + jsTicket + "&noncestr=" + noncestr + "×tamp=" + timeStamp.ToString() + "&url=" + jsUrl;
                    object SignObj = new object();
                    SignObj = new
                    {
                        corpId = ConfigurationManager.AppSettings["DD_corpid"],
                        nonceStr = noncestr,
                        timeStamp = timeStamp,
                        signature = EncryptHelper.SHA1(plainTex).ToLower()
                    };
                    return new ResultObject { result = SignObj, code = 1, message = "获取成功!" };
                }
                else
                {
                    return new ResultObject { result = null, code = 9, message = "获取Ticket失败!" };
                }
            }
            catch( Exception ex) 
            {
                return new ResultObject { result = null, code = 10, message = "获取Ticket出现异常!" };
            }
        }
    /// <summary>
    /// 返回结果对象
    /// </summary>
    public class ResultObject
    {
        public ResultObject()
        {
            code = 1;
            timestamp = GetTimeStamp();
        }
        /// <summary>
        /// 代码
        /// </summary>
        public int code { get; set; }
        /// <summary>
        /// 消息
        /// </summary>
        public string message { get; set; }
        /// <summary>
        /// 时间戳
        /// </summary>
        public string timestamp { get; set; }
        /// <summary>
        /// 结果
        /// </summary>
        public object result { get; set; }

        /// <summary>  
        /// 获取时间戳  
        /// </summary>  
        /// <returns></returns>  
        protected string GetTimeStamp()
        {
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalMilliseconds).ToString();
        }  
    }

    5、将corpid、nonceStr、timeStamp、signature都返回给前端,前端进行对比鉴权即完成。


本文属于个人原创作品,属于个人完成公司项目之后的含泪总结,谢绝转载、抄袭。

如果您有疑问或者希望沟通交流,可以联系QQ:865562060。

猜你喜欢

转载自blog.csdn.net/qq_23009105/article/details/80942472