釘釘OAuth2.0授權登錄對接

在企業(yè)級應用開發(fā)中,第三方授權登錄是常見需求。本文記錄如何基于 OAuth2.0 協(xié)議實現(xiàn)釘釘掃碼登錄功能。

釘釘OAuth2.0授權登錄對接

什么是OAuth2.0

OAuth2.0 是一種開放標準的授權協(xié)議,它允許用戶在不暴露賬號密碼的前提下,授權第三方應用訪問其在某個服務平臺上的資源。簡單來說,就是讓用戶可以安全地"借用"自己的身份給其他應用使用。

OAuth2.0 核心流程

  1. 觸發(fā)授權:用戶在應用中點擊登錄按鈕
  2. 跳轉(zhuǎn)授權頁:重定向到釘釘?shù)氖跈囗撁?/li>
  3. 用戶確認:用戶掃碼并同意授權,釘釘返回臨時授權碼(code)
  4. 換取令牌:后端用 code 向釘釘服務器換取 access_token
  5. 獲取信息:使用 access_token 調(diào)用釘釘接口獲取用戶信息
  6. 完成登錄:后端生成系統(tǒng)自身的登錄憑證,返回給前端

一、開發(fā)前準備

登錄 釘釘開發(fā)者后臺,點擊"創(chuàng)建應用"。

image.png

填寫應用名稱和描述,點擊確定完成創(chuàng)建。

image.png

創(chuàng)建成功后,在應用詳情頁找到 AppKey 和 AppSecret,這兩個參數(shù)后續(xù)開發(fā)會用到,建議先記錄下來。

image.png

進入"權限管理"頁面,開通以下兩個必要權限:

image.png

在"登錄與分享"配置中設置回調(diào)域名。這個地址是前端接收授權碼的頁面地址。如果是本地開發(fā)測試,可以使用 cpolar 等內(nèi)網(wǎng)穿透工具將本地服務暴露到公網(wǎng)。

image.png

二、整體交互流程

整個登錄過程涉及四個角色:釘釘用戶、前端頁面、后端服務和釘釘開放平臺。具體流程如下:

image.png

三、業(yè)務場景設計

在實際項目中,我們通常會遇到兩種場景:

場景一:賬號綁定

用戶先使用賬號密碼登錄系統(tǒng),然后在個人中心綁定釘釘賬號。綁定過程走完整的 OAuth2.0 流程,將獲取到的釘釘用戶標識(unionId/openId)與系統(tǒng)用戶關聯(lián)存儲。

場景二:釘釘直接登錄

用戶直接通過釘釘掃碼登錄。后端拿到釘釘返回的用戶標識后,在數(shù)據(jù)庫中查找是否已綁定該系統(tǒng)賬號。如果找到,則直接生成系統(tǒng)登錄憑證完成自動登錄;如果未綁定,則提示用戶先進行賬號綁定。

四、Java 代碼實現(xiàn)

為了便于后續(xù)擴展其他 OAuth2.0 提供商(如微信、企業(yè)微信等),我們先定義一個通用接口:

public interface OAuthProviderClient {

    /**
     * provider 標識。
     */
    String provider();

    /**
     * provider 是否可用。
     */
    boolean enabled();

    /**
     * 解析回調(diào)地址:請求傳值優(yōu)先,未傳使用配置默認值。
     */
    String resolveRedirectUri(String requestRedirectUri);

    /**
     * 構建授權地址。
     */
    String buildAuthorizeUrl(String state, String redirectUri);

    /**
     * 使用授權碼換取用戶身份。
     */
    OAuthUserIdentity fetchIdentity(String code, String redirectUri);
}

構建授權 URL

這個方法用于生成釘釘掃碼登錄頁面的地址,對應流程中的第 2 步:

@Override
    public String buildAuthorizeUrl(String state, String redirectUri) {
        OAuthProperties.ProviderConfig config = requireConfig();
        String resolvedRedirectUri = resolveRedirectUri(redirectUri);

        String scope = defaultIfBlank(config.getScope(), "openid");
        return UriComponentsBuilder.fromUriString(requireValue(config.getAuthorizeUri(), "釘釘 authorizeUri 未配置"))
                .queryParam("client_id", requireValue(config.getClientId(), "釘釘 clientId 未配置"))
                .queryParam("response_type", "code")
                .queryParam("redirect_uri", resolvedRedirectUri)
                .queryParam("scope", scope)
                //值為consent時,會進入授權確認頁
                .queryParam("prompt", "consent")
                .queryParam("state", requireValue(state, "state 不能為空"))
                .build(true)
                .toUriString();
    }

換取用戶身份信息

這一步是整個流程的核心,包含兩個關鍵操作:用 code 換取 access_token,再用 access_token 獲取用戶信息。對應流程中的第 4、5 步:

@Override
    public OAuthUserIdentity fetchIdentity(String code, String redirectUri) {
        OAuthProperties.ProviderConfig config = requireConfig();
        RestClient restClient = restClientBuilder.build();

        Map<String, Object> tokenRequest = new LinkedHashMap<>();
        tokenRequest.put("clientId", requireValue(config.getClientId(), "釘釘 clientId 未配置"));
        tokenRequest.put("clientSecret", requireValue(config.getClientSecret(), "釘釘 clientSecret 未配置"));
        tokenRequest.put("code", requireValue(code, "code 不能為空"));
        tokenRequest.put("grantType", "authorization_code");

        Map<String, Object> tokenResponse;
        try {
            tokenResponse = restClient.post()
                    .uri(requireValue(config.getTokenUri(), "釘釘 tokenUri 未配置"))
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(tokenRequest)
                    .retrieve()
                    .body(new ParameterizedTypeReference<>() {
                    });
            if (log.isInfoEnabled()) {
                log.info("DingTalk OAuth token response received");
            }
        } catch (Exception ex) {
            logDingTalkError("釘釘換取 accessToken 失敗", ex);
            throw new BusinessException(ErrorCode.OAUTH_LOGIN_FAILED, "釘釘換取 accessToken 失敗");
        }

        String accessToken = pickString(tokenResponse, "accessToken", "access_token");
        if (isBlank(accessToken)) {
            log.info("DingTalk OAuth token response missing accessToken, response={}", safeToString(tokenResponse));
            throw new BusinessException(ErrorCode.OAUTH_LOGIN_FAILED, "釘釘 accessToken 為空");
        }

        Map<String, Object> userResponse;
        try {
            userResponse = restClient.get()
                    .uri(requireValue(config.getUserInfoUri(), "釘釘 userInfoUri 未配置"))
                    .header("x-acs-dingtalk-access-token", accessToken)
                    .retrieve()
                    .body(new ParameterizedTypeReference<>() {
                    });
            if (log.isInfoEnabled()) {
                log.info("DingTalk OAuth user-info response received");
            }
        } catch (Exception ex) {
            logDingTalkError("釘釘獲取用戶信息失敗", ex);
            throw new BusinessException(ErrorCode.OAUTH_LOGIN_FAILED, "釘釘獲取用戶信息失敗");
        }

        String unionId = pickString(userResponse, "unionId", "unionid");
        String openId = pickString(userResponse, "openId", "openid");
        String nickname = pickString(userResponse, "nick", "nickname", "name");

        if (isBlank(openId) && !isBlank(unionId)) {
            openId = unionId;
        }
        if (isBlank(openId)) {
            log.info("DingTalk OAuth user-info missing openId, response={}", safeToString(userResponse));
            throw new BusinessException(ErrorCode.OAUTH_LOGIN_FAILED, "釘釘用戶標識為空");
        }

        return OAuthUserIdentity.builder()
                .provider(PROVIDER)
                .openId(openId)
                .unionId(unionId)
                .nickname(nickname)
                .build();
    }

通過以上步驟,我們已經(jīng)拿到了釘釘用戶的唯一標識(openId/unionId)和昵稱等信息。接下來就可以根據(jù)業(yè)務需求,實現(xiàn)賬號綁定或直接登錄的邏輯了。

源代碼

五、實際效果演示

1. 賬號綁定

用戶登錄系統(tǒng)后,進入個人中心點擊"綁定釘釘":

image.png
image.png

跳轉(zhuǎn)到釘釘授權頁面,使用釘釘 APP 掃碼確認:

image.png

2. 釘釘掃碼登錄

在登錄頁面選擇"釘釘?shù)卿?,同樣會彈出掃碼授權頁面:

image.png
image.png

掃碼確認后,后端會自動識別已綁定的賬號并完成登錄,用戶直接進入系統(tǒng)首頁。

參考資料


?? 如果在對接過程中遇到問題,歡迎在評論區(qū)留言討論~

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容