准备工作:
使用的jar包,和dome下载地址:
https://github.com/opendingtalk/eapp-isv-quick-start-java.git
1.接入说明
由于业务发展,需要接入钉钉平台,特整理了一份简明的接入文档,此文档旨在帮助用户快速熟悉钉钉平台,调用钉钉相关接口,以实现具体的业务逻辑。
详细的官方文档地址:https://open-doc.dingtalk.com/
2.入门
2.1相关术语解释
企业内部应用:企业自建应用,只能用于本企业内部
第三方应用(ISV):第三方企业建立的应用,本企业使用,需要授权等过程。
免登:即单点登录,在企业H5微应用中,通过身份认证接口获取当前钉钉用户的个人信息,然后和数据库中的相关信息比对,正确即可自动登录,无需输入用户名密码。
AccessToken:访问钉钉接口的唯一票据,调用接口需要附带AccessToken才能成功。通过CropID和Corp_secret换取。有效期7200秒,需全局缓存。
CorpID:企业的ID (在开发者后台中得到)
corp_secret:企业的秘钥(在开发者后台中得到)
suite_key:
suite_secret:
suite_ticket:
suite_access_token:
tmp_auth_code:
permanent_auth_code:
jsapi_ticket:是开发者调用钉钉JS接口的临时授权码,其作用主要用于生成签名,有效期7200秒,建议全局缓存。
2.2官方Demo,SDK与调试工具
Java Demo
demo说明https://bbs.aliyun.com/read/291640.html?spm=a219a.7629140.0.0.Jv8hW9
企业内部应用demo地址https://github.com/ddtalk/HarleyCorp?spm=a219a.7629140.0.0.Jv8hW9
isv应用地址
https://github.com/hetaoZhong/ding-isv-common 公共方法类库
https://github.com/hetaoZhong/ding-isv-access 对接工程
调试工具
移动端JSAPI调试工具:http://wsdebug.dingtalk.com 打开后,在页面上配置jsapi的参数,即传入的参数,点击手机会有相应的反馈
服务端企业API调试工具https://debug.dingtalk.com/?spm=a219a.7629140.0.0.l9Pmzq
服务端ISVAPI接入调试工具https://debug.dingtalk.com/isv.html
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环境中,可以通过钉钉的相关接口调用原生控件,例如拍照,录音,地图定位等。接口调用步骤
页面需要引入js文件 http://g.alicdn.com/dingding/open-develop/1.6.9/dingtalk.js 得到一个全局变量dd
需要进行JSAPI权限验证,demo地址https://github.com/injekt/openapi-demo-java/blob/master/WebContent/javascripts/demo.js。
调用钉钉接口。
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 + "×tamp=" + 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建立连接
两种连接方式
主动调用,即后台主动调用钉钉接口,操作通讯录,获取钉钉数据,或者通过微应用来向用户推送消息等。
调用时需要使用https协议、Json数据格式、UTF8编码,访问域名为 https://oapi.dingtalk.com。
每次调用需要带上AccessToken,AccessToken参数由CorpID和CorpSecret换取。
POST请求请在HTTP Header中设置 Content-Type:application/json,否则接口调用失败。
主动调用有一定的频率限制。
回调模式:当用户触发某个事件,钉钉会向我们的后台地址(此地址在注册回调事件时指定)推送此事件的相关信息,例如通讯录人员增加了,会触发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接入流程
下面一个完整的接入流程图
钉钉会定时向我们的服务器回调地址推送ticket,此ticket的作用是结合 suite_key , suite_secret 换取套件访问TOKEN (suit_access_token)。
企业管理员(用户)在安装此应用时,会有授权的界面,点击之后,钉钉会向我们后后推送临时授权码(temp_auth_code)。
.通过suit_access_token和临时授权码(temp_auth_code)换取永久授权码(permanent_code)。
临时授权码通过企业管理员(用户)点击安装授权的时候推送到我们的后台来获取。
通过永久授权码 和 授权方的企业id (auth_copid),来获取企业授权的access_token,通过这个token,可以来操作企业的相关接口。
企业用户授权开通套件时,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小时,期限内重新获取,返回相同结果,并自动续期