钉钉平台接入文档

钉钉平台接入文档

Auth-date: [add by Easzz 2017-07-18]

1.接入说明

由于业务发展,需要接入钉钉平台,特整理了一份简明的接入文档,此文档旨在帮助用户快速熟悉钉钉平台,调用钉钉相关接口,以实现具体的业务逻辑。

详细的官方文档地址:https://open-doc.dingtalk.com/

2.入门

2.1相关术语解释

  1. 企业内部应用:企业自建应用,只能用于本企业内部
  2. 第三方应用(ISV):第三方企业建立的应用,本企业使用,需要授权等过程。
  3. 免登:即单点登录,在企业H5微应用中,通过身份认证接口获取当前钉钉用户的个人信息,然后和数据库中的相关信息比对,正确即可自动登录,无需输入用户名密码。
  4. AccessToken:访问钉钉接口的唯一票据,调用接口需要附带AccessToken才能成功。通过CropID和Corp_secret换取。有效期7200秒,需全局缓存。
  5. CorpID:企业的ID (在开发者后台中得到)
  6. corp_secret:企业的秘钥(在开发者后台中得到)
  7. suite_key:
  8. suite_secret:
  9. suite_ticket:
  10. suite_access_token:
  11. tmp_auth_code:
  12. permanent_auth_code:
  13. jsapi_ticket:是开发者调用钉钉JS接口的临时授权码,其作用主要用于生成签名,有效期7200秒,建议全局缓存。

2.2官方Demo,SDK与调试工具

Java Demo

调试工具

SDK

java SDK 分为TOP(淘宝开放平台)和OAPI两种,前者是通过restfull方式请求,后者直接使用http请求。

TOP SDK地址https://open-doc.dingtalk.com/doc/sdk.htm

OAPI SDK 地址https://github.com/ddtalk/client_sdk

大部分接口可以直接使用OAPI调用,例如通讯录(人员,部门),获取token,jsapi_ticket等。

少部分接口,例如审批,角色相关等 ,使用TOP方式调用。

具体的使用方式官方接口文档均有详细说明,后续会选取几个作为例子。

3.客户端开发

3.1开始开发

微应用

企业创建微应用后,在钉钉客户端工作台中会出现相应的应用,点进去,即进入到H5开发环境,可以完成一些业务需求。在H5环境中,可以通过钉钉的相关接口调用原生控件,例如拍照,录音,地图定位等。接口调用步骤

  1. 页面需要引入js文件 http://g.alicdn.com/dingding/open-develop/1.6.9/dingtalk.js 得到一个全局变量dd
  2. 需要进行JSAPI权限验证,demo地址https://github.com/injekt/openapi-demo-java/blob/master/WebContent/javascripts/demo.js
  3. 调用钉钉接口。

JSAPI列表https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.mNdKWw&treeId=171&articleId=106834&docType=1

有些API**不需要ddconfig,可以直接在dd.ready里面调用。有些需要**ddconfig,要先鉴权,才能调用成功。

3.2JSAPI鉴权

调用jsapi之前需要进行dd.config(),所需参数如下,参数一般是从后台计算然后返回到前端页面。注意,timeStamp,nonceStr一定和后端计算签名所用随机数和时间戳是一致的。

dd.config({
    agentId: '', // 必填,微应用ID
    corpId: '',//必填,企业ID
    timeStamp: , // 必填,生成签名的时间戳
    nonceStr: '', // 必填,生成签名的随机串
    signature: '', // 必填,签名
    type:0/1,   //选填。0表示微应用的jsapi,1表示服务窗的jsapi。不填默认为0。该参数从dingtalk.js的0.8.3版本开始支持
    jsApiList : [ 'runtime.info', 'biz.contact.choose',
        'device.notification.confirm', 'device.notification.alert',
        'device.notification.prompt', 'biz.ding.post',
        'biz.util.openLink' ] // 必填,需要使用的jsapi列表,注意:不要带dd。
});

其中的signature签名是通过jsapi_ticket(通过jsapi_ticket接口获得),noncestr(随机字符串,自己随便填写即可),timestamp(当前时间戳,具体值为当前时间到1970年1月1号的秒数),url(当前网页的URL,不包含#及其后面部分,需要对url中query部分做一次urldecode)这几个参数计算得到的。具体代码如下

//向前端返回的config数据
public static String getConfig(HttpServletRequest request) {
        String urlString = request.getRequestURL().toString();
        String queryString = request.getQueryString();
        String queryStringEncode;
        String url;
        if (queryString != null) {
            queryStringEncode = URLDecoder.decode(queryString);
            url = urlString + "?" + queryStringEncode;
        } else {
            url = urlString;
        }
        String nonceStr = getRandomStr();
        long timeStamp = System.currentTimeMillis() / 1000;
        String signedUrl = url;
        String accessToken = null;
        String ticket = null;
        String signature = null;
        String agentid = null;
        try {
            accessToken = AuthHelper.getAccessToken();
            ticket = AuthHelper.getJsapiTicket(accessToken);
            signature = AuthHelper.sign(ticket, nonceStr, timeStamp, signedUrl);
            agentid = "111853665";//微应用ID
        } catch (OApiException e) {
            e.printStackTrace();
        }
        String configValue = "{jsticket:'" + ticket + "',signature:'" + signature + "',nonceStr:'" + nonceStr + "',timeStamp:'"+ timeStamp + "',corpId:'" + Env.CORP_ID + "',agentid:'" + agentid + "'}";
        System.out.println(configValue);
        return configValue;
    }

 /**
     * 签名算法
     * @param ticket 
     * @param nonceStr 随机数
     * @param timeStamp 时间戳
     * @param url  前端页面url
     * @return
     * @throws OApiException
     */
    public static String sign(String ticket, String nonceStr, long timeStamp, String url) throws OApiException {
        String plain = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" +      String.valueOf(timeStamp) + "&url=" + url;
        try {
            MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
            sha1.reset();
            sha1.update(plain.getBytes("UTF-8"));
            return bytesToHex(sha1.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new OApiResultException(e.getMessage());
        } catch (UnsupportedEncodingException e) {
            throw new OApiResultException(e.getMessage());
        }
    }

在dd.config成功后触发dd.ready(function(){}); jsapi需要在此回调函数触发后使用

dd.ready(function(){
   //拍照接口
    dd.biz.util.uploadImageFromCamera({
            onSuccess: function (info) {
                alert(JSON.stringify(info));
            },
            onFail: function (err) {
                alert("camera fail:"+JSON.stringify(err));
            }
        })
});

dd.config失败后触发dd.error(function(error){}); 错误信息可以在返回的error中查看

dd.error(function(error){
       /**
        {
           message:"错误信息",//message信息会展示出钉钉服务端生成签名使用的参数,请和您生成签名的参数作对比,找出错误的参数
           errorCode:"错误码"
        }
       **/
       alert('dd error: ' + JSON.stringify(err));
});

3.3免登

免登流程图

在dd.config之后,可以调用免登接口获取code,再通过code获取用户身份,具体代码如下

dd.ready(function(){
  dd.runtime.permission.requestAuthCode({
    corpId: "corpid", //传入企业ID
    onSuccess: function(result) {
    /*{
        code: 'hYLK98jkf0m' //string authCode
    }*/
      alert(result.code);
    },
    onFail : function(err) {}
  })
});

拿到code后,通过/user/getuserinfo接口 获取到用户的userid,再通过/user/get 接口获取到用户的详细信息。和数据库中的用户信息比对,一致则允许登录。

4.服务端开发(企业内部)

4.1建立连接

两种连接方式

  1. 主动调用,即后台主动调用钉钉接口,操作通讯录,获取钉钉数据,或者通过微应用来向用户推送消息等。
    • 调用时需要使用https协议、Json数据格式、UTF8编码,访问域名为 https://oapi.dingtalk.com。
    • 每次调用需要带上AccessToken,AccessToken参数由CorpID和CorpSecret换取。
    • POST请求请在HTTP Header中设置 Content-Type:application/json,否则接口调用失败。
    • 主动调用有一定的频率限制。
  2. 回调模式:当用户触发某个事件,钉钉会向我们的后台地址(此地址在注册回调事件时指定)推送此事件的相关信息,例如通讯录人员增加了,会触发user_add_org事件,我们后台可以监听接受到此事件的信息,可以获得增加人员的userid,来操作一些业务,同步数据库等。回调事件需要提前注册,后续会详细说明。

4.2SDK调用说明

官方提供的SDK分为两种,TOP和OAPI

其中TOP需要导入taobao-sdk-java.jar这个jar包。

OAPI需要导入

  • client-sdk.core-{version}.jar:SDK核心类库,负责网络通信及API扫描
  • client-sdk.common-{version}.jar:SDK内共用代码如注解的定义
  • client-sdk.api-{version}.jar:SDK会扫描此包内的API,如需自定义新的API可按照规范在此包内增加
  • client-sdk.spring-{version}.jar:如果需要在Spring环境中使用SDK才需要依赖此jar

这四个jar包。

TOP方式调用实例(审批)

接口文档https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.ppBVWX&treeId=355&articleId=106846&docType=1

首先需要在后台OA模板中新增审批模板,只支持三种表单控件,单行文本框,图片和明细,其中明细中可以包含多个文本框和图片

另外还需要获取processCode,在模板的url参数中获取processCode

DingTalkClient client = new DefaultDingTalkClient('https://eco.taobao.com/router/rest');
SmartworkBpmsProcessinstanceCreateRequest req = new SmartworkBpmsProcessinstanceCreateRequest();
req.setAgentId(41605932L);//微应用ID
req.setProcessCode("PROC-BH3K80XU-LC9M6901U1R5IER41D1S1-V8V6VN4J-X");
req.setOriginatorUserId("1124321606898017");//审批发起人
req.setDeptId(13688215L);//部门ID
req.setApprovers("1124321606898017");;//审批人
req.setCcList("zhangsan,lisi");//抄送人
List<FormComponentValueVo> list2 = new ArrayList<FormComponentValueVo>();
FormComponentValueVo obj3 = new FormComponentValueVo();
list2.add(obj3);
obj3.setName("文本框"); //name 需要和设计表单时的标题名一致
obj3.setValue("测试文本数据审批");
req.setFormComponentValues(list2);
SmartworkBpmsProcessinstanceCreateResponse rsp = client.execute(req, access_token);
System.out.println(rsp.getBody());

OAPI调用实例

平台大部分接口可以直接通过OAPI接口来调用,其底层也是封装的http请求,更方便我们的调用。

在client-sdk.api.jar中根据分类来封装了不同的接口请求,以及对应的Model,我们可以直接使用。

    @Test
    public void getUserInfo(String userId) {
        try {
          //通过userId获取用户的详细信息
            CorpUserService corpUserService = ServiceFactory.getInstance().getOpenService(CorpUserService.class);
            CorpUserDetail corpUser = corpUserService.getCorpUser(access_token, userId);
            System.out.println(corpUser.getName());
        } catch (ServiceNotExistException | SdkInitException | ServiceException e) {
            e.printStackTrace();
        }
    }

    @Test
    public String getAccessToken(String corpID,String,corpSecret) {
        try {
          //获取AccessToken
         ServiceFactory serviceFactory = ServiceFactory.getInstance();
         CorpConnectionService corpConnectionService =serviceFactory.getOpenService(CorpConnectionService.class);
         return corpConnectionService.getCorpToken(corpID, corpSecret);
        } catch (ServiceNotExistException | SdkInitException | ServiceException e) {
            e.printStackTrace();
        }
    }

4.3回调事件注册

文档地址https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.vLNbs0&treeId=385&articleId=104975&docType=1#s1

用户的一些操作,会触发回调事件,(例如通讯录成员变更,群回话变更,以及审批单据的变化)。触发后,钉钉会向我们的回调地址推送消息,通过消息来执行特定的业务逻辑。

要使用回调,必须先要注册回调事件

钉钉服务器向回调url推送事件的时候,会携带signature,timestamp,nonce,以及加密的encrypt字段,我们需要进行解密,来获取我们想要的数据。在接收到推送之后,需要返回加密后的“success”字符串,代表已经接收到了推送。如果不返回,钉钉会持续推送,达到一定阈值将不再推送。所以在回调事件中,如果有比较复杂的业务逻辑,建议使用异步来执行。让”success”字符串成功返回,避免重复推送。

下面是具体的代码

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        /**url中的签名**/
        String msgSignature = request.getParameter("signature");
        /**url中的时间戳**/
        String timeStamp = request.getParameter("timestamp");
        /**url中的随机字符串**/
        String nonce = request.getParameter("nonce");

        /**post数据包数据中的加密数据**/
        ServletInputStream sis = request.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(sis));
        String line = null;
        StringBuilder sb = new StringBuilder();
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        JSONObject jsonEncrypt = JSONObject.parseObject(sb.toString());
        String encrypt = jsonEncrypt.getString("encrypt");

        /**对encrypt进行解密**/
        DingTalkEncryptor dingTalkEncryptor = null;
        String plainText = null;
        try {
            dingTalkEncryptor = new DingTalkEncryptor(Env.TOKEN, Env.ENCODING_AES_KEY, Env.CORP_ID);
            plainText = dingTalkEncryptor.getDecryptMsg(msgSignature, timeStamp, nonce, encrypt);
        } catch (DingTalkEncryptException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }

        /**对从encrypt解密出来的明文进行处理**/
        JSONObject plainTextJson = JSONObject.parseObject(plainText);
        //异步业务操作,防止响应时间过长,钉钉重复推送回调。
        CallBackThread callBackThread=new CallBackThread(plainTextJson);
        callBackThread.start();
       /* String eventType = plainTextJson.getString("EventType");
        switch (eventType) {
            case "user_add_org"://通讯录用户增加 do something
                JSONArray userId = plainTextJson.getJSONArray("UserId");
                for(int i=0;i<userId.size();i++){
                    UserTest.getUserInfo(userId.get(i).toString());
                }
                break;
            case "user_modify_org"://通讯录用户更改 do something
                break;
            case "user_leave_org"://通讯录用户离职  do something
                JSONArray leave_id = plainTextJson.getJSONArray("UserId");
                break;
            case "org_admin_add"://通讯录用户被设为管理员 do something
                break;
            case "org_admin_remove"://通讯录用户被取消设置管理员 do something
                break;
            case "org_dept_create"://通讯录企业部门创建 do something
                break;
            case "org_dept_modify"://通讯录企业部门修改 do something
                break;
            case "org_dept_remove"://通讯录企业部门删除 do something
                break;
            case "org_remove"://企业被解散 do something
                break;

            case "check_url"://do something
                break;
            case "bpms_task_change":

                System.out.println(plainTextJson.toString());
                CallBackThread cth=new CallBackThread(plainTextJson);
                cth.start();
                break;
            case "bpms_instance_change":
                System.out.println(plainTextJson.toString());
                break;
            default: //do something
                break;
        }*/

        /**对返回信息进行加密**/
        long timeStampLong = Long.parseLong(timeStamp);
        Map<String, String> jsonMap = null;
        try {
            jsonMap = dingTalkEncryptor.getEncryptedMap("success", timeStampLong, nonce);
        } catch (DingTalkEncryptException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
        JSONObject json = new JSONObject();
        json.putAll(jsonMap);
        response.getWriter().append(json.toString());
    }

注册回调事件

调用/call_back/register_call_back/ 接口来注册回调事件

可以注册的回调事件一共有20种 https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.vLNbs0&treeId=385&articleId=104975&docType=1#s2

 public static void main(String[] args) {
        String accessToken = "";
        try {
            accessToken = AuthHelper.getAccessToken();
        } catch (OApiException e) {
            e.printStackTrace();
        }
   //注册四个回调事件,审批变化,通讯录用户增加,修改,删除
        String t1 = "bpms_task_change";
        String t2="user_add_org";
        String t3="user_modify_org";
        String t4="user_leave_org";
        List<String> callBackTag = new ArrayList<>();
        callBackTag.add(t1);
        callBackTag.add(t2);
        callBackTag.add(t3);
        callBackTag.add(t4);
        String callBackUrl = "http://easzz.tunnel.echomod.cn/selfApp/eventreceive";//后台接受的回调地址,即上方代码
        try {
            String s = EventChangeHelper.registerEventChange(accessToken, callBackTag, Env.TOKEN, Env.ENCODING_AES_KEY, callBackUrl);
            System.out.println(s);
        } catch (OApiException e) {
            e.printStackTrace();
        }
    }

//注册事件回调接口
    public static String registerEventChange(String accessToken, List<String> callBackTag, String token, String aesKey, String url) throws OApiException{
        String signUpUrl = Env.OAPI_HOST + "/call_back/register_call_back?" +
                "access_token=" + accessToken;
        JSONObject args = new JSONObject();
        args.put("call_back_tag", callBackTag);
        args.put("token", token);
        args.put("aes_key", aesKey);
        args.put("url", url);

        JSONObject response = HttpHelper.httpPost(signUpUrl, args);
        if (response.containsKey("errcode")) {
            return response.getString("errcode");
        }
        else {
            return null;
        }
    }

若需要添加其他的回调事件,需要调用更新回调事件接口。

4.4群机器人

可以做为监控报警等。

建立一个自定义机器人,添加成员,获取机器人对应的Webhook地址

下面的例子为云销售无法访问时,向群推送一条消息。

 private String url = "https://oapi.dingtalk.com/robot/send?access_token=2dfdb25ef9874a322c7476f5afa38f7d497e4bc79565915fae54605e47b3e0e0";

    @Test
    public void connTest() {
        if (!isLive()) {
            TextMessage textMessage = new TextMessage();
            textMessage.setText("http://www.cloudsales.com.cn 无法连接...");
            List<String> list = new ArrayList<>();
            list.add("13687240031");
            textMessage.setAtMobile(list);
            try {
                HttpHelper.httpPost(url, JSONObject.parse(textMessage.toString()));
            } catch (OApiException e) {
                e.printStackTrace();
            }
        }
    }

    private Boolean isLive() {
        String ur = "http://www.cloudsales.com.cn";
        HttpGet httpGet = new HttpGet(ur);
        CloseableHttpResponse closeableHttpResponse = null;
        CloseableHttpClient httpClient = HttpClients.createDefault();
        RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(5000).setSocketTimeout(5000).build();
        httpGet.setConfig(config);
        try {
            closeableHttpResponse = httpClient.execute(httpGet, new BasicHttpContext());
            return closeableHttpResponse.getStatusLine().getStatusCode() == 200;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

5.服务端开发(ISV应用)

5.1第三方应用说明(ISV)

ISV为专门开发软件的提供商,经过审核后可以上架到钉钉应用市场,企业的管理员可以在市场搜索来添加到本企业的工作面板中,添加的时候,会出现一个授权的界面,上面显示此应用需要的一些权限信息,确定之后,普通员工就可以在钉钉客户端看到此应用。ISV应用相对于企业内部应用开发来说,开发要复杂一些,主要是处理回调授权的过程。

5.2接入前准备

开发ISV应用,需要企业先进行认证,认证通过之后才可以开发。

认证后,注册后台开发者,注册完之后,就可以新建一个套件(suite)

其中Token和数据加密秘钥需要保存在后台,后续处理回调请求的时候,需要用此秘钥来进行解密。

注册时,需验证回调URL有效性,钉钉会发出一个回调事件,,返回加密后的“Random”,验证通过才算注册完成。

此地址作用于三个场景,1.创建套件 2.授权,激活。3.用户购买套件。

一共有11个回调类型。

钉钉服务器会定时向此地址推送Ticket ,我们后台需要保存起来,后续会结合suite_key,suite_secret获取套件访问token(suite_access_token)

注册完成之后,会生成套件Key(suite_key),套件secret(suite_secret)

随后可以创建微应用,微应用的主页地址需要使用$CORPID$模板参数表示corpid,

例如http://www.cloudsales.com.cn/isv/index.html?corpid=$CORPID$

其中的$CORPID$,会自动替换为当前企业的corpid,用于获取企业的access_token

5.3接入流程

下面一个完整的接入流程图

  1. 钉钉会定时向我们的服务器回调地址推送ticket,此ticket的作用是结合 suite_key , suite_secret 换取套件访问TOKEN (suit_access_token)

  2. 企业管理员(用户)在安装此应用时,会有授权的界面,点击之后,钉钉会向我们后后推送临时授权码(temp_auth_code)

  3. .通过suit_access_token和临时授权码(temp_auth_code)换取永久授权码(permanent_code)

  4. 临时授权码通过企业管理员(用户)点击安装授权的时候推送到我们的后台来获取。

  5. 通过永久授权码 和 授权方的企业id (auth_copid),来获取企业授权的access_token,通过这个token,可以来操作企业的相关接口。

  6. 企业用户授权开通套件时,ISV需要激活此套件(调用激活接口),否则企业无法使用。

5.4部署安装

地址https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.lXYJrx&treeId=366&articleId=105374&docType=1

6.常见问题

官方FAQ地址https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.YetWfq&treeId=173&articleId=105077&docType=1

企业应用中,access_token过期时间为2小时,期限内获取返回相同的数据,并自动续期。

​ Jsticket过期时间为2小时,期限内获取,是全新的jsticket,过期时间为2h

ISV应用中,jsticket过期时间为2小时,期限内重新获取,返回相同结果,并自动续期

  • 8
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值