• 钉钉微应用的开发——主前端


    钉钉微应用的开发——主前端

    经济基础决定上层建筑。

    第一步,这个地方如果当做一般的数据请求来看,没有什么要说的,用jquery或者zepto的ajax请求都可以很快速实现。就说一下我在这个踩的坑,我在使用ajax异步请求的时候,忽略了异步加载然而同步加载不停止的问题。深入这个话题我也还需要去学去实践,所以只是简单说明我的问题,后面会提供链接去深入学习。下面的代码,由于我没实现手机电脑联调,所以只能退而其次用alert测试,见谅。js在加载的时候,会先全部同步顺序加载,但是ajax请求不会影响同步加载,因而,会按照123的alert弹出,而非顺序弹出312。由于的我浅薄理解,导致我后面还没拿到ajax请求到的_config,就开始执行函数DDConfig(_config)配置钉钉,所以一直不弹出任何弹窗。这个问题我开始解决的方案是将这段ajax单独放在一个script标签里面,最先引入,然后再配置钉钉鉴权信息,这个在Android上测试时正常的,然而只是侥幸,iOS不买这个账。所以使用ajax的complete函数,在这里面执行DDConfig(_config),可看源代码。

    var _config = null; // 定义全局变量_config,初始值为null,用来接收API获取到的签名信息

    var getConfig = $.ajax({

    type: 'POST',

    url: '获取企业签名的API,后台提供',

    data: {

    agentId: 109243825,

    url: '这是你开发微应用页面的线上地址,一般是由钉钉管理员配置的。',

    },

    dataType: 'json',

    success: function(data){

    console.log('---success-post-dingInfo---');

    if(data.status){

    _config = data.data;

    alert('3. API获取签名信息:'+JSON.stringify(_config));

    // 开始配置钉钉

    DDConfig(_config);

    }else{

    alert('请求信息出错');

    }

    },

    error: function(data){

    console.log(---error-post-dingInfo---);

    }

    });

    alert('1. API请求开始:'+JSON.stringify(getConfig));

    alert('2. 全局输出_config:'+JSON.stringify(_config));

    第二步,这里官方给出很详细的步骤钉钉移动jsapi开发,你需要使用的api放进dd.config的jsApiList里面即可。其实钉钉的jsapi思路是这样的。引入dingtalk.js(官方文档有提供)这个js会给你提供一个全局变量dd,你可以在Chrome的控制台打印出来看看是个什么东西,里面可以识别钉钉版本,手机系统,以及提供一个个api。钉钉移动jsapi里面介绍所有的api,分为无需鉴权api和需要鉴权api,无需鉴权api可以再引入dingtalk.js之后全局使用;鉴权api就需要走后端接口以保证安全性,且鉴权通过才可以使用这部分api。思路就是这样。

    问题1:如果你发现你的dd.ready/dd.error都没执行,那可能是我上一步遇见的问题,即没开始配置dd.config,却执行了dd.ready和dd.error,因为dd是全局变量,不受函数和异步限制,所以写法上面没有错,但是就是什么都不反应,很痛苦。还有一个很粗暴的方法去review你的代码,那就是清空js代码,不做ajax请求,直接开始钉钉鉴权,即dd.config、dd.ready、dd.error,这个时候你可以先用固定的鉴权信息(agentId,corpId,timeStamp,nonceStr,signature)去配置,这个时候因为不是实时的鉴权信息,所以肯定要直接进dd.error来提示校验失败,那么你就应该知道怎么一点点去排查你的错误了。

    问题2:如果你发现dd.error被执行了,先恭喜你一下,至少你进入钉钉的api了,哈哈哈哈哈哈哈哈。。。

    这个时候报错,说明你的dd.config里面有信息是错误的。那就去一个个打印出来检查。

    还有可能是上一步中的url的问题,比如说我的微应用的链接是https://open-doc.dingtalk.com/;那么url也必须完全一样,注意https也不能错的哦。可看前辈们的问题集锦钉钉开放平台“常见问题常见问题常见问题

    第三步,应该不用说了,只要鉴权通过,就可以直接用钉钉api获取用户的信息,只是这个信息很简单,不怎么涉及安全问题。可看源码。

    第四步,题外话。这个免登,是需要你在通过钉钉api拿到authcode之后再去找后端API请求,告诉他你需要免登获取更多用户的信息钉钉免登,这些都是涉及到安全性的了,所有涉及安全的问题都要走后端API。这个我也没做,暂时还没用到,以后若是开发有坑还会继续说。钉钉的生态说不上很稳定,但是由于公司用的多,所以很多东西不管是官方还是前辈们都写的有详细文档和代码,可以多搜搜。

    提交操作,可看源码。
    ————————————————
    版权声明:本文为CSDN博主「藍色珊瑚礁」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/weixin_34125336/article/details/112018023

    创建钉钉H5应用
    顾名思义,钉钉H5应用,和微信WEB应用一样,本质都是一个有前端有后端的网站,由平台本身对网站基础功能进行扩充,提供专用接口满足开发者各式各样的和平台相关的需求。 开发者平台:https://open-dev.dingtalk.com/ 先决条件:公司管理员和子管理员权限 创建应用的流程很简单,开发者平台里新建一个应用,再为应用配置域名、IP白名单、接口权限等信息即可。

    9665d57e2bc1c00e18ef11e94bed182f.png

    fd142ce6d3235529cdcb881e761ac659.png

    2dffb4e8cae7e854dad13d255a916441.png

    0f194304aea209b71f39702581b77daf.png

    75d686bcb60a76bbba5350b529b64102.png

    关于免登
    免登的关键在于如何识别用户,微信网页也好,微信小程序也好,钉钉也好,都开放了获取用户信息的接口,在这基础上做免登的流程是:向平台获取用户信息 -> 为用户登录。 微信网页获取用户信息的流程是:用户同意授权(scope=snsapi_userinfo时) -> 获取code -> 通过code换取网页授权access_token -> 拉取用户信息。在获取code时,本质是由微信客户端刷新页面,并在URL中添加CODE参数;此外,获取access_token时,scope参数如果是snsapi_base,可以进行无感知获取用户openid,所以只有当需要获取详细信息时,才会用scope=snsapi_userinfo来显示请求授权,其它场景中(不需要获取用户信息,或已经获取了对应openid的用户信息)只要使用snsapi_base即可。(官方文档地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html) 钉钉流程与之类似,区别如下:

        微信通过URL传递code,钉钉通过JSAPI的dd.runtime.permission.requestAuthCode接口获取code;

        不需要用户授权(真正意义上的无感知);

        直接获取用户信息而不需要scope字段。

    此外,因为平台性质的差异,钉钉的用户字段包含了丰富的真实个人信息。 签名校验

    微信的wx.config参数配置,和钉钉H5的dd.config参数配置,不管是校验流程、签名参数的参数名和,还是校验算法都完全一样。

    在整个过程中(包括其它开发步骤里),有一个非常重要的原则需要格外留意:敏感参数绝对不能出现在前端(比如jsapi_ticket、access_token)。

    流程如下:

        获取access_token

        获取jsapi_ticket

        计算签名(微信和钉钉均为jsapi_ticket, nonceStr, timeStamp, url)

        将生成签名的参数nonceStr,timeStamp, url和最终生成的签名Signature传到前端,供config接口配置和注册权限;除了这几个参数,dd.config还需要用到agentId(即应用ID)、corpId(即公司ID),wx.config需要用到appId,本质上都是用来标识一个应用。

    TOKEN的维护

    钉钉的Token有一个服务端缓存刷新机制,只要在失效前请求接口,access_token的过期时间会恢复为7200秒,借由这个机制,可以在后台跑一个定时任务,隔一段时间请求一下,就可以保证当前access_token一直有效。
    开发、部署流程(与微信WEB应用一样):

        开发阶段

            可以用自己熟悉的环境、熟悉的框架按普通的WEB开发过程进行前后端开发;

            在需要使用功能的前端页面引入核心JS-SDK;

            通过dd.config接口注入权限验证配置;

            调用钉钉JSAPI接口时,需要发起者的IP存在于H5应用后台配置的服务器出口IP列表中。

        部署阶段

            常规:把网站部署到服务器,配置DNS解析指向网站;

            登入开发者平台,为应用配置应用首页地址。

    关于H5 DEMO
    页面和微信WEB版完全一样,只有接口调用方式不一样。 为了便于解析Token、Ticket、GetUser接口的结果,创建专门的类用于反序列化HTTPResponse。

        public class BaseResponse    {        public int errcode { get; set; }        public string errmsg { get; set; }    }    public class TokenResponse : BaseResponse    {        public string access_token { get; set; }    }    public class TicketResponse : BaseResponse    {        public string ticket { get; set; }        public int expires_in { get; set; }    }    public class GetUserBase : BaseResponse    {//多余的属性用不到        public string userid { get; set; }    }

    新增DDUser类,并创建一个对应的WxUser对象,作为网站用户。出于隐私考虑,Nickname由userid取Hash而来,避免暴露真实ID。

    //DDHelper的GetUserInfo方法        public static DDUser GetUserInfo(string code)        {//先借code取userid,再借userid取详细信息            try            {                string userid = JsonConvert.DeserializeObject(                    ApiGet($"https://oapi.dingtalk.com/user/getuserinfo?access_token={Token}&code={code}")).userid;                string res = ApiGet($"https://oapi.dingtalk.com/user/get?access_token={Token}&userid={userid}");                return JsonConvert.DeserializeObject(res);            }            catch (Exception)            {                return null;            }        }            public class DDUser    {//删掉了一大堆用不到的属性        public string userid { get; set; }        public string errmsg { get; set; }        public string avatar { get; set; }        public string name { get; set; }        public WxUser WxUser => new WxUser()        {            Avatar = string.IsNullOrEmpty(avatar) ? "/ding.png" : avatar,            Created = DateTime.Now,            LastUpdate = DateTime.Now,            Message = 0,            Nickname = "Ding-" + (Convert.ToInt64(userid.GetHashCode()) + int.MaxValue).ToString("x2"),            Openid = userid,            X = 10000,            Y = 0        };    }

    与微信项目类似,为了方便生成统一的ConfigData,创建一个专门的类,自动生成nonceStr和timeStamp,并在构造函数里直接计算签名。

    //DDHelper的GetTicket方法,获取jsapi_ticket        static string _ticket = "";        static DateTime ticket_exp;        public static string GetTicket()        {            if (ticket_exp < DateTime.Now || string.IsNullOrEmpty(_ticket))            {                TicketResponse res =                    JsonConvert.DeserializeObject(                        ApiGet($"https://oapi.dingtalk.com/get_jsapi_ticket?access_token={Token}"));                _ticket = res.ticket;                ticket_exp = DateTime.Now.AddSeconds(res.expires_in);            }            return _ticket;        }            public class DDConfigData    {        public string TimeStamp;        public string NonceStr;        public string Signature;        public string Url;        public DDConfigData(string url = "")        {//参数生成以后,直接计算结果            Url = url;            NonceStr = Guid.NewGuid().ToString("N").Substring(0, 16);            TimeStamp = Convert.ToInt64((DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds).ToString();            var data = $"jsapi_ticket={DDHelper.GetTicket()}&noncestr={NonceStr}&timestamp={TimeStamp}&url={url}";            Console.WriteLine(data);            Signature = General.SHA1(data).ToLower();        }    }

    DDHelper.GetToken(),定时任务,用于access_token有效期刷新,需要手动触发一次(比如放到Startup.cs):

            public static void GetToken()        {            //后台任务无限刷新Token            Task.Run(() =>            {                while (true)                {                    try                    {                        string res = ApiGet($"https://oapi.dingtalk.com/gettoken?appkey={AppKey}&appsecret={AppSecret}");                        Token = JsonConvert.DeserializeObject(res).access_token;                        Thread.Sleep(600000);                    }                    catch (Exception ex)                    {                        Console.WriteLine("GETTOKEN ERROR: " + ex.ToString());                        GetToken();                        break;                    }                }            });        }

    DDHelper剩余部分

            public static string ApiGet(string url)        {            using WebClient client = new WebClient();            try            {                string res = client.DownloadString(url);                Console.WriteLine($"【APIGET: {url} RESULT: {res}】");                return res;            }            catch (Exception) { throw; }        }        public static string ApiPost(string url, string content)        {            using WebClient client = new WebClient();            client.Headers["Content-Type"] = "application/json;charset=utf8";            string res =                 Encoding.UTF8.GetString(client.UploadData(url, Encoding.UTF8.GetBytes(content)));            Console.WriteLine($"【APIPOST: {url} {content} RESULT: {res}】");            return res;        }

    首页做微调,识别不同浏览器并调用不同视图进行渲染

            public async Task Index()        {            WxUser user = General.GetUser(HttpContext);            if (General.Users.Count(u => u.Openid == user?.Openid) == 0 &&                HttpContext.User.Identity.IsAuthenticated)            {                //用户登录状态还在,但用户列表里不存在该用户,直接登出并刷新                //原因是demo环境用户列表没有做持久化+开发环境用户状态未清空,                //正式环境不会出现这种问题。                await HttpContext.SignOutAsync();                return RedirectToAction("Index");            }            ViewBag.User = user;            switch (General.UA(Request.Headers["User-Agent"]))            {                case UserAgents.Dingtalk:                    return View("IndexDingtalk");                case UserAgents.Wechat:                    return View("IndexWx");                default:                    return Content("BROWSER_NOT_SUPPORTED");            }        }

    Action - DDAuth,作为接口使用,前端页面调用后,通过钉钉接口获取用户信息,并在成功后自动登录。

            public async TaskDDAuth(string code = "")        {            DDUser user = DDHelper.GetUserInfo(code);            if (user.userid is null)            {                return Content("登录失败");            }            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);            WxUser wxuser = user.WxUser;            identity.AddClaim(new Claim(ClaimTypes.Sid, wxuser.Openid));            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)).ConfigureAwait(false);            General.Users.Add(wxuser);            return Content("succ");        }

    Action - DDConfig,mime类型为text/javascript,验证用户登录状态,生成dd.config参数并返回dd.config配置js到前端。

            public ContentResult DDConfig(string url)        {            ContentResult js = new ContentResult            {                ContentType = "text/javascript"            };            if (HttpContext.User.Identity.IsAuthenticated)            {                //string url = Request.Headers["Referer"].FirstOrDefault();                DDConfigData config = new DDConfigData(url);                Console.WriteLine(JsonConvert.SerializeObject(config));                js.Content = "dd.config({" +$"    agentId: '{DDHelper.AgentId}'," +$"    corpId: '{DDHelper.CorpId}'," +$"    timeStamp: '{config.TimeStamp}'," +$"    nonceStr: '{config.NonceStr}'," +$"    signature: '{config.Signature}'," +"    type: 0," +"    jsApiList: [ 'device.geolocation.get' ]" +"});";            }            else            {                js.Content = "var result='BAD_REQUEST.'";            }            return js;        }

    前端部分和微信WEB应用几乎一样

        <script>        var words=@Html.Raw(JsonConvert.SerializeObject(General.Words))        $.getScript("/Home/DDConfig?url="+encodeURIComponent(window.location.href));        dd.ready(function () {            if ('@(login?"Y":"N")' == 'N')            {                dd.runtime.permission.requestAuthCode({                    corpId: '@DDHelper.CorpId',                    onSuccess: function (result) {                        $.get("/Home/DDAuth?code=" + result.code, function (e) {                            if (e == "succ") {                                window.location.reload();                            }                         });                    },                    onFail: function (err) {                    }                });}        });        dd.error(function (error) {        });        var userlist =@Html.Raw(JsonConvert.SerializeObject(General.Users));        function getusers() {            $.get("/Home/Nearby", function (e) {                $("#users li").remove();                $.each(e, function (i, val) {                    $("#users").append('            

    ' + '
    ' + ' ' + ' + val.nickname + '">' + ' ' + '
    ' + '
    ' + '
    '
    + val.nickname + ' (' + val.distance + ')' + '

    '
    + words[val.message] + '' + '
    ' + '
    '); }); }); } function upload(msg) { dd.device.geolocation.get({ targetAccuracy: 200, coordinate: 0, withReGeocode: Boolean, useCache: false, onSuccess: function (res) { $.post("/Home/Upload", { X: res.latitude, Y: res.longitude, Message: msg }, function (e) { if (e == "succ") { window.location.reload(); } }); }, onFail: function (err) { dd.device.notification.alert({ message: JSON.stringify(err), title: "UPLOAD ERROR", buttonName: "OK", onSuccess: function () { }, onFail: function (err) { } }); } }); } getusers();script> 最终效果 钉钉版:

    02cb768a13ee109f6837862729ba6280.png
    微信版:
    ————————————————
    版权声明:本文为CSDN博主「宋梦寒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/weixin_42527178/article/details/112205479


    开发环境

    • Chrome Latest Version
    • iOS钉钉最新版、Android钉钉开发最新版
    • 其实我一直想实现电脑Chrome可以调试手机内部APP,苦于尝试多次没有成功,所以还是电脑和手机同时测试,为了开发时间,只能先委屈一下自己了。因为舒适的测试环境也是开发的一个重要先决条件。

    开发目的

    • 企业微应用。
    • 产品需求是在手机端钉钉上开发一个微应用,用来给公司员工填写反馈信息。
    • 产品要求nickname是通过钉钉接口获取用户的昵称,获取之后不允许用户修改。这便涉及到钉钉的接口用需要鉴权的dd.config。
    • 目的具体至实现钉钉企业微应用的鉴权,获取简单的用户信息,顺便提一下免登陆。
      页面以及控制台
    • 看完页面图会发现,这个需求简直算是前端开发里面最简单的需求了吧。实际上也是很简单,如果不是去第一次开发钉钉微应用的话。哈哈哈哈哈哈哈哈哈。。。。。

    开发进度

    • 页面布局、样式按下不表。
    • 页面需要后端人员接进钉钉页面,即进度从打开钉钉微应用能够进来这个页面开始。

    开发思路

    1. 首先,借用公司的agentId和微应用的url(一般由你司企业钉钉管理员提供)通过后台提供的API接口获取到实时的鉴权信息(agentId,corpId,timeStamp,nonceStr,signature);
    2. 然后,用这个鉴权信息区配置钉钉api接口的dd.config,然后去操作钉钉部分需要安全鉴定的api;
    3. 之后,用钉钉api的biz.user.get获取用户信息;
    4. (题外)免登需要dd.runtime.permission.requestAuthCode先获取authCode,然后去请求后台提供的API,由后台返回更加安全的用户信息;
    5. 最后,拿到用户信息之后,将nickname赋值进输入框,然后提交给后台即可。
      官方的思路图

    嘿嘿,借用官方爸爸的微应用开发思路图,真的很一目了然,从五个鉴权信息开始都是前端的操作了哦。

    DEMO

    开发步骤

    1. 第一步,这个地方如果当做一般的数据请求来看,没有什么要说的,用jquery或者zepto的ajax请求都可以很快速实现。就说一下我在这个踩的坑,我在使用ajax异步请求的时候,忽略了异步加载然而同步加载不停止的问题。深入这个话题我也还需要去学去实践,所以只是简单说明我的问题,后面会提供链接去深入学习。下面的代码,由于我没实现手机电脑联调,所以只能退而其次用alert测试,见谅。js在加载的时候,会先全部同步顺序加载,但是ajax请求不会影响同步加载,因而,会按照123的alert弹出,而非顺序弹出312。由于的我浅薄理解,导致我后面还没拿到ajax请求到的_config,就开始执行函数DDConfig(_config)配置钉钉,所以一直不弹出任何弹窗。这个问题我开始解决的方案是将这段ajax单独放在一个script标签里面,最先引入,然后再配置钉钉鉴权信息,这个在Android上测试时正常的,然而只是侥幸,iOS不买这个账。所以使用ajax的complete函数,在这里面执行DDConfig(_config),可看源代码。

      var _config = null; // 定义全局变量_config,初始值为null,用来接收API获取到的签名信息
      var getConfig = $.ajax({
          type: 'POST',
          url: '获取企业签名的API,后台提供',
          data: {
              agentId: 109243825,
              url: '这是你开发微应用页面的线上地址,一般是由钉钉管理员配置的。',
          },
          dataType: 'json',
          success: function(data){
              console.log('---success-post-dingInfo---');
              if(data.status){
                  _config = data.data;
                  alert('3. API获取签名信息:'+JSON.stringify(_config));
                  // 开始配置钉钉
                  DDConfig(_config);
              }else{
                  alert('请求信息出错');
              }
          },
          error: function(data){
              console.log(---error-post-dingInfo---);
          }
      });
      alert('1. API请求开始:'+JSON.stringify(getConfig));
      alert('2. 全局输出_config:'+JSON.stringify(_config));
    2. 第二步,这里官方给出很详细的步骤钉钉移动jsapi开发,你需要使用的api放进dd.config的jsApiList里面即可。其实钉钉的jsapi思路是这样的。引入dingtalk.js(官方文档有提供)这个js会给你提供一个全局变量dd,你可以在Chrome的控制台打印出来看看是个什么东西,里面可以识别钉钉版本,手机系统,以及提供一个个api。钉钉移动jsapi里面介绍所有的api,分为无需鉴权api和需要鉴权api,无需鉴权api可以再引入dingtalk.js之后全局使用;鉴权api就需要走后端接口以保证安全性,且鉴权通过才可以使用这部分api。思路就是这样。

      • 问题1:如果你发现你的dd.ready/dd.error都没执行,那可能是我上一步遇见的问题,即没开始配置dd.config,却执行了dd.ready和dd.error,因为dd是全局变量,不受函数和异步限制,所以写法上面没有错,但是就是什么都不反应,很痛苦。还有一个很粗暴的方法去review你的代码,那就是清空js代码,不做ajax请求,直接开始钉钉鉴权,即dd.config、dd.ready、dd.error,这个时候你可以先用固定的鉴权信息(agentId,corpId,timeStamp,nonceStr,signature)去配置,这个时候因为不是实时的鉴权信息,所以肯定要直接进dd.error来提示校验失败,那么你就应该知道怎么一点点去排查你的错误了。
      • 问题2:如果你发现dd.error被执行了,先恭喜你一下,至少你进入钉钉的api了,哈哈哈哈哈哈哈哈。。。

    3. 第三步,应该不用说了,只要鉴权通过,就可以直接用钉钉api获取用户的信息,只是这个信息很简单,不怎么涉及安全问题。可看源码。
    4. 第四步,题外话。这个免登,是需要你在通过钉钉api拿到authcode之后再去找后端API请求,告诉他你需要免登获取更多用户的信息钉钉免登,这些都是涉及到安全性的了,所有涉及安全的问题都要走后端API。这个我也没做,暂时还没用到,以后若是开发有坑还会继续说。钉钉的生态说不上很稳定,但是由于公司用的多,所以很多东西不管是官方还是前辈们都写的有详细文档和代码,可以多搜搜。
    5. 提交操作,可看源码。

    最最最坑

    • 这次最大的坑是我对异步同步的理解不够到位;
    • 说实话,所有的坑,都还是源于基础。开篇即说“经济基础决定上层建筑”,尤其是技术上,基础决定你未来的路可以走多宽多远多一马平川而不至于坑坑洼洼。

    推荐知识

  • 相关阅读:
    【杂谈】操作系统如何有效地掌控CPU
    【API知识】一种你可能没见过的Controller形式
    【详解】Tomcat是如何监控并删除超时Session的?
    【API知识】RestTemplate的使用
    【杂谈】Tomcat 之 Lifecycle接口
    【杂谈】FilterChain相关知识整理
    【杂谈】Remember-Me的实现
    【杂谈】没有公网IP的电脑如何与外部通信
    【杂谈】tocmat是何时写回响应数据报的
    js的class基础
  • 原文地址:https://www.cnblogs.com/xinxihua/p/15056455.html
Copyright © 2020-2023  润新知