钉钉OAuth2.0授权登录对接

在企业级应用开发中,第三方授权登录是常见需求。本文记录如何基于 OAuth2.0 协议实现钉钉扫码登录功能。

钉钉OAuth2.0授权登录对接

什么是OAuth2.0

OAuth2.0 是一种开放标准的授权协议,它允许用户在不暴露账号密码的前提下,授权第三方应用访问其在某个服务平台上的资源。简单来说,就是让用户可以安全地"借用"自己的身份给其他应用使用。

OAuth2.0 核心流程

  1. 触发授权:用户在应用中点击登录按钮
  2. 跳转授权页:重定向到钉钉的授权页面
  3. 用户确认:用户扫码并同意授权,钉钉返回临时授权码(code)
  4. 换取令牌:后端用 code 向钉钉服务器换取 access_token
  5. 获取信息:使用 access_token 调用钉钉接口获取用户信息
  6. 完成登录:后端生成系统自身的登录凭证,返回给前端

一、开发前准备

登录 钉钉开发者后台,点击"创建应用"。

image.png

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

image.png

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

image.png

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

image.png

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

image.png

二、整体交互流程

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

image.png

三、业务场景设计

在实际项目中,我们通常会遇到两种场景:

场景一:账号绑定

用户先使用账号密码登录系统,然后在个人中心绑定钉钉账号。绑定过程走完整的 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. 账号绑定

用户登录系统后,进入个人中心点击"绑定钉钉":

image.png
image.png

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

image.png

2. 钉钉扫码登录

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

image.png
image.png

扫码确认后,后端会自动识别已绑定的账号并完成登录,用户直接进入系统首页。

参考资料


💡 如果在对接过程中遇到问题,欢迎在评论区留言讨论~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容