前言
之前一直因為騰訊的文檔可讀性差而吐槽,而這次對接釘釘開放平台時也遇到了很多問題。
一句話概括原因:當前(2025年)正值釘釘兩代API切換的過程中,新舊API同時存在,造成釘釘官方文檔內容分散,來不及更新,且第三方博客新舊共存。初次接觸時無從下手,API調用時因為版本不對可能導致問題。
本文基於最新的API及文檔,儘可能全面的描述釘釘SSO流程。
SSO
SSO(Single Sign-On,單點登錄)是一種身份驗證機制。通俗的説,假設要新開發一個項目B,希望充分利用已有歷史項目A中的用户信息,最終實現在系統A中登錄後,用户在系統A中的鑑權可以帶到系統B中,無需再次登錄。
例如常見的微信和釘釘都提供了SSO,用户在綁定微信和第三方平台後,就可以從微信一鍵登錄第三方平台。
對於一般情況下的SSO時序圖也是老生產談了。
釘釘企業自建應用的三種SSO方式
具體到釘釘上,釘釘提供了三種SSO方式:
- 釘釘內點擊程序圖標,①直接請求釘釘平台,②帶CODE回調到後端,③後端完成校驗+登錄(官方稱為:使用釘釘提供的頁面登錄授權)
- 釘釘內點擊程序圖標,①加載前端,②通過JSAPI請求CODE發送給後端,③後端完成校驗+登錄(這個可以在H5免登的文檔中獲取DEMO)
- 釘釘內點擊程序圖標,①加載前端,②用户掃碼獲取CODE發送給後端,③後端完成校驗+登錄(官方稱為:內嵌二維碼方式登錄授權)
第一種方式最省事,後端和釘釘交互,前端不用動。但測試了一下沒有成功,文檔鏈接 https://open.dingtalk.com/document/orgapp/obtain-identity-cre... ,文檔是基於v1版本,應該是不適配v2了。
第一種方式的時序圖,基於官方的進行修改,增加詳細步驟(已經不能用了,留個紀念):
本文主要場景是第二種,需要用到前端的jsapi,實際上只要把坑都注意到,原理都差不多。
時序圖:
核心就兩步(也是最容易出問題的步驟):
- 訪問
https://oapi.dingtalk.com/gettoken,攜帶appKey、appSecret,獲取access_token - 訪問
https://oapi.dingtalk.com/topapi/v2/user/getuserinfo,攜帶access_token、code,獲取用户信息
理論搞明白了,接下來介紹如何DEBUG
如何找文檔、調試
從本節起,開始進入避坑的部分。
一,文檔
第一眼看到文檔可能會無所適從,如果照着圖中的箭頭點進來,可能會發現同一個話題中,引用文檔和當前話題中説的不一致,前者讓下載JSSDK balabala,後者只説調用接口,也不知道該聽誰的。
二、接口
再看接口調試程序,光是獲取AccessToken就有一堆
然後獲取用户信息的也有四個
其中的接口有新的有舊的,上文提到的文檔中也混雜着兩種寫法。
最大的麻煩是:如果調試接口時報錯,根本分不清是調試過程有問題,還是接口找錯了。
進一步地,由於反覆嘗試出錯,官方又沒有一篇文章能拍胸脯説“看我,我就是最新的!”,所以會原地打轉消耗很多無用時間。
怎麼辦?
清楚自己現在做的是什麼
通常,下圖中的應用被官方稱為“企業內部應用”,所以查文檔時要注意這個關鍵詞,而不是別的關鍵詞。
通過API路徑區分版本
- 舊的API路徑前綴為
/v1.0 - 新的API路徑包含
v2
這是非常關鍵的信息,通常v1和v1搭配使用,v2和v2搭配使用,如果搞錯,就回出現驢 + 馬 = 騾子,而騾子是無法通過驗證的,表現出來就是你感覺哪寫的都對,但就是報錯提示token無效。
此外還有一個技巧:把鼠標放在參數上,可以看到參數的來源,這個信息通常不會錯,就容易看到API的依賴關係了。
不要過於相信文檔
即使有些文檔發佈與2025年,文中的API版本也可能是v1。
當前的開發者平台難以滿足v1所需的參數(其中crop_srcret屬於非常隱私的值,並且具有整個團隊所有項目的權限,風險較大,v2版本已無需用到,且不再輕易提供),本文建議全部使用V2的接口。
只能説,如果一口氣難以更新全部文檔,至少要先讓新手入門的文檔可用吧?難繃。
前置操作
我們需要用到什麼?
由於新舊版本不統一,出現了一堆參數,可能會讓開發者繞的雲裏霧裏。我們來總結一下。
cropId、cropSrcret屬於團隊的屬性,直接和團隊關聯。cropSrcret當前已不再輕易提供,而cropId目前仍需使用。
ClientId(v2) = AppKey(v1) = SuiteKey(v1),看到這三個名字認為是同一個東西就行。
同理Client Secret(v2) = AppSecret(v1) = SuiteSecret(v1)。這兩個值都需要用到。
此外,code和access_token是實時生成的。
access_token是供自建項目後端請求釘釘開放平台的憑據,有效期2小時。
code是用户回調時的一次性憑據,用於判斷當前用户是誰,有效期5分鐘,只能用一次。
總結一下:
- 我們需要記下cropId、ClientId、Client Secret
- 後端會定時獲取access_token
- 每次登錄會實時生成一次性code
- 其他信息都不再需要了
基本設置:
網頁應用——把首頁地址設置為前端的SSO登錄組件對應的地址:
安全設置——服務器出口IP是開發者公網IP,把回調域名設置為後端SSO登錄的方法
然後發佈應用,在釘釘中能看到即可。
前端編碼
前端項目添加依賴:
npm install --save dingtalk-jsapi@3.1.0
在SSO對應的組件上ts層添加:
import * as dd from 'dingtalk-jsapi';
ngOnInit() {
const corpId = this.route.snapshot.queryParamMap.get('corpId'); // 接收參數
const clientId = this.route.snapshot.queryParamMap.get('clientId');
if (!!corpId && !!clientId) {
dd.requestAuthCode({
corpId,
clientId,
onSuccess: async (result: { code: string }) => {
try {
const response = await fetch(`/api/sso/loginByCode?code=${result.code}`); // 開發者後端的SSO方法
const data = await response.json();
console.log(data);
this.errorInfo.set('');
this.router.navigate(['/']).then(); // 調試時可以去掉所有路由跳轉,重點觀察返回值
} catch (error) {
console.error('獲取用户信息失敗:', error);
} finally {
}
},
onFail: (err: any) => {
console.error('獲取授權碼失敗:', err);
}
}).then(r => {});
}
}
示例使用Angular,如使用VUE調整一下接收參數的代碼即可。
請求時訪問此組件,傳入corpId和clientId兩個參數,如http://localhost:8018/login?corpId=ding594xxxx&clientId=dingpn3xxxx
如果釘釘內調試時不符合預期,可以參考官方的四端調試工具 https://open-dev.dingtalk.com/fe/api-tools,點擊調試就能看到控制枱和網絡了。
後端編碼
在後端實現之前,為了調試前端,至少controller層要有個接收參數的方法:
@GetMapping("loginByCode")
public void loginByCode(@RequestParam String code) {
// 此處打斷點,就可以拿到code,直接輸入到官方的API調試頁面進行調試了
}
maven依賴:
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.2.34</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
後端——增加配置
app:
dingDing:
ClientId:
ClientSecret:
cropId:
給出Service的實現:
@Value("${app.dingDing.cropId}")
private String corpId;
@Value("${app.dingDing.ClientId}")
private String clientId;
@Value("${app.dingDing.ClientSecret}")
private String clientSecret;
// 緩存的 token 值
private String cachedToken;
// 過期時間戳(毫秒)
private long expireAt = 0;
/**
* getAccessToken
*/
public String getAccessToken() {
// 判斷緩存是否還有效(預留200秒作為刷新緩衝)
if (cachedToken != null && (expireAt - 200_000) > System.currentTimeMillis()) {
this.logger.info("accessToken: {}", cachedToken);
return cachedToken;
}
try {
// 獲取client、構造請求
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
OapiGettokenRequest req = new OapiGettokenRequest();
req.setAppkey(clientId);
req.setAppsecret(clientSecret);
req.setHttpMethod("GET");
// 發送請求獲取access_token
String accessToken = client.execute(req).getAccessToken();
this.logger.info("accessToken: {}", accessToken);
// 緩存token,設置有效期7200秒
this.cachedToken = accessToken;
this.expireAt = System.currentTimeMillis() + 7200_000;
} catch (ApiException err) {
this.logger.error("獲取accessToken失敗:{}", err.getErrMsg());
err.printStackTrace();
}
return cachedToken;
}
/**
* getUserInfo
* @param code
*/
public OapiV2UserGetuserinfoResponse.UserGetByCodeResponse getUserInfo(String code) {
try {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getuserinfo");
OapiV2UserGetuserinfoRequest req = new OapiV2UserGetuserinfoRequest();
req.setCode(code);
OapiV2UserGetuserinfoResponse rsp = client.execute(req, getAccessToken());
if (!rsp.isSuccess()) {
this.logger.error("Failed to get user info: {}", rsp.getErrmsg());
return null;
}
this.logger.info("Successfully got user info: {}", rsp.getBody());
return rsp.getResult();
} catch (ApiException err) {
this.logger.error("獲取UserInfo失敗:{}", err.getErrMsg());
err.printStackTrace();
}
return null;
}
授人以漁——代碼怎麼來的?基本不需要看文檔,而是去看接口。
首先確認使用下圖兩個接口:
接下來只需要選擇這兩個接口,填入信息,嘗試發起請求,成功後搬走代碼自行修改即可。
如何DEBUG?——如果出現不符合預期的情況,需要在JAVA後端打印獲取的code和access_token,和官方調試頁面比較一下看看是否一致,如果不一致説明接口找錯了或信息填錯了,需要糾正。
System.out.println(corpId);
System.out.println(clientId);
System.out.println(clientSecret);
System.out.println(cachedToken);
成功後預期的信息:
2025-10-02T10:54:31.325+08:00 INFO 36608 --- [nio-8080-exec-3] c.y.xxxx.service.DingTalkServiceImpl : Successfully got user info: {"errcode":0,"errmsg":"ok","result":{"device_id":"a7e9f627xxxxx","name":"xxxxx","sys":true,"sys_level":2,"unionid":"dX0N6gjKxxxxxxxx","userid":"2807400000000"},"request_id":"15rp1xxxxxx"}
只要能打印出用户信息就大功告成,剩下的就是根據實際情況接入登錄功能了。
例如:
@GetMapping("loginByCode")
public void loginByCode(@RequestParam String code, HttpServletRequest request, HttpServletResponse response) {
// 獲取用户信息
try {
OapiV2UserGetuserinfoResponse.UserGetByCodeResponse user = this.dingTalkService.getUserInfo(code);
this.logger.info(user.toString());
// 登錄,向前端返回
} catch (Exception e) {
this.logger.error("loginByCode失敗:{}", e.toString());
}
}
後記
其實整個邏輯非常簡單,問題就出在API版本正在切換、新舊文檔混亂、新版本的博客少,這給調試帶來了很大的麻煩。
至今仍記得被諸如{ "errcode":40078, "errmsg":"不存在的臨時授權碼", "request_id":"15rzcr35687zq" }之類的報錯搞得懷疑人生,卻無法從文檔中獲取有效信息,好在最後總算是找到了。
第一次成功返回用户信息的時候,有一種如釋重負的感覺。
關鍵點:接口一定要選對,有依賴關係的接口版本要一致。