创建钉钉H5应用

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

关于免登

免登的关键在于如何识别用户,微信网页也好,微信小程序也好,钉钉也好,都开放了获取用户信息的接口,在这基础上做免登的流程是:向平台获取用户信息 -> 为用户登录。微信网页获取用户信息的流程是:用户同意授权(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:\r\n{url}\r\nRESULT:\t{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:\r\n{url}\r\n{content}\r\nRESULT:\t{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>最终效果钉钉版:

微信版:

如何在钉钉上开发自己的应用_快速上手——钉钉H5微应用开发接入相关推荐

  1. h5应用 vue 钉钉_钉钉企业内部H5微应用开发

    企业内部H5微应用开发 分为 服务端API和前端API的开发,主要涉及到进入应用免登流程和JSAPI鉴权. JSAPI鉴权开发步骤: 1.创建H5微应用 登入钉钉开放平台(https://open-d ...

  2. 钉钉H5微应用开发免登以及接口调试

    钉钉H5微应用开发以及接口调试 企业内部应用免登 https://open.dingtalk.com/document/orgapp-server/enterprise-internal-applic ...

  3. 浙里办H5微应用开发流程

    一.代码开发 1.   源代码准备: 由于源代码需要能够部署到政务中台,即要支持npm run build构建源码.(解释:把源代码上传政务中台后,它会后台自动编译构建项目并取build文件夹下的产物 ...

  4. 浙政钉h5微应用开发vue

    浙政钉h5开发简单总结 1.在页面引入专有钉钉 JSAPI npm install --save gdt-jsapi import dd from 'gdt-jsapi'; /在使用页面导入 Vue. ...

  5. 推荐一款全能测试开发神器!1分钟快速上手!

    关注上方"测试开发技术",选择星标, 干货技术,第一时间送达! 1. 说一下背景 在日常开发或者测试工作中,经常会因为下游服务不可用或者不稳定时,通过工具或者技术手段去模拟一个HT ...

  6. 【游戏开发教程】Unity Cinemachine快速上手,详细案例讲解(虚拟相机系统 | 新发出品 | 良心教程)

    文章目录 一.前言 二.插件下载 三.案例1:第三人称自由视角,Free Look character场景 1.场景演示 2.组件参数 2.1.CinemachineBrain:核心 2.2.Cine ...

  7. 【游戏开发教程】Unity Cinemachine快速上手,详细案例讲解(虚拟相机系统 新发出品 良心教程)

    文章目录 一.前言 二.插件下载 三.案例1:第三人称自由视角,Free Look character场景 1.场景演示 2.组件参数 2.1.CinemachineBrain:核心 2.2.Cine ...

  8. 快速上手Django(一) 项目结构、开发环境、开发流程、视图、视图集

    文章目录 一.Django 基础 1. django 项目的目录结构 2. 开发环境(Pycharm 启动django) 1)先找到mange.py 2)项目配置远程python解释器 3)点击`ed ...

  9. 正点原子stm32f407开发板pcb图_#试用名单公布#正点原子ARM Linux开发板I.MX6ULL

    活动报名链接: http://www.cirmall.com/bbs/thread-161572-1-1.html 恭喜以下五位获得试用资格,管理员会将确认邮件发送至各位获奖者邮箱,请在3个工作日内回 ...

最新文章

  1. 2017年首份中美数据科学对比报告,Python受欢迎度排名第一,美国数据工作者年薪中位数高达11万美金
  2. R中rJava包载入时报错的问题
  3. Python编程基础:第二十七节 format输出Format
  4. FreeMarker基础语法教程
  5. VS与Matlab混合编译 - mexw64 (C++版)
  6. PHP exit函数介绍
  7. 武汉科技大学ACM:1006: 我是老大
  8. plsql破解版,plsql安装包
  9. php对联广告,js 左右悬浮对联广告代码示例
  10. 5+:基于单细胞测序和转录组数据构建胰腺癌中坏死性凋亡相关的预后模型
  11. 文献阅读---普通狗牙根阳江基因组单倍型解析与基因组稳定性和匍匐性研究
  12. oracle 数据占百分比,占总数比例怎么算_占总金额百分比公式
  13. MES系统之设备管理的基础功能
  14. Unity软件界面--Unity基本介绍
  15. 【webrtc0419 点对点视频聊天功能】
  16. java异常-绝对解决! The valid characters are defined in RFC 7230 and RFC 3986
  17. 百度地图 - 绘制驾车路线图
  18. 2021年11月11日文章读后感
  19. 第一次投稿(Elsevier)爱斯维尔期刊经验(持续更新)
  20. 《我的前半生》其实是一部披着职场励志外衣的《霸道总裁爱上我》

热门文章

  1. exc导入mysql phpcms_PHP如何将EXCEL导入MYSQL,急!!!急!!哪位大师能帮帮忙啊,给个详细代码...
  2. mac安装gnu make_Linux Make的使用以及命令安装详解
  3. 第13-14讲 建图
  4. 洛谷P1216 [USACO1.5][IOI1994]数字三角形 Number Triangles
  5. 理解Lucene中的Analyzer
  6. Altium AD20开槽处铺铜出现大面积空白区域的解决方法
  7. fatal: HttpRequestException encountered (附:网盘下载地址)
  8. Java:源文件名、公共类名、main()方法之间关系
  9. 软工实践-第三次会议
  10. 说一说JavaScript 中的原型ProtoType