转载请注明原文地址:http://www.javashuo.com/article/p-vskfeyvn-e.html (by lnexin@aliyun.com 世间草木)javascript
此教程注意点:html
开发前准备:前端
其余一些须要明白的:java
另外本教程重在说明钉钉微应用的免登流程,因此前端部分使用原生的, 最简单的 js, 仅供参考;jquery
建立完成以后:git
配置完成以后,信息以下:web
在开发者后台添加完大概就这样了, 其余信息:如 回调URL(在服务端搭好以后填写), 首页地址等, 后续能够修改.spring
1. 相关配置参数可参照上面 应用基础信息 那张图来一 一对应 . 2. 全部的关键信息 是存储在服务端的, 如咱们的suiteKey/suiteSecret/suiteTicket/aesKey/token; 3. 因此和钉钉相关的数据交互都是在服务端,后台完成的, 除了获取 免登受权码; 4. 咱们的前端和咱们的服务端交互过程当中, corpId 由前端获取, 传递给咱们; 5. 服务端和钉钉交互所使用的accessToken , 能够每次都去钉钉从新获取, 可是更建议在有效期内, 后端获取一次, 而后存储在前端, 每次的数据交互将token 传递给后端; 6. 钉钉向咱们服务器发送请求, 也就是钉钉应用里面的回调地址; 7. 钉钉的全部消息都是经过回调通知咱们的, 并且消息的结构是一致的;
下面这里给出一些关键代码: (完整的项目代码可参照上面的示例地址)json
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×tamp=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; } }
构建发布程序, 发布到本身的服务器上. 若是使用内网穿透工具, 请忽略;
根据上面的相关说明将服务端放置在本身的公网服务器也好,或者使用相关的 内网穿透工具 也好 (自行解决)
总之, 如今要有一个能够访问咱们 服务端项目的 公网地址
确保你本身的服务器可使用公网地址访问到,而且成功返回数据;
同时确保:
好比我本身的测试例子为:
// 这里是我本身的测试地址 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" }
如今,通过以上步骤, 咱们已经准备好的东西有:
完成上述步骤,在客户端依旧是没有应用入口的,如:
点击受权以后,会在咱们服务器收到钉钉发给咱们的消息,咱们服务端在通过一系列处理以后,向钉钉发送激活企业的请求,若是激活成功,那么受权就成功了;
点击受权后服务器收到的消息:
若是激活成功,以下所示:
此时受权激活成功,在客户端就有了相关微应用入口。如:
至此,全部前置准备工做已经完成,下面主要是免登和页面jsapi 对接。
通过前面的步骤,咱们如今能够看到微应用,而且拥有了可访问的公网服务端接口地址。
如今须要准备一个前端的公网地址,若是是使用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>
差很少第三方企业开发的免登和受权流程已经完毕了,剩下的就是每一个应用本身的业务逻辑处理了,这个我的本身解决吧。