欢迎光临
我们一直在努力

钉钉企业内部应用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" }之类的报错搞得怀疑人生,却无法从文档中获取有效信息,好在最后总算是找到了。

第一次成功返回用户信息的时候,有一种如释重负的感觉。

关键点:接口一定要选对,有依赖关系的接口版本要一致。

https://segmentfault.com/a/1190000047304258

未经允许不得转载:IT极限技术分享汇 » 钉钉企业内部应用SSO单点登录实战及踩坑过程

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址