動態

詳情 返回 返回

釘釘企業內部應用SSO單點登錄實戰及踩坑過程 - 動態 詳情

前言

之前一直因為騰訊的文檔可讀性差而吐槽,而這次對接釘釘開放平台時也遇到了很多問題。

一句話概括原因:當前(2025年)正值釘釘兩代API切換的過程中,新舊API同時存在,造成釘釘官方文檔內容分散,來不及更新,且第三方博客新舊共存。初次接觸時無從下手,API調用時因為版本不對可能導致問題。

本文基於最新的API及文檔,儘可能全面的描述釘釘SSO流程。

SSO

SSO(Single Sign-On,單點登錄)是一種身份驗證機制。通俗的説,假設要新開發一個項目B,希望充分利用已有歷史項目A中的用户信息,最終實現在系統A中登錄後,用户在系統A中的鑑權可以帶到系統B中,無需再次登錄。

例如常見的微信和釘釘都提供了SSO,用户在綁定微信和第三方平台後,就可以從微信一鍵登錄第三方平台。

對於一般情況下的SSO時序圖也是老生產談了。

圖片.png

釘釘企業自建應用的三種SSO方式

具體到釘釘上,釘釘提供了三種SSO方式:

  1. 釘釘內點擊程序圖標,①直接請求釘釘平台,②帶CODE回調到後端,③後端完成校驗+登錄(官方稱為:使用釘釘提供的頁面登錄授權)
  2. 釘釘內點擊程序圖標,①加載前端,②通過JSAPI請求CODE發送給後端,③後端完成校驗+登錄(這個可以在H5免登的文檔中獲取DEMO)
  3. 釘釘內點擊程序圖標,①加載前端,②用户掃碼獲取CODE發送給後端,③後端完成校驗+登錄(官方稱為:內嵌二維碼方式登錄授權)

第一種方式最省事,後端和釘釘交互,前端不用動。但測試了一下沒有成功,文檔鏈接 https://open.dingtalk.com/document/orgapp/obtain-identity-cre... ,文檔是基於v1版本,應該是不適配v2了。

第一種方式的時序圖,基於官方的進行修改,增加詳細步驟(已經不能用了,留個紀念):

圖片.png

本文主要場景是第二種,需要用到前端的jsapi,實際上只要把坑都注意到,原理都差不多。

時序圖:

圖片.png

核心就兩步(也是最容易出問題的步驟):

  1. 訪問https://oapi.dingtalk.com/gettoken,攜帶appKeyappSecret,獲取access_token
  2. 訪問https://oapi.dingtalk.com/topapi/v2/user/getuserinfo,攜帶access_tokencode,獲取用户信息

理論搞明白了,接下來介紹如何DEBUG

如何找文檔、調試

從本節起,開始進入避坑的部分。

一,文檔

第一眼看到文檔可能會無所適從,如果照着圖中的箭頭點進來,可能會發現同一個話題中,引用文檔和當前話題中説的不一致,前者讓下載JSSDK balabala,後者只説調用接口,也不知道該聽誰的。

圖片.png


二、接口

再看接口調試程序,光是獲取AccessToken就有一堆

圖片.png

然後獲取用户信息的也有四個

圖片.png

其中的接口有新的有舊的,上文提到的文檔中也混雜着兩種寫法。

最大的麻煩是:如果調試接口時報錯,根本分不清是調試過程有問題,還是接口找錯了。

進一步地,由於反覆嘗試出錯,官方又沒有一篇文章能拍胸脯説“看我,我就是最新的!”,所以會原地打轉消耗很多無用時間。


怎麼辦?

清楚自己現在做的是什麼

通常,下圖中的應用被官方稱為“企業內部應用”,所以查文檔時要注意這個關鍵詞,而不是別的關鍵詞。

圖片.png

通過API路徑區分版本

  • 舊的API路徑前綴/v1.0
  • 新的API路徑包含v2

這是非常關鍵的信息,通常v1和v1搭配使用,v2和v2搭配使用,如果搞錯,就回出現驢 + 馬 = 騾子,而騾子是無法通過驗證的,表現出來就是你感覺哪寫的都對,但就是報錯提示token無效。

圖片.png

圖片.png

此外還有一個技巧:把鼠標放在參數上,可以看到參數的來源,這個信息通常不會錯,就容易看到API的依賴關係了。

圖片.png

不要過於相信文檔

即使有些文檔發佈與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分鐘,只能用一次。

總結一下:

  1. 我們需要記下cropId、ClientId、Client Secret
  2. 後端會定時獲取access_token
  3. 每次登錄會實時生成一次性code
  4. 其他信息都不再需要了

基本設置:

網頁應用——把首頁地址設置為前端的SSO登錄組件對應的地址:

圖片.png

安全設置——服務器出口IP是開發者公網IP,把回調域名設置為後端SSO登錄的方法

圖片.png

然後發佈應用,在釘釘中能看到即可。

前端編碼

前端項目添加依賴:

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,點擊調試就能看到控制枱和網絡了。

圖片.png

後端編碼

在後端實現之前,為了調試前端,至少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;
    }

授人以漁——代碼怎麼來的?基本不需要看文檔,而是去看接口。

首先確認使用下圖兩個接口:

圖片.png

圖片.png

接下來只需要選擇這兩個接口,填入信息,嘗試發起請求,成功後搬走代碼自行修改即可。

如何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" }之類的報錯搞得懷疑人生,卻無法從文檔中獲取有效信息,好在最後總算是找到了。

第一次成功返回用户信息的時候,有一種如釋重負的感覺。

關鍵點:接口一定要選對,有依賴關係的接口版本要一致。

user avatar u_16297326 頭像 vanve 頭像 AmbitionGarden 頭像 u_15702012 頭像 ligaai 頭像 daqianduan 頭像 jkdataapi 頭像 aipaobudeshoutao 頭像 lenve 頭像 wnhyang 頭像 java_study 頭像 linybjikezhilu 頭像
點贊 94 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.