zoukankan      html  css  js  c++  java
  • 钉钉企业应用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免登流程图片,修正一处错误。

  • 相关阅读:
    阿铭每日一题 day 1 20180111
    计算机网络之基础链路
    android学习笔记 对话框合集
    android 音乐播放器简单实现
    Service的生命周期&Service绑定方法
    android学习笔记 Service
    android学习笔记 activity生命周期&任务栈&activity启动模式
    android 短信助手demo
    android 显示意图
    java 多线程断点下载demo
  • 原文地址:https://www.cnblogs.com/pleiades/p/7140318.html
Copyright © 2011-2022 走看看