来源: 钉钉企业应用C#开发笔记之一(免登) – Pleiades – 博客园
关于钉钉
钉钉是阿里推出的企业移动OA平台,本身提供了丰富的通用应用,同时其强大的后台API接入能力让企业接入自主开发的应用成为可能,可以让开发者实现几乎任何需要的功能。
近期因为工作需要研究了一下钉钉的接入,发现其接入文档、SDK都是基于java编写的,而我们的企业网站使用ASP.NET MVC(C#)开发,所以接入只能从头自己做SDK。
接入主要包括免登、获取数据、修改数据等接口。
免登流程
首先需要理解一下钉钉的免登流程,借用官方文档的图片:

是不是很熟悉?是的,基本是按照OAUTH的原理来的,版本嘛,里面有计算签名的部分,我觉得应该是OAUTH1.0。
有的读者会问,那第一步是不是应该跳转到第三方认证页面啊。我觉得“魔法”就藏在用来打开页面的钉钉内置浏览器里,在dd.config()这一步里,“魔法”就生效了。
其实简单来说,主要分为五步:
- 在你的Web服务器端调用api,传入CorpId和CorpSecret,获取accessToken,即访问令牌。
- 在服务器端调用api,传入accessToken,获取JsApiTicket,即JsApi的访问许可(门票)。
- 按照既定规则,在后台由JsApiTicket、NonceStr、Timestamp、本页面Url生成字符串,计算SHA1消息摘要,即签名Signature。
- 将AgentId、CorpId、Timestamp、NonceStr、Signature等参数传递到前台,在前台调用api,得到authCode,即授权码。
- 根据授权码,在前台或后台调用api,获得userId,进而再根据userId,调用api获取用户详细信息。
PS:为什么需要在后台完成一些api的调用呢?应该是因为js跨域调用的问题,我具体没有深究。
实践方法
理解了上述步骤,我对登陆过程的实现也大致有了一个设想,既然免登需要前后端一起来完成,那就添加一个专门的登陆页面,将登陆过程都在里面实现,将登陆结果写入到Session,并重定向回业务页面,即算完成。图示如下:

其中每个api的调用方式,在官方文档中都有说明。同时,我在阿里云开发者论坛找到了网友提供的SDK,有兴趣可以下载:钉钉非官方.Net SDK
另外,GitHub上还有官方的JQuery版免登开发Demo,可以参考:GitHub JQuery免登。
我参考的是.Net SDK,将其中的代码,提取出了我所需要的部分,做了简化处理。基本原理就是每次调用API都是发起HttpRequest,将结果做JSON反序列化。
核心代码如下:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Web;
5 using System.IO;
6 using Newtonsoft.Json;
7 using Newtonsoft.Json.Linq;
8 using DDApi.Model;
9
10 namespace DDApi
11 {
12 public static class DDHelper
13 {
14 public static string GetAccessToken(string corpId, string corpSecret)
15 {
16 string url = string.Format("https://oapi.dingtalk.com/gettoken?corpid={0}&corpsecret={1}", corpId, corpSecret);
17 try
18 {
19 string response = HttpRequestHelper.Get(url);
20 AccessTokenModel oat = Newtonsoft.Json.JsonConvert.DeserializeObject<AccessTokenModel>(response);
21
22 if (oat != null)
23 {
24 if (oat.errcode == 0)
25 {
26 return oat.access_token;
27 }
28 }
29 }
30 catch (Exception ex)
31 {
32 throw;
33 }
34 return string.Empty;
35 }
36
37 /* https://oapi.dingtalk.com/get_jsapi_ticket?access_token=79721ed2fc46317197e27d9bedec0425
38 *
39 * errmsg "ok"
40 * ticket "KJWkoWOZ0BMYaQzWFDF5AUclJOHgO6WvzmNNJTswpAMPh3S2Z98PaaJkRzkjsmT5HaYFfNkMdg8lFkvxSy9X01"
41 * expires_in 7200
42 * errcode 0
43 */
44 public static string GetJsApiTicket(string accessToken)
45 {
46 string url = string.Format("https://oapi.dingtalk.com/get_jsapi_ticket?access_token={0}", accessToken);
47 try
48 {
49 string response = HttpRequestHelper.Get(url);
50 JsApiTicketModel model = Newtonsoft.Json.JsonConvert.DeserializeObject<JsApiTicketModel>(response);
51
52 if (model != null)
53 {
54 if (model.errcode == 0)
55 {
56 return model.ticket;
57 }
58 }
59 }
60 catch (Exception ex)
61 {
62 throw;
63 }
64 return string.Empty;
65 }
66
67 public static long GetTimeStamp()
68 {
69 TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
70 return Convert.ToInt64(ts.TotalSeconds);
71 }
72
73 public static string GetUserId(string accessToken, string code)
74 {
75 string url = string.Format("https://oapi.dingtalk.com/user/getuserinfo?access_token={0}&code={1}", accessToken, code);
76 try
77 {
78 string response = HttpRequestHelper.Get(url);
79 GetUserInfoModel model = Newtonsoft.Json.JsonConvert.DeserializeObject<GetUserInfoModel>(response);
80
81 if (model != null)
82 {
83 if (model.errcode == 0)
84 {
85 return model.userid;
86 }
87 else
88 {
89 throw new Exception(model.errmsg);
90 }
91 }
92 }
93 catch (Exception ex)
94 {
95 throw;
96 }
97 return string.Empty;
98 }
99
100 public static string GetUserDetailJson(string accessToken, string userId)
101 {
102 string url = string.Format("https://oapi.dingtalk.com/user/get?access_token={0}&userid={1}", accessToken, userId);
103 try
104 {
105 string response = HttpRequestHelper.Get(url);
106 return response;
107 }
108 catch (Exception ex)
109 {
110 throw;
111 }
112 return null;
113 }
114
115 public static UserDetailInfo GetUserDetail(string accessToken, string userId)
116 {
117 string url = string.Format("https://oapi.dingtalk.com/user/get?access_token={0}&userid={1}", accessToken, userId);
118 try
119 {
120 string response = HttpRequestHelper.Get(url);
121 UserDetailInfo model = Newtonsoft.Json.JsonConvert.DeserializeObject<UserDetailInfo>(response);
122
123 if (model != null)
124 {
125 if (model.errcode == 0)
126 {
127 return model;
128 }
129 }
130 }
131 catch (Exception ex)
132 {
133 throw;
134 }
135 return null;
136 }
137
138 public static List<DepartmentInfo> GetDepartmentList(string accessToken, int parentId = 1)
139 {
140 string url = string.Format("https://oapi.dingtalk.com/department/list?access_token={0}", accessToken);
141 if (parentId >= 0)
142 {
143 url += string.Format("&id={0}", parentId);
144 }
145 try
146 {
147 string response = HttpRequestHelper.Get(url);
148 GetDepartmentListModel model = Newtonsoft.Json.JsonConvert.DeserializeObject<GetDepartmentListModel>(response);
149
150 if (model != null)
151 {
152 if (model.errcode == 0)
153 {
154 return model.department.ToList();
155 }
156 }
157 }
158 catch (Exception ex)
159 {
160 throw;
161 }
162 return null;
163 }
164 }
165 }
HttpRequestHelper View Code其中的Model,就不再一一贴出来了,大家可以根据官方文档自己建立,这里只举一个例子,即GetAccessToken的返回结果:
public class AccessTokenModel
{
public string access_token { get; set; }
public int errcode { get; set; }
public string errmsg { get; set; }
}
我创建了一个类DDApiService,将上述方法做了封装:
DDApiService View Code以上是底层核心部分。登录页面的实现在控制器DDController中,代码如下:
DDController View Code视图View的代码:
Login.cshtml View Code其中nonstr理论上最好应该每次都随机,留待读者去完成吧:-)
钉钉免登就是这样,只要弄懂了就会觉得其实不难,还顺便理解了OAUTH。
后续改进
这个流程没有考虑到AccessToken、JsApiTicket的有效期时间(2小时),因为整个过程就在一个页面中都完成了。如果想要进一步扩展,多次调用api的话,需要考虑到上述有效期。
如果为了图简便每都去获取AccessToken也是可以的,但是会增加服务器负担,而且api的调用频率是有限制的(1500次/s好像),所以应当采取措施控制。例如可以将AccessToken、JsApiTicket存放在this.HttpContext.Application[“accessToken”]中,每次判断有效期是否过期,如果过期就调用api重新申请一个。
以上就是这样,感谢阅读。
20170710编辑,更新mvc免登流程图片,修正一处错误。
Mikel
