钉钉开发第三⽅H5微应⽤⼊门详细教程[ISV][免登流程][授权码][HTTP回调推送][。。。转载请注明原⽂地址: (by lnexin@aliyun 世间草⽊)
此教程注意点:
适⽤于第三⽅企业开发H5微应⽤形式,⾮企业内部开发, ⾮钉钉推荐的“⼩程序”⽅式;
消息推送模式为HTTP回调,不使⽤钉钉收费的“RDS钉钉云推送数据源“模式;
开发前准备:
关于服务器,有公⽹服务器最好,没有的话需要内⽹穿透⼯具;
调试的时候,由于钉钉的H5微应⽤调试只能“真机”调试,极其恶⼼,所以极其建议调试的时候使⽤内⽹穿透⼯具;
关于域名什么的,有没有⽆所谓,随缘;
其他⼀些需要明⽩的:
需要⾃备⼀个钉钉企业(没有的可以⾃⼰创建⼀个),测试应⽤⽆所谓认证不认证,发布的时候相关限制请参阅说明⽂档;
H5微应⽤前端⽹页获取当前使⽤的企业的corpId ,需要在⾸页URL地址⾥⾯使⽤$CORPID$占位符 ,然后页⾯⾥解析 url 参数,可获得corpId
⾸页地址后⾯可以更改,创建时⽆所谓,回调地址需要搭建好我们⾃⼰的服务器,然后填写的时候需要验证有效性,可参考服务端-⽰例⾥⾯的::)
在我们⾃⾝的服务器回调接⼝搭建好之前, 不能够填写回调地址;
在配置好回调地址前,不能进⾏企业授权;
在回调⾥⾯激活了当前企业, 才算授权成功;
在未授权之前, ⼿机端,PC端肯定实在应⽤⾥⾯看不到我们的应⽤的;
另外本教程重在说明钉钉微应⽤的免登流程,所以前端部分使⽤原⽣的, 最简单的 js, 仅供参考;
⽬录
  ⼀、创建H5微应⽤
  ⼆、搭建微应⽤服务端 (服务点git⽰例代码地址:  )
  三、确认⾃⼰的服务端程序运⾏成功, 并且填写回调地址;
  四、实现授权 > 激活流程,将微应⽤添加到企业客户端的应⽤列表中;
  五、编写简单的微应⽤⾸页 (html⽹页) 进⾏测试;
  六、从安卓端和PC段访问,确认登录流程没有问题;
⼀. 创建H5微应⽤
   创建完成之后:
的;
依赖于我们的服务器的
   在客户端和PC端是看不到这个程序的, 如果想看到这个程序, 就需要授权> 激活的流程; ⽽授权>激活是依赖于我们的服务器
是为了让钉钉可以给我们发消息;
添加有效的回调地址
回调地址是为了让钉钉可以给我们发消息
   添加有效的
⽽在我们服务器的回调地址程序⾥⾯做正确业务的处理, 才能完成授权的流程;  只有当授权完成>激活企业应⽤了之后, 在客户端才能看到微应⽤;     
   ⽽在我们服务器的
   没有有效的回调地址,不在⾃⼰服务器⾥⾯处理授权>激活流程, 那么你在客户端永远也看不到这个程序;
第⼀步:填写基础信息
第⼆步. 配置开发信息,配置完点击创建应⽤即可。
  配置完成之后,信息如下:
安卓开发教程 pdf  在开发者后台添加完⼤概就这样了, 其他信息:如回调URL(在服务端搭好之后填写), ⾸页地址等, 后续可以修改.
⼆. 搭建微应⽤服务端
  服务端程序可参照 ()
1. 相关配置参数可参照上⾯应⽤基础信息那张图来⼀⼀对应 .
2. 所有的关键信息是存储在服务端的, 如我们的suiteKey/suiteSecret/suiteTicket/aesKey/token;
3. 所以和钉钉相关的数据交互都是在服务端,后台完成的, 除了获取免登授权码;
4. 我们的前端和我们的服务端交互过程中, corpId 由前端获取, 传递给我们;
5. 服务端和钉钉交互所使⽤的accessToken , 可以每次都去钉钉重新获取, 但是更建议在有效期内, 后端获取⼀次, 然后存储在前端, 每次的数据交互将token  传递给后端;
6. 钉钉向我们服务器发送请求, 也就是钉钉应⽤⾥⾯的回调地址;
7. 钉钉的所有消息都是通过回调通知我们的, ⽽且消息的结构是⼀致的;
  下⾯这⾥给出⼀些关键代码: (完整的项⽬代码可参照上⾯的地址)
  1. 钉钉回调请求接收
package cn.ller;
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.stant.CallbackConstant.*;
/**
* [钉钉] - 钉钉的回调接⼝, 包含开通,授权,启⽤,停⽤,下单等
*
* @author lnexin@foxmail
**/
public class SuiteCallbackController {
static Logger logger = Logger(SuiteCallbackController.class);
/**
* 钉钉发过来的数据格式:
* <p>
* 您服务端部署的IP:您的端⼝/callback?signature=111108bb8e6dbce3c9671d6fdb69d150********×tamp=1783610513&nonce=380320111
* 包含的json数据为:
* {
* "encrypt":"1ojQf0NSvw2WPvW7LijxS8UvISr8pdDP+rXpPbcLGOmIBNbWetRg7IP0vdhVgkVwSoZBJeQwY2zhROsJq/HJ+q6tp1qhl9L1+ccC9ZjKs1wV5bmA9NoAWQiZ+7MpzQVq+j74rJQljdVyBdI/dGOvsnBSCxCVW0ISWX0vn9lYTuuHSoaxw      * }
*/
@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 = ("encrypt").textValue();
String plainText = suiteCallbackService.decryptText(signature, timestamp, nonce, encryptMsg);
JsonNode plainNode = Node(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 = ("EventType").textValue();
switch (eventType) {
case SUITE_TICKET_CALLBACK_URL_VALIDATE:
logger.info("[callback] 验证回调地址有效性质:{}", plainNode);
resultMap = ptText(CALLBACK_RETURN_SUCCESS);
break;
case TEMP_AUTH_CODE_ACTIVE:
logger.info("[callback] 企业开通授权:{}", plainNode);
Boolean active = suiteActive(plainNode);
resultMap = ptText(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 = ptText(CALLBACK_RETURN_SUCCESS);
break;
case CHECK_CREATE_SUITE_URL:
logger.info("[callback] 检查钉钉向回调URL POST数据解密后是否成功:" + plainNode);
resultMap = ptText(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 = ptText("事件类型未定义, 请联系应⽤提供⽅!" + eventType);
break;
}
return resultMap;
}
/**
* 激活应⽤授权
* tmp_auth_code
*/
private Boolean suiteActive(JsonNode activeNode) {
Boolean isActive = false;
String corpId = ("AuthCorpId").textValue();
String tempAuthCode = ("AuthCode").textValue();
String suiteToken = SuiteToken();
String permanentCode = PermanentCode(suiteToken, tempAuthCode);
if (!Strings.isNullOrEmpty(permanentCode)) {
isActive = suiteCallbackService.activateSuite(suiteToken, corpId, permanentCode);
} else {
<("获取永久授权码出错");
}
return isActive;
}
⼯具实现:
package cn.lnexin.dingtalk.service.impl;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.quest.OapiServiceActivateSuiteRequest;
import com.quest.OapiServiceGetPermanentCodeRequest;
import com.quest.OapiServiceGetSuiteTokenRequest;
import com.sponse.OapiServiceActivateSuiteResponse;
import com.sponse.OapiServiceGetPermanentCodeResponse;
import com.sponse.OapiServiceGetSuiteTokenResponse;
import com.taobao.api.ApiException;
import cn.stant.DingProperties;
import cn.pt.DingTalkEncryptException;
import cn.pt.DingTalkEncryptor;
import cn.pt.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
* @Description TODO
**/
@Service
public class SuiteCallbackServiceImpl implements ISuiteCallbackService {
Logger logger = Logger(SuiteCallbackServiceImpl.class);
@Autowired
DingProperties dingProperties;
@Override
public String decryptText(String signature, String timestamp, String nonce, String encryptMsg) {
String plainText = "";
try {
DingTalkEncryptor dingTalkEncryptor = new SuiteToken(), EncodingAESKey(), SuiteKey());            plainText = DecryptMsg(signature, timestamp, nonce, encryptMsg);
} catch (DingTalkEncryptException e) {
<("钉钉消息体解密错误, 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 SuiteToken(), EncodingAESKey(), SuiteKey());            resultMap = EncryptedMap(text, System.currentTimeMillis(), RandomStr(8));
} catch (DingTalkEncryptException e) {
<("钉钉消息体加密,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.SuiteKey());
request.SuiteSecret());
request.SuiteTicket());
String accessToken = "";
try {
OapiServiceGetSuiteTokenResponse response = ute(request);
accessToken = response != null ? SuiteAccessToken() : "";
} catch (ApiException e) {
<("获取第三⽅应⽤凭证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 String());
OapiServiceGetPermanentCodeRequest req = new OapiServiceGetPermanentCodeRequest();
req.setTmpAuthCode(tempCode);
String permanentCode = "";
try {
OapiServiceGetPermanentCodeResponse rsp = ute(req);
permanentCode = (rsp != null ? PermanentCode() : "");
} catch (ApiException e) {
<("获取永久授权码出错, tempCode: {}, code: {}, msg: {}", tempCode, e.getErrCode(), e.getErrMsg());
}
logger.debug("获取永久授权码, tempCode: {}, permanentCode: {}", tempCode, permanentCode);
return permanentCode;
}
/**
* 激活企业授权的应⽤
* {
* "errcode":0,