一、写在前面
鉴权:钉钉提供了一些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。