文章目录
前言
企业级应用中,单点登录一般应用的较为广泛。如常见的企微单点快捷登录、QQ登录、微信登录等等。
本篇博客主要说明钉钉单点登录
的一些操作流程,从创建应用到实际应用全面说明。
参考资料
后端依赖
<!-- 新版依赖 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>1.1.86</version>
</dependency>
<!-- 旧版依赖 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
创建应用
1、进入钉钉开放平台
的应用开发
界面,创建应用。
2、进入已创建的应用详情页,在基础信息页面可以查看到应用的SuiteKey/SuiteSecret(第三方企业应用)或AppKey/AppSecret(企业内部应用)。
3、设置第三方页面的回调地址。
OAuth 登录授权
当要代表用户使用DingTalk OpenAPI读取和写入资源,需要通过OAuth 2.0授权流程完成授权。OAuth 2.0授权的流程如下图所示。
前端设置钉钉入口
方式一:开启第2个标签页展示二维码信息
在中间页面中,编写代码,携带重要参数,请求钉钉,获取钉钉返回的二维码页面信息,进行展示。
https://login.dingtalk.com/oauth2/auth?
redirect_uri=回调地址
&response_type=code
&client_id=dingbbbbbbb
&scope=openid corpid
&state=dddd
&prompt=consent
关于上述的调用url,携带参数说明参照官方说明:OAuth登录授权
当页面展示二维码后,APP端扫码后:
成功时跳转到:https://www.aaaaa.com/a/b?authCode=xxxx&state=dddd
失败时跳转到:https://www.aaaaa.com/a/b?error=yyyyyy&state=dddd
方式二:内嵌二维码方式登录授权
1、在页面中引入钉钉扫码登录JSSDK。
<script src="https://g.alicdn.com/dingding/h5-dingtalk-login/0.21.0/ddlogin.js"></script>
2、在需要引入扫码登录的地方,调用如下方法。
<!-- STEP1:在HTML中添加包裹容器元素 -->
<div id="self_defined_element" class="self-defined-classname"></div>
<style>
/* STEP2:指定这个包裹容器元素的CSS样式,尤其注意宽高的设置 */
.self-defined-classname {
width: 300px;
height: 300px;
}
</style>
<script>
// STEP3:在需要的时候,调用 window.DTFrameLogin 方法构造登录二维码,并处理登录成功或失败的回调。
window.DTFrameLogin(
{
id: 'self_defined_element',
width: 300,
height: 300,
},
{
redirect_uri: encodeURIComponent('http://www.aaaaa.com/a/b/'),
client_id: 'dingxxxxxxxxxxxx',
scope: 'openid',
response_type: 'code',
state: 'xxxxxxxxx',
prompt: 'consent',
},
(loginResult) => {
const {redirectUrl, authCode, state} = loginResult;
// 这里可以直接进行重定向
window.location.href = redirectUrl;
// 也可以在不跳转页面的情况下,使用code进行授权
console.log(authCode);
},
(errorMsg) => {
// 这里一般需要展示登录失败的具体原因
alert(`Login Error: ${errorMsg}`);
},
);
</script>
参数说明((TypeScript语言描述)):
// ********************************************************************************
// window.DTFrameLogin方法定义
// ********************************************************************************
window.DTFrameLogin: (
frameParams: IDTLoginFrameParams, // DOM包裹容器相关参数
loginParams: IDTLoginLoginParams, // 统一登录参数
successCbk: (result: IDTLoginSuccess) => void, // 登录成功后的回调函数
errorCbk?: (errorMsg: string) => void, // 登录失败后的回调函数
) => void;
// ********************************************************************************
// DOM包裹容器相关参数
// ********************************************************************************
// 注意!width与height参数只用于设置二维码iframe元素的尺寸,并不会影响包裹容器尺寸。
// 包裹容器的尺寸与样式需要接入方自己使用css设置
interface IDTLoginFrameParams {
id: string; // 必传,包裹容器元素ID,不带'#'
width?: number; // 选传,二维码iframe元素宽度,最小280,默认300
height?: number; // 选传,二维码iframe元素高度,最小280,默认300
}
// ********************************************************************************
// 统一登录参数
// ********************************************************************************
// 参数意义与“拼接链接发起登录授权”的接入方式完全相同(缺少部分参数)
// 增加了isPre参数来设定运行环境
interface IDTLoginLoginParams {
redirect_uri: string; // 必传,注意url需要encode
response_type: string; // 必传,值固定为code
client_id: string; // 必传
scope: string; // 必传,如果值为openid+corpid,则下面的org_type和corpId参数必传,否则无法成功登录
prompt: string; // 必传,值为consent。
state?: string; // 选传
org_type?: string; // 选传,当scope值为openid+corpid时必传
corpId?: string; // 选传,当scope值为openid+corpid时必传
exclusiveLogin?: string; // 选传,如需生成专属组织专用二维码时,可指定为true,可以限制非组织帐号的扫码
exclusiveCorpId?: string; // 选传,当exclusiveLogin为true时必传,指定专属组织的corpId
}
// ********************************************************************************
// 登录成功后返回的登录结果
// ********************************************************************************
interface IDTLoginSuccess {
redirectUrl: string; // 登录成功后的重定向地址,接入方可以直接使用该地址进行重定向
authCode: string; // 登录成功后获取到的authCode,接入方可直接进行认证,无需跳转页面
state?: string; // 登录成功后获取到的state
}
获取访问凭证 user_token (后端)
使用返回的auth_code和应用的信息,调用获取用户token接口得到access_token。
accessToken的有效期为7200秒(2小时),有效期内重复获取会返回相同结果并自动续期,过期后获取会返回新的accessToken。
// This file is auto-generated, don't edit it. Thanks.
package com.aliyun.sample;
import com.aliyun.tea.*;
import com.aliyun.teautil.*;
import com.aliyun.dingtalkoauth2_1_0.*;
import com.aliyun.dingtalkoauth2_1_0.models.*;
import com.aliyun.teaopenapi.*;
import com.aliyun.teaopenapi.models.*;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkoauth2_1_0.Client createClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkoauth2_1_0.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dingtalkoauth2_1_0.Client client = Sample.createClient();
GetUserTokenRequest getUserTokenRequest = new GetUserTokenRequest()
.setClientId("dingxxx")
.setClientSecret("1234")
// 前端请求后界面返回的参数值
// https://www.aaaaa.com/a/b?authCode=xxxx&state=dddd
// 对应 authCode
.setCode("abcd")
//.setRefreshToken("abcd")
.setGrantType("authorization_code");
try {
client.getUserToken(getUserTokenRequest);
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}
返回示例:
{
"accessToken" : "abcd",
"refreshToken" : "abcd",
"expireIn" : 7200,
"corpId" : "corpxxxx"
}
根据 user_token 获取当前扫码者的信息
// This file is auto-generated, don't edit it. Thanks.
package com.aliyun.sample;
import com.aliyun.tea.*;
import com.aliyun.teautil.*;
import com.aliyun.teautil.models.*;
import com.aliyun.dingtalkcontact_1_0.*;
import com.aliyun.dingtalkcontact_1_0.models.*;
import com.aliyun.teaopenapi.*;
import com.aliyun.teaopenapi.models.*;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkcontact_1_0.Client createClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkcontact_1_0.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dingtalkcontact_1_0.Client client = Sample.createClient();
GetUserHeaders getUserHeaders = new GetUserHeaders();
getUserHeaders.xAcsDingtalkAccessToken = "<your access token>";
try {
// 如需获取当前授权人的信息,unionId参数可以传me。
client.getUserWithOptions("me", getUserHeaders, new RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}
返回示例:
{
"nick" : "zhangsan",
"avatarUrl" : "https://xxx",
"mobile" : "150xxxx9144",
"openId" : "123",
"unionId" : "z21HjQliSzpw0Yxxxx",
"email" : "zhangsan@alibaba-inc.com",
"stateCode" : "86"
}
根据 unionId 获取用户id
上面的接口返回结果中,虽然有用户昵称
、用户手机号
等信息,如果当前企业应用中已经做了同步钉钉的组织架构与人员信息
的话,这些信息并不能与数据库中的数据进行匹配。
此时为了解决这个问题,需要调用另一个接口根据unionid获取用户userid
但是调用这个接口,需要注意一个坑:
请求这个接口的token,并
不是
上面的user_token
而是access_token
!!!
如果不是按照这个处理,即使在应用管理页面中,添加有qyapi_get_member
权限,依旧会出现无权限的问题。
1、获取 access_token。
import com.alibaba.fastjson.JSONObject;
import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
import lombok.extern.slf4j.Slf4j;
/**
* https://open.dingtalk.com/document/orgapp/obtain-the-access_token-of-an-internal-app
*/
@Slf4j
public class GetAccessTokenDemo {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkoauth2_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkoauth2_1_0.Client(config);
}
public static void main(String[] args) throws Exception {
com.aliyun.dingtalkoauth2_1_0.Client client = createClient();
com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest = new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
.setAppKey("应用的 Client ID")
.setAppSecret("应用的 Client Secret");
GetAccessTokenResponse accessToken = client.getAccessToken(getAccessTokenRequest);
log.info("请求回执信息:{}", JSONObject.toJSONString(accessToken.getBody()));
// {"accessToken":"6315e4d453583c69b1fb89ae455ba772","expireIn":7200}
System.out.println(accessToken.getBody().accessToken);
}
}
2、根据 access_token 和 unionId 调用接口获取 user_id 信息。
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/getbyunionid");
OapiUserGetbyunionidRequest req = new OapiUserGetbyunionidRequest();
req.setUnionid("用户信息的 unionId");
OapiUserGetbyunionidResponse rsp = client.execute(req, access_token);
System.out.println(rsp.getBody());
返回示例:
{
"errcode":"0",
"errmsg":"ok",
"result":{
"contact_type":"0",
"userid":"zhangsan"
},
"request_id": "zcqi5450rpit"
}
钉钉组织架构人员信息同步
钉钉的组织架构同步操作,并不能知道具体是哪个人员进行的登录操作。
所以同步需要统一使用access_token
进行鉴权操作。
获取 access_token
import com.alibaba.fastjson.JSONObject;
import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
import lombok.extern.slf4j.Slf4j;
/**
* https://open.dingtalk.com/document/orgapp/obtain-the-access_token-of-an-internal-app
*/
@Slf4j
public class GetAccessTokenDemo {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkoauth2_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkoauth2_1_0.Client(config);
}
public static void main(String[] args) throws Exception {
com.aliyun.dingtalkoauth2_1_0.Client client = createClient();
com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest = new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
.setAppKey("应用的 Client ID")
.setAppSecret("应用的 Client Secret");
GetAccessTokenResponse accessToken = client.getAccessToken(getAccessTokenRequest);
log.info("请求回执信息:{}", JSONObject.toJSONString(accessToken.getBody()));
// {"accessToken":"6315e4d453583c69b1fb89ae455ba772","expireIn":7200}
System.out.println(accessToken.getBody().accessToken);
}
}
获取当前企业根部门信息
public class Main {
public static void main(String[] args) {
try {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/department/get");
OapiV2DepartmentGetRequest req = new OapiV2DepartmentGetRequest();
// 默认值,可以不写
req.setDeptId(1L);
OapiV2DepartmentGetResponse rsp = client.execute(req, "access_token值");
System.out.println(rsp.getBody());
} catch (ApiException e) {
e.printStackTrace();
}
}
}
返回示例:
{
"errcode": 0,
"errmsg": "ok",
"result": {
"dept_permits": [
3,
4,
5
],
"outer_permit_users": [
"user123",
"1234"
],
"dept_manager_userid_list": [
"1020302901-431772414"
],
"org_dept_owner": "manager9153",
"outer_dept": false,
"dept_group_chat_id": "chat1fccdb4b921f2bde18c26xxxx",
"group_contain_sub_dept": true,
"auto_add_user": true,
"hide_dept": false,
"name": "测试",
"outer_permit_depts": [
500,
600
],
"user_permits": [],
"dept_id": 1,
"create_dept_group": true,
"order": 0,
"code":"100",
"union_dept_ext":{
"corp_id": "test",
"dept_id": 1234567
}
},
"request_id": "4e7ljtq91rgo"
}
获取当前企业中的部门列表信息
package test;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiV2DepartmentListsubRequest;
import com.dingtalk.api.response.OapiV2DepartmentListsubResponse;
import com.taobao.api.ApiException;
import lombok.extern.slf4j.Slf4j;
/**
* https://open.dingtalk.com/document/orgapp/obtain-the-department-list-v2
*/
@Slf4j
public class GetDeptMentInfo {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkoauth2_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkoauth2_1_0.Client(config);
}
public static void main(String[] args) throws ApiException {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/department/listsub");
OapiV2DepartmentListsubRequest req = new OapiV2DepartmentListsubRequest();
// 根目录 传递 1
req.setDeptId(1L);
OapiV2DepartmentListsubResponse rsp = client.execute(req, "access_token 值");
log.info(rsp.getBody());
/**
* {"errcode":0,"errmsg":"ok",
* "result":[
* {"auto_add_user":true,"create_dept_group":true,"dept_id":982430535,"name":"部门3","parent_id":1},
* {"auto_add_user":true,"create_dept_group":true,"dept_id":982694169,"name":"部门1","parent_id":1},
* {"auto_add_user":true,"create_dept_group":true,"dept_id":982793129,"name":"部门2","parent_id":1}],
* "request_id":"16lt30q49vmno"}
*/
}
}
返回示例:
/**
* {"errcode":0,"errmsg":"ok",
* "result":[
* {"auto_add_user":true,"create_dept_group":true,"dept_id":982430535,"name":"部门3","parent_id":1},
* {"auto_add_user":true,"create_dept_group":true,"dept_id":982694169,"name":"部门1","parent_id":1},
* {"auto_add_user":true,"create_dept_group":true,"dept_id":982793129,"name":"部门2","parent_id":1}],
* "request_id":"16lt30q49vmno"}
*/
根据部门id 查询部门下的人员信息
人员每次最大查询数为
100
。
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiUserListsimpleRequest;
import com.dingtalk.api.response.OapiUserListsimpleResponse;
import com.taobao.api.ApiException;
import lombok.extern.slf4j.Slf4j;
/**
* https://open.dingtalk.com/document/orgapp/queries-the-simple-information-of-a-department-user
*/
@Slf4j
public class GetUserFromDeptDemo {
public static void main(String[] args) throws ApiException {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/listsimple");
OapiUserListsimpleRequest req = new OapiUserListsimpleRequest();
req.setDeptId(982430535L);
req.setCursor(0L);
req.setSize(100L);
req.setOrderField("modify_desc");
req.setContainAccessLimit(false);
req.setLanguage("zh_CN");
OapiUserListsimpleResponse rsp = client.execute(req, "access_token值");
log.info(rsp.getBody());
/**
* {"errcode":0,"errmsg":"ok",
* "result":{"has_more":false,"list":[]},"request_id":"15rd4f5edtl9l"}
*/
}
}