文章轉載自:https://www.cnblogs.com/applerosa/p/11509512.html (by [email protected] 世間草木)
完整代碼地址(出自原作者)
開發前準備:
- 關於服務器,有公網服務器最好,沒有的話需要 內網穿透工具;
- 調試的時候,由於釘釘的H5微應用調試只能“真機”調試,極其噁心,所以極其建議調試的時候使用 內網穿透工具;
- 關於域名什麼的,有沒有無所謂,隨緣;
其他一些需要明白的:
- 需要自備一個釘釘企業(沒有的可以自己創建一個),測試應用無所謂認證不認證,發佈的時候相關限制請參閱說明文檔;
- H5微應用前端網頁獲取當前使用的企業的corpId ,需要在 首頁URL地址裏面 使用 $CORPID$ 佔位符 ,然後頁面裏解析 url 參數,可獲得 corpId
- 首頁地址後面可以更改,創建時無所謂,回調地址需要搭建好我們自己的服務器,然後填寫的時候需要驗證有效性,可參考 服務端-示例 裏面的 cn.lnexin.dingtalk.controller.SuiteCallbackController::callback(args...)
-
在我們自身的服務器回調接口搭建好之前, 不能夠填寫回調地址;
-
在配置好回調地址前, 不能進行企業授權;
- 在回調裏面激活了當前企業, 纔算授權成功;
-
在未授權之前, 手機端,PC端 肯定實在應用裏面看不到我們的應用的;
另外本教程重在說明釘釘微應用的免登流程,所以前端部分使用原生的, 最簡單的 js, 僅供參考;
一. 創建H5微應用
創建完成之後:
在客戶端和PC端是看不到這個程序的, 如果想看到這個程序, 就需要 授權> 激活的流程; 而授權>激活 是依賴於我們的服務器的;
添加有效的回調地址是爲了讓釘釘可以給我們發消息;
而在我們服務器的回調地址程序裏面做正確業務的處理, 才能完成授權的流程; 只有當授權完成>激活企業應用了之後, 在客戶端 才能看到微應用;
沒有有效的回調地址,不在自己服務器裏面處理授權>激活流程, 那麼你在客戶端永遠也看不到這個程序;
第一步:填寫基礎信息
第二步. 配置開發信息,配置完點擊創建應用即可。
配置完成之後,信息如下:
在開發者後臺添加完大概就這樣了, 其他信息:如 回調URL(在服務端搭好之後填寫), 首頁地址等, 後續可以修改.
二. 搭建微應用服務端
服務端程序可參照 (服務端-示例)
1. 相關配置參數可參照上面 應用基礎信息 那張圖來一 一對應 . 2. 所有的關鍵信息 是存儲在服務端的, 如我們的suiteKey/suiteSecret/suiteTicket/aesKey/token; 3. 所以和釘釘相關的數據交互都是在服務端,後臺完成的, 除了獲取 免登授權碼; 4. 我們的前端和我們的服務端交互過程中, corpId 由前端獲取, 傳遞給我們; 5. 服務端和釘釘交互所使用的accessToken , 可以每次都去釘釘重新獲取, 但是更建議在有效期內, 後端獲取一次, 然後存儲在前端, 每次的數據交互將token 傳遞給後端; 6. 釘釘向我們服務器發送請求, 也就是釘釘應用裏面的回調地址; 7. 釘釘的所有消息都是通過回調通知我們的, 而且消息的結構是一致的;
下面這裏給出一些關鍵代碼:
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 [email protected]
**/
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 [email protected]
* @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
構建發佈程序, 發佈到自己的服務器上. 如果使用內網穿透工具, 請忽略;
三. 確認自己的服務端程序運行成功, 並且填寫回調地址
根據上面的相關說明將服務端放置在自己的公網服務器也好,或者使用相關的 內網穿透工具 也好 (自行解決)
總之, 現在要有一個可以訪問我們 服務端項目的 公網地址
確保你自己的服務器可以使用公網地址訪問到,並且成功返回數據;
同時確保:
- 必須有回調地址藉口用來接收釘釘發送的消息; (本文示例地址: /ding/callback )
- 必須有一個接收免登授權碼和企業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"
}
四. 實現授權 > 激活流程,將微應用添加到企業客戶端的應用列表中
現在,經過以上步驟, 我們已經準備好的東西有:
- 公網可以訪問的服務端地址, 接收釘釘發給我們的消息(回調地址)如: http://ding.lnexin.cn/server/ding/callback,我們自己的登錄地址,如: http://ding.lnexin.cn/server/ding/login
- 在釘釘開發者平臺創建配置好的一個H5微應用;
- 確保服務端的參數和微應用的基礎信息一致;
完成上述步驟,在客戶端依舊是沒有應用入口的,如:
下面需要在開發者平臺進行授權
點擊授權之後,會在我們服務器收到釘釘發給我們的消息,我們服務端在經過一系列處理之後,向釘釘發送激活企業的請求,如果激活成功,那麼授權就成功了;
點擊授權後服務器收到的消息:
如果激活成功,如下所示:
此時授權激活成功,在客戶端就有了相關微應用入口。如:
至此,所有前置準備工作已經完成,下面主要是免登和頁面jsapi 對接。
五. 編寫簡單的微應用首頁 (html網頁) 進行測試
經過前面的步驟,我們現在可以看到微應用,並且擁有了可訪問的公網服務端接口地址。
現在需要準備一個前端的公網地址,如果是使用springboot 前後端一體的可以忽略。( 我這裏是分離的,大家需要根據自己的情況而定,示例地址如: http://ding.lnexin.cn/ )
下面我們編寫一個最簡單前端html 網頁:
html 前端示例代碼如下:
<!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>
六. 從安卓端和PC段訪問, 確認流程沒有問題;
差不多第三方企業開發的免登和授權流程已經完畢了,剩下的就是每個應用自己的業務邏輯處理了,這個個人自己解決吧。