釘釘微應用的開發——主前端

經濟基礎決定上層建築。
第一步,這個地方如果當做一般的數據請求來看,沒有什么要說的,用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}×tamp={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> 最終效果 釘釘版:
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。
- 目的具體至實現釘釘企業微應用的鑒權,獲取簡單的用戶信息,順便提一下免登陸。
- 看完頁面圖會發現,這個需求簡直算是前端開發里面最簡單的需求了吧。實際上也是很簡單,如果不是去第一次開發釘釘微應用的話。哈哈哈哈哈哈哈哈哈。。。。。
開發進度
- 頁面布局、樣式按下不表。
- 頁面需要后端人員接進釘釘頁面,即進度從打開釘釘微應用能夠進來這個頁面開始。
開發思路
- 首先,借用公司的agentId和微應用的url(一般由你司企業釘釘管理員提供)通過后台提供的API接口獲取到實時的鑒權信息(agentId,corpId,timeStamp,nonceStr,signature);
- 然后,用這個鑒權信息區配置釘釘api接口的dd.config,然后去操作釘釘部分需要安全鑒定的api;
- 之后,用釘釘api的biz.user.get獲取用戶信息;
- (題外)免登需要dd.runtime.permission.requestAuthCode先獲取authCode,然后去請求后台提供的API,由后台返回更加安全的用戶信息;
- 最后,拿到用戶信息之后,將nickname賦值進輸入框,然后提交給后台即可。
嘿嘿,借用官方爸爸的微應用開發思路圖,真的很一目了然,從五個鑒權信息開始都是前端的操作了哦。
DEMO
開發步驟
-
第一步,這個地方如果當做一般的數據請求來看,沒有什么要說的,用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。這個我也沒做,暫時還沒用到,以后若是開發有坑還會繼續說。釘釘的生態說不上很穩定,但是由於公司用的多,所以很多東西不管是官方還是前輩們都寫的有詳細文檔和代碼,可以多搜搜。
- 提交操作,可看源碼。
最最最坑
- 這次最大的坑是我對異步同步的理解不夠到位;
- 說實話,所有的坑,都還是源於基礎。開篇即說“經濟基礎決定上層建築”,尤其是技術上,基礎決定你未來的路可以走多寬多遠多一馬平川而不至於坑坑窪窪。