本文是根据钉钉官方文档归纳所出,最后有 PHP 测试成功的 demo。
概述
钉钉登录是基于 OAuth2.0 协议标准构建的钉钉 OAuth2.0 授权登录系统。 在进行钉钉 OAuth2.0 授权登录接入之前,需要先创建一个应用,并获得相应的
AgentId
和AppSecret
。
OAuth2.0 授权说明
钉钉 OAuth2.0 授权登录让钉钉用户使用钉钉身份安全登录第三方应用或网站,在钉钉用户授权登录已接入钉钉 OAuth2.0 的第三方应用后,第三方可以获取到用户的接口调用凭证
sns_token
,通过sns_token
可以进行钉钉开放平台授权关系接口调用,从而实现获取钉钉用户基本开放信息和帮助用户实现基础开放功能等。
获取登录用户信息时序图:
钉钉 OAuth2.0 授权登录支持
authorization_code
模式,适用于拥有 server 端的应用授权。该模式整体流程为:
- 第三方发起钉钉授权登录请求,钉钉用户允许授权第三方应用后,钉钉会拉起应用或重定向到第三方网站,并且带上授权临时票据
code
参数。- 通过
Code
调用sns/getuserinfo_bycode
接口获取授权登录用户信息。
配置参数说明
Corpid 是企业在钉钉中的标识,每个企业拥有唯一的 Corpid
UserID 企业内每个员工都有唯一的 UserID,创建后不可修改。
Unionid 用户在当前钉钉开放平台账号范围内的唯一标识,同一个钉钉开放平台账号可以包含多个开放应用
AppKey/AppSecret
- Appkey 是 企业内部应用 应用的唯一身份标识
- AppSecret 是对应的调用密钥
扫码登录第三方网站
实现扫码登录第三方网站。扫码登录指钉钉客户端扫码并确认登录 web 系统,在系统内获取正在访问用户的钉钉身份,无需输入账号密码的开发流程。
注意
- 此网站并不是钉钉客户端内使用的企业/第三方企业应用。
- 此功能与企业自建应用/第三方企业应用无关,并且不是钉钉内的应用免登,此功能只能获取到用户 unionId(无手机号和企业相关信息)。
- 第三方企业应用可根据用户 unionId 判断当前扫码用户是否为企业内部员工。
- 如果扫码用户是当前企业内部员工,可以根据文档示例,使用 unionId 获取 userid,并获取用户详情。
- 如果扫码用户是钉钉三方应用授权企业内员工,获取 unionId 后,可以根据 unionId 在 ISV 获取的授权企业内员工信息表格中,检索得到该扫码人信息。
- 如果扫码用户是外部用户需企业自行处理。
开发流程
1. 配置回调域名
创建 企业内部 应用,详情请参考创建应用。
应用创建后,在基础信息页面可以查看到应用的 AppKey 和 AppSecret。
进入应用详情页,然后单击钉钉登录与分享,添加应用回调的URL,以 http 或 https 开头。
2. 构造扫码登录页面
Web 系统可以通过两种方式实现钉钉扫码登录。
方式一 : 使用钉钉提供的扫码登录页面
在企业 Web 系统里,用户点击 使用钉钉扫码登录 时第三方 Web 系统跳转到如下地址 :
https://oapi.dingtalk.com/connect/qrconnect?appid=AppKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI
url 里的参数需要换成第三方 Web 系统对应的参数。在钉钉用户扫码登录并确认后,会 302 到你指定的
redirect_uri
,并向 url 参数中追加临时授权友code
及state
两个参数。
注意:参数
redirect_uri=REDIRECT_URI
涉及的域名,需和登录配置的回调域名一致,否则会提示无权限访问。
方式二 : 支持网站将钉钉登录二维码内嵌到自己页面中
用户使用钉钉扫码登录后 JS 会将
loginTmpCode
返回给网站。
JS钉钉登录主要用途:网站希望用户在网站内就能完成登录,无需跳转到钉钉域下登录后再返回,提升钉钉登录的流畅性与成功率。
网站内嵌钉钉二维码实现钉钉登录 JS 实现办法
// 1. 在页面中先引入如下 JS 文件 (支持 HTTPS)
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
// 2. 在需要使用钉钉登录的地方实例以下 JS 对象
/*
* 解释一下goto参数,参考以下例子:
* var url = encodeURIComponent('http://localhost.me/index.php?test=1&aa=2');
* var goto = encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=AppKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri='+url)
*/
var obj = DDLogin({
id:"login_container",//这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
goto: "", //请参考注释里的方式
style: "border:none;background-color:#FFFFFF;",
width : "365",
height: "400"
});
参数说明
引入的 js 会在获取用户扫描之后将获取的
loginTmpCode
通过window.parent.postMessage(loginTmpCode,'*');
返回给您的网站。
1. 可以通过以下代码获取
loginTmpCode
var handleMessage = function (event) { var origin = event.origin; console.log("origin", event.origin); if( origin == "https://login.dingtalk.com" ) { //判断是否来自ddLogin扫码事件。 var loginTmpCode = event.data; //获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了 console.log("loginTmpCode", loginTmpCode); } }; if (typeof window.addEventListener != 'undefined') { window.addEventListener('message', handleMessage, false); } else if (typeof window.attachEvent != 'undefined') { window.attachEvent('onmessage', handleMessage); }
2. 通过 JS 获取到
loginTmpCode
后,需要构造并跳转到如下链接:https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=AppKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI&loginTmpCode=loginTmpCode
3. 此链接处理成功后,会 302 跳转到
goto
参数指定的redirect_uri
,并向 url 参数中追加临时授权码code
及state
参数。
3. 服务端通过临时授权码获取授权用户的个人信息。
调用
sns/getuserinfo_bycode
接口获取授权用户的个人信息,主要是为了拿到用户的unionid
,详情请参考 根据sns临时授权码获取用户信息。
注意 : 通过临时授权码Code
获取用户信息,临时授权码只能使用一次。
4. 根据 unionid 获取 userid。
调用
user/getbyunionid
接口获取userid
,详情请参考 根据unionid获取用户信息。
说明 : 根据unionid
获取userid
,需要创建企业内部应用(小程序或微应用),使用内部应用的 Appkey 和 AppSecret 调用接口 access_token。
5. 根据 userid 获取用户详情。
调用
user/get
接口获取用户信息,详情请参考获取用户详情。
具体实现过程
第一步: 钉钉配置
第二步: 前端 - 使用钉钉提供的扫码登录页面
第一种: 按钮链接跳转
在企业 Web 系统里,用户点击 使用钉钉扫码登录 按钮时跳转到如下地址 :
https://oapi.dingtalk.com/connect/qrconnect?appid=AppKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI
url 里的大写单词是需要替换的变量值。在钉钉用户扫码登录并确认后,会 302 到你指定的
redirect_uri
,并向 url 参数中追加临时授权友code
及state
两个参数。
注意:参数
redirect_uri=REDIRECT_URI
涉及的域名,需和登录配置的回调域名一致,否则会提示无权限访问。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>钉钉按钮登录</title>
</head>
<body>
<a href="https://oapi.dingtalk.com/connect/qrconnect?appid=AppKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI">使用钉钉扫码登录</a>
</body>
</html>
第二种: 生成扫描二维码
根据自己的配置进行调整
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>钉钉扫码登录</title>
</head>
<body>
<div id="login_container"></div>
</body>
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
<script>
/*
* 解释一下goto参数,参考以下例子:
* var url = encodeURIComponent('http://localhost.me/index.php?test=1&aa=2');
* var goto = encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=AppKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri='+url)
*/
// 回调 URL
var url = encodeURIComponent("<!--{$this->redirect_uri}-->");
// goto url
var goto = encodeURIComponent("<!--{$this->goto}-->"+url);
var obj = DDLogin({
id:"login_container",//这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
goto: goto, //请参考注释里的方式
style: "border:none;background-color:#FFFFFF;",
width : "365",
height: "400"
});
var handleMessage = function (event) {
var origin = event.origin;
console.log("origin", event.origin);
if( origin == "https://login.dingtalk.com" ) {
//判断是否来自ddLogin扫码事件。
var loginTmpCode = event.data;
//获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了
// 构造链接跳转
// 链接格式 https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=AppKey&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI&loginTmpCode=loginTmpCode
var http_url = "<!--{$this->http_url}-->" + loginTmpCode;
window.location.href = http_url;
}
};
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', handleMessage, false);
} else if (typeof window.attachEvent != 'undefined') {
window.attachEvent('onmessage', handleMessage);
}
</script>
</html>
第三步: 后端 - 回调接口处理
1. 服务端通过临时授权码 Code
获取授权用户的个人信息。
调用
sns/getuserinfo_bycode
接口获取授权用户的个人信息,主要是为了拿到用户的unionid
,详情请参考 根据sns临时授权码获取用户信息。
注意 : 通过临时授权码Code
获取用户信息,临时授权码只能使用一次。
请求示例 (PHP SDK)
// 请求示例 (PHP SDK)
include "TopSdk.php";
$c = new DingTalkClient(DingTalkConstant::$CALL_TYPE_OAPI, DingTalkConstant::$METHOD_POST , DingTalkConstant::$FORMAT_JSON);
$req = new OapiSnsGetuserinfoBycodeRequest;
// 设置 Code 值,
$code = $_GET['code']
$req->setTmpAuthCode($code);
// 改为自己的 AppId 与 AppSecret
$responce_obj = $c->executeWithAccessKey($req, "https://oapi.dingtalk.com/sns/getuserinfo_bycode","yourAppId","yourAppSecret");
var_dump($responce_obj)
返回示例
// 主要是为了获取 unionid,下一步使用 unionid 获取 userid
{
"errcode":0,
"user_info":{
"nick":"名字",
"unionid":"dingdkjjojoixxxx",
"openid":"dingsdsqwlklklxxxx",
"main_org_auth_high_level":true
},
"errmsg":"ok"
}
返回参数说明
2. 根据 unionid 获取 userid。
调用
user/getbyunionid
接口获取userid
,详情请参考 根据unionid获取用户信息。
说明 : 根据unionid
获取userid
,需要创建企业内部应用(小程序或微应用),使用内部应用的 Appkey 和 AppSecret 调用接口 access_token。
请求示例(PHP SDK)
//先获取 access_token, 文档链接: https://developers.dingtalk.com/document/app/obtain-orgapp-token
$access_token = 'abc';
include "TopSdk.php";
date_default_timezone_set('Asia/Shanghai');
$c = new DingTalkClient(DingTalkConstant::$CALL_TYPE_OAPI, DingTalkConstant::$METHOD_POST , DingTalkConstant::$FORMAT_JSON);
$req = new OapiUserGetbyunionidRequest;
$unionid = $responce_array['user_info']['unionid'];
$req->setUnionid($unionid);
$response_obj = $c->execute($req, $access_token, "https://oapi.dingtalk.com/topapi/user/getbyunionid");
返回示例
{
"errcode":"0",
"errmsg":"ok",
"result":{
"contact_type":"0",
"userid":"zhangsan"
},
"request_id": "zcqi5450rpit"
}
返回参数说明
3. 根据 userid 获取用户详情。
调用
user/get
接口获取用户信息,详情请参考获取用户详情。
请求示例(PHP SDK)
include "TopSdk.php";
date_default_timezone_set('Asia/Shanghai');
$c = new DingTalkClient(DingTalkConstant::$CALL_TYPE_OAPI, DingTalkConstant::$METHOD_POST , DingTalkConstant::$FORMAT_JSON);
$req = new OapiV2UserGetRequest;
// 设置 uid
$userid = $response_array['result']['userid'];
$req->setUserid($userid);
$resp = $c->execute($req, $access_token, "https://oapi.dingtalk.com/topapi/v2/user/get");
返回示例
-
企业内部应用
{ "errcode": 0, "errmsg": "ok", "result": { "extension": "{\"爱好\":\"旅游\",\"年龄\":\"24\"}", "unionid": "xBnhjgjmofhhsLxxx", "boss": false, "unionEmpExt": { "corpId": "ding1c417cfd9e3142d1acaaa37764f9xxxx", "userid": "45694432-1019596262", "unionEmpMapList": [ { "corpId": "ding1c417cfd9e3142d1acaaa37764f9xxxx", "userid": "45694432-1019596262" } ] }, "role_list": [ { "group_name": "默认", "id": 1507113578, "name": "主管理员" } ], "admin": true, "remark": "杨XX", "title": "服务经理", "hired_date": 1598457600000, "userid": "manager4220", "work_place": "杭州", "dept_order_list": [ { "dept_id": 1, "order": 176318669012199520 }, { "dept_id": 379661095, "order": 176318556766960500 } ], "real_authed": true, "dept_id_list": [ 1, 379661095 ], "job_number": "10001", "email": "[email protected]", "leader_in_dept": [ { "dept_id": 379661095, "leader": false }, { "dept_id": 1, "leader": false } ], "manager_userid": "user01", "mobile": "188xxxx1234", "active": true, "telephone": "010-8xxxx6-2345", "avatar": "", "hide_mobile": false, "senior": false, "name": "杨xxx", "state_code": "86" }, "request_id": "51jql88tpa6g" }
返回参数说明
4. PHP 代码整合
前三步所实现的代码,在同一个逻辑处理中,我来将他整合一下,以下为我自己的测试的 demo ,可根据自己逻辑进行调整。
必须修改
注意 : 要想代码跑的通,必须修改 以下地方
在下载的 钉钉 SDK 包里找到./dingtalk/DingTalkClient.php
文件,约481行
将private function getMillisecond() { list($s1, $s2) = explode(' ', microtime()); return (float)sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000); }
替换为
private function getMillisecond() { list($s1, $s2) = explode(' ', microtime()); return (string)sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000); }
代码示例
<?php
include "./TopSdk.php";
date_default_timezone_set('Asia/Shanghai');
// 基本配置, 替换成自己的
$AgentId = '';
$AppKey = '';
$AppSecret = '';
// 获取传值
$get = $_GET;
file_put_contents('./data.log', 'Code: ' . $get['code']. ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
// 第一步 : 先获取 access_token - 开始 , access_token 有效期为 7200秒,有效期内重复获取会返回相同结果并自动续期,过期后再获取会返回新的 access_token
// 这里没有将 access_token 缓存, 实际操作时, 可将 access_token 进行缓存
$get_c = new DingTalkClient(DingTalkConstant::$CALL_TYPE_OAPI, DingTalkConstant::$METHOD_GET , DingTalkConstant::$FORMAT_JSON);
$access_token_request = new OapiGettokenRequest;
$access_token_request->setAppkey($AppKey);
$access_token_request->setAppsecret($AppSecret);
$access_token_response = $get_c->execute($access_token_request, $access_token, "https://oapi.dingtalk.com/gettoken");
if ($access_token_response->errcode != 0){
// 错误
} else {
$access_token = $access_token_response->access_token;
}
file_put_contents('./data.log', 'access_token: ' . $access_token . ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
// 先获取 access_token - 结束
// 第二步 : 根据临时授权码 Code 获取用户
$post_c = new DingTalkClient(DingTalkConstant::$CALL_TYPE_OAPI, DingTalkConstant::$METHOD_POST , DingTalkConstant::$FORMAT_JSON);
$unionid_request = new OapiSnsGetuserinfoBycodeRequest;
// 设置 Code 值,
$code = $_GET['code'];
if (empty($code)){
file_put_contents('./data.log', 'Code 为空'. ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
}
$unionid_request->setTmpAuthCode($code);
$unionid_responce_obj = $post_c->executeWithAccessKey($unionid_request, "https://oapi.dingtalk.com/sns/getuserinfo_bycode","$AppKey","$AppSecret");
if (!empty($unionid_responce_obj)){
$unionid_responce_string = json_encode($unionid_responce_obj);
file_put_contents('./data.log', 'unionid_responce_string: ' . $unionid_responce_string . ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
} else {
file_put_contents('./data.log', 'unionid 获取失败'. ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
}
// 根据临时授权码 Code 获取用户 unionid - 结束
// 第三步 : 根据 unionid 获取 用户 userid - 开始
$userid_request = new OapiUserGetbyunionidRequest;
$unionid = $unionid_responce_obj->user_info->unionid;
$userid_request->setUnionid($unionid);
$userid_response_obj = $post_c->execute($userid_request, $access_token, "https://oapi.dingtalk.com/topapi/user/getbyunionid");
if (!empty($userid_response_obj)){
$userid_responce_string = json_encode($userid_response_obj);
file_put_contents('./data.log', 'userid_responce_string: ' . $userid_responce_string . ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
} else {
file_put_contents('./data.log', 'userid 获取失败'. ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
}
// 根据 unionid 获取 用户 userid - 结束
// 第四步 : 根据 userid 获取 用户信息 - 开始
$userinfo_request = new OapiV2UserGetRequest;
// 设置 uid
$userid = $userid_response_obj->result->userid;
$userinfo_request->setUserid($userid);
$userinfo_response_obj = $post_c->execute($userinfo_request, $access_token, "https://oapi.dingtalk.com/topapi/v2/user/get");
if (!empty($userinfo_response_obj)){
$userinfo_responce_string = json_encode($userinfo_response_obj);
file_put_contents('./data.log', 'userinfo_responce_string: ' . $userinfo_responce_string . ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
} else {
file_put_contents('./data.log', '用户信息 获取失败'. ' ' . date("Y-m-d H:i:s", time()) . "\n" . "\n", FILE_APPEND);
}
// 根据 userid 获取 用户信息 - 结束
var_dump($userinfo_response_obj);
?>