釘釘企業應用C#開發筆記之一(免登)


關於釘釘

釘釘是阿里推出的企業移動OA平台,本身提供了豐富的通用應用,同時其強大的后台API接入能力讓企業接入自主開發的應用成為可能,可以讓開發者實現幾乎任何需要的功能。

近期因為工作需要研究了一下釘釘的接入,發現其接入文檔、SDK都是基於java編寫的,而我們的企業網站使用Asp.Net MVC(C#)開發,所以接入只能從頭自己做SDK。

接入主要包括免登、獲取數據、修改數據等接口。

免登流程

首先需要理解一下釘釘的免登流程,借用官方文檔的圖片:

釘釘免登流程圖

是不是很熟悉?是的,基本是按照OAUTH的原理來的,版本嘛,里面有計算簽名的部分,我覺得應該是OAUTH1.0。

有的讀者會問,那第一步是不是應該跳轉到第三方認證頁面啊。我覺得“魔法”就藏在用來打開頁面的釘釘內置瀏覽器里,在dd.config()這一步里,“魔法”就生效了。

 

其實簡單來說,主要分為五步:

  1. 在你的Web服務器端調用api,傳入CorpId和CorpSecret,獲取accessToken,即訪問令牌。
  2. 在服務器端調用api,傳入accessToken,獲取JsApiTicket,即JsApi的訪問許可(門票)。
  3. 按照既定規則,在后台由JsApiTicket、NonceStr、Timestamp、本頁面Url生成字符串,計算SHA1消息摘要,即簽名Signature。
  4. 將AgentId、CorpId、Timestamp、NonceStr、Signature等參數傳遞到前台,在前台調用api,得到authCode,即授權碼。
  5. 根據授權碼,在前台或后台調用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 }
 1 using System.IO;
 2 using System.Net;
 3 
 4 namespace DDApi
 5 {
 6     public class HttpRequestHelper
 7     {
 8         public static string Get(string url)
 9         {
10             WebRequest request = HttpWebRequest.Create(url);
11             WebResponse response = request.GetResponse();
12             Stream stream = response.GetResponseStream();
13             StreamReader reader = new StreamReader(stream);
14             string content = reader.ReadToEnd();
15             return content;
16         }
17 
18         public static string Post(string url)
19         {
20             WebRequest request = HttpWebRequest.Create(url);
21             request.Method = "POST";
22             WebResponse response = request.GetResponse();
23             Stream stream = response.GetResponseStream();
24             StreamReader reader = new StreamReader(stream);
25             string content = reader.ReadToEnd();
26             return content;
27         }
28     }
29 }
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,將上述方法做了封裝:

using DDApi.Model;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Security.Cryptography;
using System.Text;

namespace DDApi
{
    /// <summary>
    ///
    /// </summary>
    public class DDApiService
    {
        public static readonly DDApiService Instance = new DDApiService();

        public string CorpId { get; private set; }
        public string CorpSecret { get; private set; }
        public string AgentId { get; private set; }

        private DDApiService()
        {
            CorpId = ConfigurationManager.AppSettings["corpId"];
            CorpSecret = ConfigurationManager.AppSettings["corpSecret"];
            AgentId = ConfigurationManager.AppSettings["agentId"];
        }

        /// <summary>
        /// 獲取AccessToken
        /// 開發者在調用開放平台接口前需要通過CorpID和CorpSecret獲取AccessToken。
        /// </summary>
        /// <returns></returns>
        public string GetAccessToken()
        {
            return DDHelper.GetAccessToken(CorpId, CorpSecret);
        }

        public string GetJsApiTicket(string accessToken)
        {
            return DDHelper.GetJsApiTicket(accessToken);
        }

        public string GetUserId(string accessToken, string code)
        {
            return DDHelper.GetUserId(accessToken, code);
        }

        public UserDetailInfo GetUserDetail(string accessToken, string userId)
        {
            return DDHelper.GetUserDetail(accessToken, userId);
        }

        public string GetUserDetailJson(string accessToken, string userId)
        {
            return DDHelper.GetUserDetailJson(accessToken, userId);
        }

        public UserDetailInfo GetUserDetailFromJson(string jsonString)
        {
            UserDetailInfo model = Newtonsoft.Json.JsonConvert.DeserializeObject<UserDetailInfo>(jsonString);

            if (model != null)
            {
                if (model.errcode == 0)
                {
                    return model;
                }
            }
            return null;
        }

        public string GetSign(string ticket, string nonceStr, long timeStamp, string url)
        {
            String plain = string.Format("jsapi_ticket={0}&noncestr={1}&timestamp={2}&url={3}", ticket, nonceStr, timeStamp, url);

            try
            {
                byte[] bytes = Encoding.UTF8.GetBytes(plain);
                byte[] digest = SHA1.Create().ComputeHash(bytes);
                string digestBytesString = BitConverter.ToString(digest).Replace("-", "");
                return digestBytesString.ToLower();
            }
            catch (Exception e)
            {
                throw;
            }
        }

        public List<DepartmentInfo> GetDepartmentList(string accessToken, int parentId = 1)
        {
            return DDHelper.GetDepartmentList(accessToken, parentId);
        }
    }
}
DDApiService View Code

以上是底層核心部分。登錄頁面的實現在控制器DDController中,代碼如下:

using DDApi;
using DDApi.Model;
using System;
using System.Web.Mvc;

namespace AppointmentWebApp.Controllers
{
    public class DDController : Controller
    {
        //
        // GET: /DD/
        public ActionResult GetUserInfo(string accessToken, string code, bool setCurrentUser = true)
        {
            try
            {
                string userId = DDApiService.Instance.GetUserId(accessToken, code);
                string jsonString = DDApiService.Instance.GetUserDetailJson(accessToken, userId);
                UserDetailInfo userInfo = DDApiService.Instance.GetUserDetailFromJson(jsonString);
                if (setCurrentUser)
                {
                    Session["AccessToken"] = accessToken;
                    Session["CurrentUser"] = userInfo;
                }
                return Content(jsonString);
            }
            catch (Exception ex)
            {
                return Content(string.Format("{{'errcode': -1, 'errmsg':'{0}'}}", ex.Message));
            }
        }

        public ActionResult Login()
        {
            BeginDDAutoLogin();
            return View();
        }

        private void BeginDDAutoLogin()
        {
            string nonceStr = "helloDD";//todo:隨機
            ViewBag.NonceStr = nonceStr;
            string accessToken = DDApiService.Instance.GetAccessToken();
            ViewBag.AccessToken = accessToken;
            string ticket = DDApiService.Instance.GetJsApiTicket(accessToken);
            long timeStamp = DDHelper.GetTimeStamp();
            string url = Request.Url.ToString();
            string signature = DDApiService.Instance.GetSign(ticket, nonceStr, timeStamp, url);

            ViewBag.JsApiTicket = ticket;
            ViewBag.Signature = signature;
            ViewBag.NonceStr = nonceStr;
            ViewBag.TimeStamp = timeStamp;
            ViewBag.CorpId = DDApiService.Instance.CorpId;
            ViewBag.CorpSecret = DDApiService.Instance.CorpSecret;
            ViewBag.AgentId = DDApiService.Instance.AgentId;
        }
    }
}
DDController View Code

視圖View的代碼:

@{
    ViewBag.Title = "Login";
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <h2 id="notice">正在登錄...</h2>
    <script src="//cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script type="text/javascript" src="http://g.alicdn.com/dingding/open-develop/1.5.1/dingtalk.js"></script>
    <script type="text/javascript">
        var _config = [];
        _config.agentId = "@ViewBag.AgentId";
        _config.corpId = "@ViewBag.CorpId";
        _config.timeStamp = "@ViewBag.TimeStamp";
        _config.nonceStr = "@ViewBag.NonceStr";
        _config.signature = "@ViewBag.Signature";

        dd.config({
            agentId: _config.agentId,
            corpId: _config.corpId,
            timeStamp: _config.timeStamp,
            nonceStr: _config.nonceStr,
            signature: _config.signature,
            jsApiList: ['runtime.info', 'biz.contact.choose',
                    'device.notification.confirm', 'device.notification.alert',
                    'device.notification.prompt', 'biz.ding.post',
                    'biz.util.openLink']
        });

        dd.ready(function () {
            dd.runtime.info({
                onSuccess: function (info) {
                    logger.e('runtime info: ' + JSON.stringify(info));
                },
                onFail: function (err) {
                    logger.e('fail: ' + JSON.stringify(err));
                }
            });

            dd.runtime.permission.requestAuthCode({
                corpId: _config.corpId,
                onSuccess: function (info) {//成功獲得code值,code值在info中  
                    //alert('authcode: ' + info.code);
                    //alert('token: @ViewBag.AccessToken');
                    /* 
                    *$.ajax的是用來使得當前js頁面和后台服務器交互的方法 
                    *參數url:是需要交互的后台服務器處理代碼,這里的userinfo對應WEB-INF -> classes文件中的UserInfoServlet處理程序 
                    *參數type:指定和后台交互的方法,因為后台servlet代碼中處理Get和post的doGet和doPost 
                    *原本需要傳輸的參數可以用data來存儲的,格式為data:{"code":info.code,"corpid":_config.corpid} 
                    *其中success方法和error方法是回調函數,分別表示成功交互后和交互失敗情況下處理的方法 
                    */
                    $.ajax({
                        url: '@Url.Action("GetUserInfo", "DD")?code=' + info.code + '&accessToken=@ViewBag.AccessToken',//userinfo為本企業應用服務器后台處理程序  
                        type: 'GET',
                        /* 
                        *ajax中的success為請求得到相應后的回調函數,function(response,status,xhr) 
                        *response為響應的數據,status為請求狀態,xhr包含XMLHttpRequest對象 
                        */
                        success: function (data, status, xhr) {
                            alert(data);
                            var info = JSON.parse(data);
                            if (info.errcode != 0) {
                                alert(data);
                            } else {
                                //alert("當前用戶:" + info.name);
                                $('#notice').text("歡迎您:" + info.name + "。瀏覽器正在自動跳轉...");
                                location.href = "@Url.Action("Index", "Home")";
                            }
                        },
                        error: function (xhr, errorType, error) {
                            logger.e("嘗試獲取用戶信息失敗:" + info.code);
                            alert(errorType + ', ' + error);
                        }
                    });

                },
                onFail: function (err) {//獲得code值失敗  
                    alert('fail: ' + JSON.stringify(err));
                }
            });
        });
        dd.error(function (err) {
            alert('dd error: ' + JSON.stringify(err));
        });
    </script>
</body>
</html>
Login.cshtml View Code

其中nonstr理論上最好應該每次都隨機,留待讀者去完成吧:-)

釘釘免登就是這樣,只要弄懂了就會覺得其實不難,還順便理解了OAUTH。

后續改進

這個流程沒有考慮到AccessToken、JsApiTicket的有效期時間(2小時),因為整個過程就在一個頁面中都完成了。如果想要進一步擴展,多次調用api的話,需要考慮到上述有效期。

如果為了圖簡便每都去獲取AccessToken也是可以的,但是會增加服務器負擔,而且api的調用頻率是有限制的(1500次/s好像),所以應當采取措施控制。例如可以將AccessToken、JsApiTicket存放在this.HttpContext.Application["accessToken"]中,每次判斷有效期是否過期,如果過期就調用api重新申請一個。

以上就是這樣,感謝閱讀。

 


 

20170710編輯,更新mvc免登流程圖片,修正一處錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM