6

前言

之前一直因为腾讯的文档可读性差而吐槽,而这次对接钉钉开放平台时也遇到了很多问题。

一句话概括原因:当前(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" }之类的报错搞得怀疑人生,却无法从文档中获取有效信息,好在最后总算是找到了。

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

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


LYX6666
1.6k 声望89 粉丝

一个正在茁壮成长的零基础小白