钉钉开发第三方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段访问, 确认流程没有问题;

  差很少第三方企业开发的免登和受权流程已经完毕了,剩下的就是每一个应用本身的业务逻辑处理了,这个我的本身解决吧。

相关文章
相关标签/搜索