在企业级应用开发中,第三方授权登录是常见需求。本文记录如何基于 OAuth2.0 协议实现钉钉扫码登录功能。
钉钉OAuth2.0授权登录对接
什么是OAuth2.0
OAuth2.0 是一种开放标准的授权协议,它允许用户在不暴露账号密码的前提下,授权第三方应用访问其在某个服务平台上的资源。简单来说,就是让用户可以安全地"借用"自己的身份给其他应用使用。
OAuth2.0 核心流程
- 触发授权:用户在应用中点击登录按钮
- 跳转授权页:重定向到钉钉的授权页面
- 用户确认:用户扫码并同意授权,钉钉返回临时授权码(code)
- 换取令牌:后端用 code 向钉钉服务器换取 access_token
- 获取信息:使用 access_token 调用钉钉接口获取用户信息
- 完成登录:后端生成系统自身的登录凭证,返回给前端
一、开发前准备
登录 钉钉开发者后台,点击"创建应用"。

填写应用名称和描述,点击确定完成创建。

创建成功后,在应用详情页找到 AppKey 和 AppSecret,这两个参数后续开发会用到,建议先记录下来。

进入"权限管理"页面,开通以下两个必要权限:

在"登录与分享"配置中设置回调域名。这个地址是前端接收授权码的页面地址。如果是本地开发测试,可以使用 cpolar 等内网穿透工具将本地服务暴露到公网。

二、整体交互流程
整个登录过程涉及四个角色:钉钉用户、前端页面、后端服务和钉钉开放平台。具体流程如下:

三、业务场景设计
在实际项目中,我们通常会遇到两种场景:
场景一:账号绑定
用户先使用账号密码登录系统,然后在个人中心绑定钉钉账号。绑定过程走完整的 OAuth2.0 流程,将获取到的钉钉用户标识(unionId/openId)与系统用户关联存储。
场景二:钉钉直接登录
用户直接通过钉钉扫码登录。后端拿到钉钉返回的用户标识后,在数据库中查找是否已绑定该系统账号。如果找到,则直接生成系统登录凭证完成自动登录;如果未绑定,则提示用户先进行账号绑定。
四、Java 代码实现
为了便于后续扩展其他 OAuth2.0 提供商(如微信、企业微信等),我们先定义一个通用接口:
public interface OAuthProviderClient {
/**
* provider 标识。
*/
String provider();
/**
* provider 是否可用。
*/
boolean enabled();
/**
* 解析回调地址:请求传值优先,未传使用配置默认值。
*/
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();
}
通过以上步骤,我们已经拿到了钉钉用户的唯一标识(openId/unionId)和昵称等信息。接下来就可以根据业务需求,实现账号绑定或直接登录的逻辑了。
五、实际效果演示
1. 账号绑定
用户登录系统后,进入个人中心点击"绑定钉钉":


跳转到钉钉授权页面,使用钉钉 APP 扫码确认:

2. 钉钉扫码登录
在登录页面选择"钉钉登录",同样会弹出扫码授权页面:


扫码确认后,后端会自动识别已绑定的账号并完成登录,用户直接进入系统首页。
参考资料
💡 如果在对接过程中遇到问题,欢迎在评论区留言讨论~