釘釘開發第三方H5微應用入門詳細教程[ISV][免登流程][受權碼][HTTP回調推送][識別用戶身份][獲取用戶信息]

轉載請註明原文地址:http://www.javashuo.com/article/p-vskfeyvn-e.html (by lnexin@aliyun.com 世間草木)javascript

 

此教程注意點:html

  • 適用於第三方企業開發 H5微應用 形式,非企業內部開發, 非釘釘推薦的「小程序」方式;
  • 消息推送模式爲 HTTP回調 ,不使用釘釘收費的「RDS釘釘雲推送數據源「模式;

 

  

 


 

開發前準備:前端

  • 關於服務器,有公網服務器最好,沒有的話須要 內網穿透工具
  • 調試的時候,因爲釘釘的H5微應用調試只能「真機」調試,極其噁心,因此極其建議調試的時候使用 內網穿透工具
  • 關於域名什麼的,有沒有無所謂,隨緣;

 

其餘一些須要明白的:java

  • 須要自備一個釘釘企業(沒有的能夠本身建立一個),測試應用無所謂認證不認證,發佈的時候相關限制請參閱說明文檔;
  • H5微應用前端網頁獲取當前使用的企業的corpId ,須要在 首頁URL地址裏面 使用 $CORPID$ 佔位符 ,而後頁面裏解析 url 參數,可得到 corpId
  • 首頁地址後面能夠更改,建立時無所謂,回調地址須要搭建好咱們本身的服務器,而後填寫的時候須要驗證有效性,可參考 服務端-示例 裏面的  cn.lnexin.dingtalk.controller.SuiteCallbackController::callback(args...)
  • 在咱們自身的服務器回調接口搭建好以前, 不可以填寫回調地址;

  • 在配置好回調地址前, 不能進行企業受權;

  • 在回調裏面激活了當前企業, 纔算受權成功;
  • 在未受權以前, 手機端,PC端 確定實在應用裏面看不到咱們的應用的;

 

另外本教程重在說明釘釘微應用的免登流程,因此前端部分使用原生的, 最簡單的 js, 僅供參考;jquery

 


 

 

 

 

 

 

 

 目錄

  1、建立H5微應用

  2、搭建微應用服務端 (服務點git示例代碼地址: https://gitee.com/lne/ding-server )

  3、確認本身的服務端程序運行成功, 而且填寫回調地址;

  4、實現受權 > 激活流程,將微應用添加到企業客戶端的應用列表中;

  5、編寫簡單的微應用首頁 (html網頁) 進行測試;

  6、從安卓端和PC段訪問,確認登陸流程沒有問題;


一. 建立H5微應用

    建立完成以後:git

    在客戶端和PC端是看不到這個程序的, 若是想看到這個程序, 就須要 受權> 激活的流程; 而受權>激活 是依賴於咱們的服務器的;

    添加有效的回調地址是爲了讓釘釘能夠給咱們發消息;

    而在咱們服務器的回調地址程序裏面作正確業務的處理, 才能完成受權的流程;  只有當受權完成>激活企業應用了以後, 在客戶端 才能看到微應用;     

    沒有有效的回調地址,不在本身服務器裏面處理受權>激活流程, 那麼你在客戶端永遠也看不到這個程序;

     

  第一步:填寫基礎信息

    

 

 

     第二步. 配置開發信息,配置完點擊建立應用便可。

     

 

 

     配置完成以後,信息以下:web

    

 

 

  在開發者後臺添加完大概就這樣了, 其餘信息:如 回調URL(在服務端搭好以後填寫), 首頁地址等, 後續能夠修改.spring

二. 搭建微應用服務端

  服務端程序可參照 (服務端-示例

1. 相關配置參數可參照上面 應用基礎信息 那張圖來一 一對應 .
2. 全部的關鍵信息 是存儲在服務端的, 如咱們的suiteKey/suiteSecret/suiteTicket/aesKey/token;
3. 因此和釘釘相關的數據交互都是在服務端,後臺完成的, 除了獲取 免登受權碼;
4. 咱們的前端和咱們的服務端交互過程當中, corpId 由前端獲取, 傳遞給咱們;
5. 服務端和釘釘交互所使用的accessToken , 能夠每次都去釘釘從新獲取, 可是更建議在有效期內, 後端獲取一次, 而後存儲在前端, 每次的數據交互將token  傳遞給後端;
6. 釘釘向咱們服務器發送請求, 也就是釘釘應用裏面的回調地址;
7. 釘釘的全部消息都是經過回調通知咱們的, 並且消息的結構是一致的;

 

  下面這裏給出一些關鍵代碼: (完整的項目代碼可參照上面的示例地址)json

  1. 釘釘回調請求接收

 
 
package cn.lnexin.dingtalk.controller;

import cn.lnexin.dingtalk.service.IDingAuthService;
import cn.lnexin.dingtalk.service.ISuiteCallbackService;
import cn.lnexin.dingtalk.utils.JsonTool;
import cn.lnexin.dingtalk.utils.Strings;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.LinkedHashMap;
import java.util.Map;
import static cn.lnexin.dingtalk.constant.CallbackConstant.*;
 
/**
* [釘釘] - 釘釘的回調接口, 包含開通,受權,啓用,停用,下單等
*
* @author lnexin@foxmail.com
**/
 
 
public class SuiteCallbackController {
    static Logger logger = LoggerFactory.getLogger(SuiteCallbackController.class);

    /**
     * 釘釘發過來的數據格式:
     * <p>
     * http://您服務端部署的IP:您的端口/callback?signature=111108bb8e6dbce3c9671d6fdb69d15066227608&timestamp=1783610513&nonce=380320111
     * 包含的json數據爲:
     * {
     * "encrypt":"1ojQf0NSvw2WPvW7LijxS8UvISr8pdDP+rXpPbcLGOmIBNbWetRg7IP0vdhVgkVwSoZBJeQwY2zhROsJq/HJ+q6tp1qhl9L1+ccC9ZjKs1wV5bmA9NoAWQiZ+7MpzQVq+j74rJQljdVyBdI/dGOvsnBSCxCVW0ISWX0vn9lYTuuHSoaxwCGylH9xRhYHL9bRDskBc7bO0FseHQQasdfghjkl"
     * }
     */

    @Autowired
    ISuiteCallbackService suiteCallbackService;

    /**
     * 釘釘服務器推送消息 的地址
     *
     * @param signature
     * @param timestamp
     * @param nonce
     * @param encryptNode
     * @return
     */
    @PostMapping(value = "/callback")
    public Map<String, String> tempAuthCodeCallback(@RequestParam String signature,
                                                    @RequestParam String timestamp,
                                                    @RequestParam String nonce,
                                                    @RequestBody JsonNode encryptNode) {
        String encryptMsg = encryptNode.get("encrypt").textValue();
        String plainText = suiteCallbackService.decryptText(signature, timestamp, nonce, encryptMsg);
        JsonNode plainNode = JsonTool.getNode(plainText);

        //進入回調事件分支選擇
        Map<String, String> resultMap = caseProcess(plainNode);
        return resultMap;
    }

    /**
     * 根據回調數據類型作不一樣的業務處理
     *
     * @param plainNode
     * @return
     */
    private Map<String, String> caseProcess(JsonNode plainNode) {
        Map<String, String> resultMap = new LinkedHashMap<>();
        String eventType = plainNode.get("EventType").textValue();
        switch (eventType) {
            case SUITE_TICKET_CALLBACK_URL_VALIDATE:
                logger.info("[callback] 驗證回調地址有效性質:{}", plainNode);
                resultMap = suiteCallbackService.encryptText(CALLBACK_RETURN_SUCCESS);
                break;
            case TEMP_AUTH_CODE_ACTIVE:
                logger.info("[callback] 企業開通受權:{}", plainNode);
                Boolean active = suiteActive(plainNode);
                resultMap = suiteCallbackService.encryptText(active ? CALLBACK_RETURN_SUCCESS : ACTIVE_RETURN_FAILURE);
                break;
            case SUITE_RELIEVE:
                logger.info("[callback] 企業解除受權:{}", plainNode);
          // 處理解除受權邏輯break;
            case CHECK_UPDATE_SUITE_URL:
                logger.info("[callback] 在開發者後臺修改回調地址:" + plainNode);
                resultMap = suiteCallbackService.encryptText(CALLBACK_RETURN_SUCCESS);
                break;
            case CHECK_CREATE_SUITE_URL:
                logger.info("[callback] 檢查釘釘向回調URL POST數據解密後是否成功:" + plainNode);
                resultMap = suiteCallbackService.encryptText(CALLBACK_RETURN_SUCCESS);
                break;
            case CONTACT_CHANGE_AUTH:
                logger.info("[callback] 通信錄受權範圍變動事件:" + plainNode);
                break;
            case ORG_MICRO_APP_STOP:
                logger.info("[callback] 停用應用:" + plainNode);
                break;
            case ORG_MICRO_APP_RESTORE:
                logger.info("[callback] 啓用應用:" + plainNode);
                break;
            case MARKET_BUY:
                logger.info("[callback] 用戶下單購買事件:" + plainNode);
                // 處理其餘企業下單購買咱們應用的具體邏輯
                break;
            default:
                logger.info("[callback] 未知事件: {} , 內容: {}", eventType, plainNode);
                resultMap = suiteCallbackService.encryptText("事件類型未定義, 請聯繫應用提供方!" + eventType);
                break;
        }
        return resultMap;
    }

    /**
     * 激活應用受權
     * tmp_auth_code
     */
    private Boolean suiteActive(JsonNode activeNode) {
        Boolean isActive = false;
        String corpId = activeNode.get("AuthCorpId").textValue();
        String tempAuthCode = activeNode.get("AuthCode").textValue();

        String suiteToken = suiteCallbackService.getSuiteToken();
        String permanentCode = suiteCallbackService.getPermanentCode(suiteToken, tempAuthCode);
        if (!Strings.isNullOrEmpty(permanentCode)) {
            isActive = suiteCallbackService.activateSuite(suiteToken, corpId, permanentCode);
        } else {
            logger.error("獲取永久受權碼出錯");
        }
        return isActive;
    }

 工具實現: 小程序

package cn.lnexin.dingtalk.service.impl;

import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiServiceActivateSuiteRequest;
import com.dingtalk.api.request.OapiServiceGetPermanentCodeRequest;
import com.dingtalk.api.request.OapiServiceGetSuiteTokenRequest;
import com.dingtalk.api.response.OapiServiceActivateSuiteResponse;
import com.dingtalk.api.response.OapiServiceGetPermanentCodeResponse;
import com.dingtalk.api.response.OapiServiceGetSuiteTokenResponse;
import com.taobao.api.ApiException;
import cn.lnexin.dingtalk.constant.DingProperties;
import cn.lnexin.dingtalk.encrypt.DingTalkEncryptException;
import cn.lnexin.dingtalk.encrypt.DingTalkEncryptor;
import cn.lnexin.dingtalk.encrypt.Utils;
import cn.lnexin.dingtalk.service.ISuiteCallbackService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 主要完成釘釘回調相關的一些功能
 * @author lnexin@foxmail.com
 * @Description TODO
 **/
@Service
public class SuiteCallbackServiceImpl implements ISuiteCallbackService {
    Logger logger = LoggerFactory.getLogger(SuiteCallbackServiceImpl.class);

    @Autowired
    DingProperties dingProperties;

    @Override
    public String decryptText(String signature, String timestamp, String nonce, String encryptMsg) {
        String plainText = "";
        try {
            DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(dingProperties.getSuiteToken(), dingProperties.getEncodingAESKey(), dingProperties.getSuiteKey());
            plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, encryptMsg);
        } catch (DingTalkEncryptException e) {
            logger.error("釘釘消息體解密錯誤, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, e: {}", signature, timestamp, nonce, encryptMsg, e);
        }
        logger.debug("釘釘消息體解密, signature: {}, timestamp: {}, nonce: {}, encryptMsg: {}, 解密結果: {}", signature, timestamp, nonce, encryptMsg, plainText);
        return plainText;
    }

    @Override
    public Map<String, String> encryptText(String text) {
        Map<String, String> resultMap = new LinkedHashMap<>();
        try {
            DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(dingProperties.getSuiteToken(), dingProperties.getEncodingAESKey(), dingProperties.getSuiteKey());
            resultMap = dingTalkEncryptor.getEncryptedMap(text, System.currentTimeMillis(), Utils.getRandomStr(8));
        } catch (DingTalkEncryptException e) {
            logger.error("釘釘消息體加密,text: {}, e: {}", text, e);
        }
        logger.debug("釘釘消息體加密,text: {}, resultMap: {}", text, resultMap);
        return resultMap;
    }

    /**
     * {
     * "suite_access_token":"61W3mEpU66027wgNZ_MhGHNQDHnFATkDa9-2llqrMBjUwxRSNPbVsMmyD-yq8wZETSoE5NQgecigDrSHkPtIYA",
     * "expires_in":7200
     * }
     */
    @Override
    public String getSuiteToken() {
        DingTalkClient client = new DefaultDingTalkClient(DingProperties.url_suite_token);
        OapiServiceGetSuiteTokenRequest request = new OapiServiceGetSuiteTokenRequest();
        request.setSuiteKey(dingProperties.getSuiteKey());
        request.setSuiteSecret(dingProperties.getSuiteSecret());
        request.setSuiteTicket(dingProperties.getSuiteTicket());

        String accessToken = "";
        try {
            OapiServiceGetSuiteTokenResponse response = client.execute(request);
            accessToken = response != null ? response.getSuiteAccessToken() : "";
        } catch (ApiException e) {
            logger.error("獲取第三方應用憑證suite_access_token出錯, code: {}, msg: {}", e.getErrCode(), e.getErrMsg());
        }
        logger.debug("獲取第三方應用憑證suite_access_token, accessToken:{}", accessToken);
        return accessToken;
    }

    /**
     * {
     * "permanent_code": "xxxx",
     * "auth_corp_info":
     * {
     * "corpid": "xxxx",
     * "corp_name": "name"
     * }
     * }
     */
    @Override
    public String getPermanentCode(String suiteAccessToken, String tempCode) {
        StringBuilder url = new StringBuilder();
        url.append(DingProperties.url_permanent_code);
        url.append("?suite_access_token=").append(suiteAccessToken);
        DingTalkClient client = new DefaultDingTalkClient(url.toString());
        OapiServiceGetPermanentCodeRequest req = new OapiServiceGetPermanentCodeRequest();
        req.setTmpAuthCode(tempCode);

        String permanentCode = "";
        try {
            OapiServiceGetPermanentCodeResponse rsp = client.execute(req);
            permanentCode = (rsp != null ? rsp.getPermanentCode() : "");
        } catch (ApiException e) {
            logger.error("獲取永久受權碼出錯, tempCode: {}, code: {}, msg: {}", tempCode, e.getErrCode(), e.getErrMsg());
        }
        logger.debug("獲取永久受權碼, tempCode: {}, permanentCode: {}", tempCode, permanentCode);
        return permanentCode;
    }

    /**
     * 激活企業受權的應用
     * {
     * "errcode":0,
     * "errmsg":"ok"
     * }
     */
    @Override
    public Boolean activateSuite(String suiteAccessToken, String corpId, String permanentCode) {
        StringBuilder url = new StringBuilder();
        url.append(DingProperties.url_activate_suite);
        url.append("?suite_access_token=").append(suiteAccessToken);
        DingTalkClient client = new DefaultDingTalkClient(url.toString());

        OapiServiceActivateSuiteRequest req = new OapiServiceActivateSuiteRequest();
        req.setSuiteKey(dingProperties.getSuiteKey());
        req.setAuthCorpid(corpId);
        req.setPermanentCode(permanentCode);
        boolean isActive = false;
        try {
            OapiServiceActivateSuiteResponse rsp = client.execute(req);
            isActive = rsp.getErrmsg().equals("ok");
        } catch (ApiException e) {
            logger.error("激活應用的企業受權出錯, corpId: {}, permanentCode: {}, code: {}, msg: {}", corpId, permanentCode, e.getErrCode(), e.getErrMsg());
        }
        logger.debug("激活應用的企業受權, corpId: {}, permanentCode: {}, isActive: {}", corpId, permanentCode, isActive);
        return isActive;
    }


}
SuiteCallbackServiceImpl.java

 

構建發佈程序, 發佈到本身的服務器上. 若是使用內網穿透工具, 請忽略;

 

 三. 確認本身的服務端程序運行成功, 而且填寫回調地址

  根據上面的相關說明將服務端放置在本身的公網服務器也好,或者使用相關的 內網穿透工具 也好  (自行解決)

  總之, 如今要有一個能夠訪問咱們 服務端項目的 公網地址 

 

  確保你本身的服務器可使用公網地址訪問到,而且成功返回數據;

  同時確保:

  1. 必須有回調地址藉口用來接收釘釘發送的消息;                                    (本文示例地址:  /ding/callback )
  2. 必須有一個接收免登受權碼和企業corpId 來返回用戶信息的接口;      (本文示例地址:  /ding/login )

  好比我本身的測試例子爲: 

// 這裏是我本身的測試地址 http://你的公網地址/ding/config
{
        "suiteId": "6707015",
        "suiteKey": "suiteqflsxxxxxxxx",
        "suiteSecret": "E7TH7H3hGtmhtoGDgq8adJhn0xxxxxxxxxxxBf-GQSTWl8NTs6_",
        "suiteToken": "customtoken",
        "encodingAESKey": "qwp51j1k8eiudktvnip2dwrkqxxxxxcci",
        "suiteTicket": "customTestTicket",
        "url_suite_token": "https://oapi.dingtalk.com/service/get_suite_token",
        "url_permanent_code": "https://oapi.dingtalk.com/service/get_permanent_code",
        "url_activate_suite": "https://oapi.dingtalk.com/service/activate_suite",
        "url_get_auth_info": "https://oapi.dingtalk.com/service/get_auth_info",
        "url_get_access_token": "https://oapi.dingtalk.com/service/get_corp_token",
        "url_get_user_id": "https://oapi.dingtalk.com/user/getuserinfo",
        "url_get_user_item": "https://oapi.dingtalk.com/user/get"
  
}

 

  

 四. 實現受權 > 激活流程,將微應用添加到企業客戶端的應用列表中

  如今,通過以上步驟, 咱們已經準備好的東西有:

  1. 公網能夠訪問的服務端地址, 接收釘釘發給咱們的消息(回調地址)如: http://ding.lnexin.cn/server/ding/callback,咱們本身的登陸地址,如: http://ding.lnexin.cn/server/ding/login
  2. 在釘釘開發者平臺建立配置好的一個H5微應用;
  3. 確保服務端的參數和微應用的基礎信息一致;

          

 

  完成上述步驟,在客戶端依舊是沒有應用入口的,如:

      

 

 

  

  下面須要在開發者平臺進行受權

   

 

  點擊受權以後,會在咱們服務器收到釘釘發給咱們的消息,咱們服務端在通過一系列處理以後,向釘釘發送激活企業的請求,若是激活成功,那麼受權就成功了;

 

  點擊受權後服務器收到的消息:   

  

 

   若是激活成功,以下所示:

  

 

  此時受權激活成功,在客戶端就有了相關微應用入口。如:

   

 

   至此,全部前置準備工做已經完成,下面主要是免登和頁面jsapi 對接。

 

 

五. 編寫簡單的微應用首頁 (html網頁) 進行測試

   通過前面的步驟,咱們如今能夠看到微應用,而且擁有了可訪問的公網服務端接口地址。

  如今須要準備一個前端的公網地址,若是是使用springboot 先後端一體的能夠忽略。( 我這裏是分離的,你們須要根據本身的狀況而定,示例地址如:  http://ding.lnexin.cn/ )

  下面咱們編寫一個最簡單前端html 網頁:

  

   

 

   html 前端示例代碼以下:(git倉庫

<!DOCTYPE html>
<meta charset="UTF-8">
<html>

<head>
    <title>H5微應用開發教學</title>
    <!-- 這個必須引入的啊,釘釘的前端js SDK, 使用框架的請自行參照開發文檔 -->
    <script src="https://g.alicdn.com/dingding/dingtalk-jsapi/2.7.13/dingtalk.open.js"></script>
    <!-- 這個jquery 想不想引入本身決定,沒什麼影響 -->
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head>

<body>
<hr>
<h1>H5微應用免登教學</h1>
<p>當前頁面的url:</p>
<p id="url"></p>
<br>
<p>解析url,獲取的corpID:</p>
<p id="corpId"></p>
<br>
<p>SDK初始化獲取的code:</p>
<p id="code"></p>
<br>
<p>請求咱們服務端,登陸返回的結果:</p>
<p id="result"></p>
</body>
<script type="text/javascript">
    $(function () {
        //釘釘sdk 初始化
        // dd.ready參數爲回調函數,在環境準備就緒時觸發,jsapi的調用須要保證在該回調函數觸發後調用,不然無效。
        dd.ready(function () {
            //獲取當前網頁的url
            //http://ding-web.lnexin.cn/?corpid=ding46a9582af5b7541b35c2fxxxxxxxxxx8f
            var currentUrl = document.location.toString()
            $("#url").append(currentUrl)

            // 解析url中包含的corpId
            var corpId = currentUrl.split("corpid=")[1];
            $("#corpId").append(corpId)

            //使用SDK 獲取免登受權碼
            dd.runtime.permission.requestAuthCode({
                corpId: corpId,
                onSuccess: function (result) {
                    var code = result.code;
                    $("#code").append(code)
                    //請求咱們服務端的登錄地址
                    $.get("http://ding.lnexin.cn/server/ding/login?code=" + code + "&corpId=" + corpId, function (response) {
                        // 咱們服務器返回的信息
                        // 下面代碼主要是將返回結果顯示出來,能夠根據本身的數據結構隨便寫
                        for (item in response) {
                            $("#result").append("<li>" + item + ":" + response[item] + "</li>")
                        }
                        if (response.user) {
                            for (item in response.user) {
                                $("#result").append("<li>\t[user 屬性] " + item + " : " + response.user[item] + "</li>")
                            }
                        }
                    });
                }
            });
        });
    })

</script>

</html>
index.html

 

六. 從安卓端和PC段訪問, 確認流程沒有問題;

  差很少第三方企業開發的免登和受權流程已經完畢了,剩下的就是每一個應用本身的業務邏輯處理了,這個我的本身解決吧。

相關文章
相關標籤/搜索