在企業(yè)級應用開發(fā)中,第三方授權登錄是常見需求。本文記錄如何基于 OAuth2.0 協(xié)議實現(xiàn)釘釘掃碼登錄功能。
釘釘OAuth2.0授權登錄對接
什么是OAuth2.0
OAuth2.0 是一種開放標準的授權協(xié)議,它允許用戶在不暴露賬號密碼的前提下,授權第三方應用訪問其在某個服務平臺上的資源。簡單來說,就是讓用戶可以安全地"借用"自己的身份給其他應用使用。
OAuth2.0 核心流程
- 觸發(fā)授權:用戶在應用中點擊登錄按鈕
- 跳轉(zhuǎn)授權頁:重定向到釘釘?shù)氖跈囗撁?/li>
- 用戶確認:用戶掃碼并同意授權,釘釘返回臨時授權碼(code)
- 換取令牌:后端用 code 向釘釘服務器換取 access_token
- 獲取信息:使用 access_token 調(diào)用釘釘接口獲取用戶信息
- 完成登錄:后端生成系統(tǒng)自身的登錄憑證,返回給前端
一、開發(fā)前準備
登錄 釘釘開發(fā)者后臺,點擊"創(chuàng)建應用"。

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

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

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

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

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

三、業(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)后,進入個人中心點擊"綁定釘釘":


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

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


掃碼確認后,后端會自動識別已綁定的賬號并完成登錄,用戶直接進入系統(tǒng)首頁。
參考資料
?? 如果在對接過程中遇到問題,歡迎在評論區(qū)留言討論~