一、注册钉钉开放平台
通过https://oa.dingtalk.com/register_new.htm注册钉钉开放平台
注册成功后,可以自己创建个公司团队
二、创建扫码登录应用
官方文档如下:https://ding-doc.dingtalk.com/doc#/serverapi2/kymkv6
登录钉钉开发者后台>应用开发>移动接入应用>登录
创建完后会生成appId
,appSecret
1、构造扫码登录页面
1.1、方式一 使用钉钉提供的扫码登录页面
在企业Web系统里,用户点击使用钉钉扫描登录,第三方Web系统跳转到如下地址:
https://oapi.dingtalk.com/connect/qrconnect?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI
url里的参数需要换成第三方Web系统对应的参数。在钉钉用户扫码登录并确认后,会302到你指定的redirect_uri,并向url参数中追加临时授权码code及state两个参数。
注意事项:
参数redirect_uri=REDIRECT_URI涉及的域名,需和创建扫码登录应用授权时填写的回调域名一致,否则会提示无权限访问。
例子:
<a href="https://oapi.dingtalk.com/connect/qrconnect?appid=xxx&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=http://127.0.0.1:5002/datarun/dingding_back">钉钉扫码登录</a>
1.2、方式二 支持网站将钉钉登录二维码内嵌到自己页面中
用户使用钉钉扫码登录后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=appid&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"
});
参数说明:
参数 | 说明 |
---|---|
goto | goto参数结构:https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI, 并且要将goto参数urlencode编码。 |
style | 渲染二维码的区域的样式,可以自定义去除背景颜色和边框 |
width | 表示显示二维码的区域的宽。width和height不代表二维码的大小,二维码大小是固定的210px*210px |
height | 表示显示二维码的区域的高。width和height不代表二维码的大小,二维码大小是固定的210px*210px。 |
您引入的js会在获取用户扫描之后将获取的loginTmpCode通过window.parent.postMessage(loginTmpCode,’*’);返回给您的网站。
您可以通过以下代码获取这个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);
}
通过JS获取到loginTmpCode后,需要由你构造并跳转到如下链接:
https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI&loginTmpCode=loginTmpCode
此链接处理成功后,会302跳转到goto参数指定的redirect_uri,并向url参数中追加临时授权码code及state参数。
参数 | 必须 | 说明 |
---|---|---|
appid | 是 | 参看本文获取appId和appSecret,查看appId |
redirect_uri | 是 | 重定向地址(如果是第一种方式需要urlencode编码,如果是第二种方式则需要将JS goto参数整体urlencode编码,不要单独对redirect_uri编码),该地址使用域名需配置为appId对应的回调域名,回调域名是在**获取appId及appSecret时填写** |
state | 是 | 用于防止重放攻击,开发者可以根据此信息来判断redirect_uri只能执行一次来避免重放攻击 |
response_type | 是 | 固定为code |
scope | 是 | 固定为snsapi_login |
loginTmpCode | 是 | 通过js获取到的loginTmpCode |
完整html代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>钉钉扫码登录</title>
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
</head>
<body>
<div>
<div id="login_container" style="text-align: center; margin-top: 100px"></div>
</div>
<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=appid&response_type=code&scope=snsapi_login&state=STATE&redirect_uri='+url)
*/
var url = encodeURIComponent('http://127.0.0.1:6001/progress/dingding_back');
var goto = encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoadxp9stueg3rk3zir&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: encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoadxp9stueg3rk3zir&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=' + url), //请参考注释里的方式*/
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;
window.location.href="https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=xxx&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI&loginTmpCode="+loginTmpCode
//获取到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);
}
</script>
</body>
</html>
三、创建h5微应用
登录钉钉开发者后台>应用开发>企业内部开发>H5微应用>创建应用
创建完后会生成AppKey
,AppSecret
四、通过AppKey、AppSecret得到access_token
官方文档如下:https://ding-doc.dingtalk.com/doc#/serverapi2/eev437
【注意】正常情况下access_token有效期为7200秒,有效期内重复获取返回相同结果,并自动续期。
完整代码如下:
def get_access_token():
"""
获取access_token
:return: access_token
"""
appkey = 'xxx'
appsecret = 'xxx'
token_url = "https://oapi.dingtalk.com/gettoken?appkey=%s&appsecret=%s" % (appkey, appsecret)
res = requests.get(token_url)
res_dict = json.loads(res.text)
access_token = res_dict.get('access_token')
print("access_token为{}".format(access_token))
return access_token
五、通过临时授权码得到unionid
官方文档如下:https://ding-doc.dingtalk.com/doc#/serverapi2/kymkv6
unionid是用户在当前开放应用所属企业的唯一标识
通过临时授权码Code获取用户信息,临时授权码只能使用一次。
请求方式:POST(HTTPS)
请求地址:https://oapi.dingtalk.com/sns/getuserinfo_bycode?accessKey=xxx×tamp=xxx&signature=xxx
请求包结构体:
{
"tmp_auth_code": "23152698ea18304da4d0ce1xxxxx"
}
URL签名参数说明:
参数 | 说明 |
---|---|
accessKey | 应用的appId,参见本篇文档获取appId及appSerect章节 |
timestamp | 当前时间戳,单位是毫秒 |
signature | 通过appSecret计算出来的签名值,签名计算方法 |
参数说明:
参数 | 类型 | 必须 | 说明 |
---|---|---|---|
tmp_auth_code | String | 是 | 用户授权的临时授权码code,只能使用一次;在前面步骤中跳转到redirect_uri时会追加code参数 |
完整代码如下:
# 获取code
# 钉钉登录扫码的时候会生成一个code
code = request.args.get('code')
def get_user_unionid(code):
"""
通过临时授权码得到unionid
:param code: 临时授权码
:return: unionid
"""
t = time.time()
# 时间戳
timestamp = str((int(round(t * 1000))))
# 替换成自己的appSecret
appSecret = 'xI6fssqEDhOFqi5pkWznNT0SFOKn0bJY2DxXNw4aKozjwdngPYawgVGa-XRn0uH3'
# 构造签名
signature = base64.b64encode(
hmac.new(appSecret.encode('utf-8'), timestamp.encode('utf-8'), digestmod=sha256).digest())
# 请求接口,换取钉钉用户名
payload = {'tmp_auth_code': code}
headers = {'Content-Type': 'application/json'}
res = requests.post('https://oapi.dingtalk.com/sns/getuserinfo_bycode?signature=' + urllib.parse.quote(
signature.decode("utf-8")) + "×tamp=" + timestamp + "&accessKey=dingoadxp9stueg3rk3zir",
data=json.dumps(payload), headers=headers) # accessKey替换成自己的appid
res_dict = json.loads(res.text)
unionid = res_dict.get('user_info').get('unionid')
print("unionid为{}".format(unionid))
return unionid
参考链接:
六、通过unionid、access_token获取userid
官方文档如下:https://ding-doc.dingtalk.com/doc#/serverapi2/ege851#602f4b15
调试工具:在线调试
请求方式:GET(HTTPS)
请求地址:https://oapi.dingtalk.com/user/getUseridByUnionid?access_token=ACCESS_TOKEN&unionid=xxx
参数说明
参数 | 类型 | 必须 | 说明 |
---|---|---|---|
access_token | String | 是 | 调用接口凭证 |
unionid | String | 是 | 员工在当前开发者企业账号范围内的唯一标识,系统生成,固定值,不会改变 |
完整代码如下:
def get_user_userid(access_token, unionid):
"""
获取userid
:param access_token:
:param unionid:
:return: userid
"""
url = 'https://oapi.dingtalk.com/user/getUseridByUnionid?access_token={access_token}&unionid={unionid}'.format(
access_token=access_token, unionid=unionid)
res2 = requests.get(url)
res2_dict = json.loads(res2.text)
userid = res2_dict.get('userid')
return userid
七、根据userid、access_token获取用户信息
官方文档如下:https://ding-doc.dingtalk.com/doc#/serverapi2/ege851/AaRQe
如果你想获取同事的信息,必须把同事拉进你自己创建的公司群中。
如果您想调用通讯录接口并同时获取员工手机号,请先参考通讯录权限说明,设置下通讯录接口权限和手机号等敏感字段权限
调试工具:在线调试
请求方式:GET(HTTPS)
请求地址:https://oapi.dingtalk.com/user/get?access_token=ACCESS_TOKEN&userid=zhangsan
参数说明:
参数 | 类型 | 必须 | 说明 |
---|---|---|---|
access_token | String | 是 | 调用接口凭证 |
userid | String | 是 | 员工id |
lang | String | 否 | 通讯录语言(默认zh_CN,未来会支持en_US) |
完整代码如下:
def get_user(access_token, userid):
"""
获取用户详情
:param access_token:
:param userid:
:return: res_dict:包含用户字典信息
"""
user_url = "https://oapi.dingtalk.com/user/get?access_token=%s&userid=%s" % (access_token, userid)
res = requests.get(user_url)
res_dict = json.loads(res.text)
return res_dict
返回结果:
{
"errcode": 0,
"unionid": "PiiiPyQqBNBii0HnCJ3zljcxxxxxx",
"remark": "remark",
"userid": "zhangsan",
"isLeaderInDepts": "{1:false}",
"isBoss": false,
"hiredDate": 1520265600000,
"isSenior": false,
"tel": "xxx-xxxxxxxx",
"department": [1,2],
"workPlace": "place",
"email": "test@xxx.com",
"orderInDepts": "{1:71738366882504}",
"mobile": "1xxxxxxxxxx",
"errmsg": "ok",
"active": false,
"avatar": "xxx",
"isAdmin": false,
"isHide": false,
"jobnumber": "001",
"name": "张三",
"extattr": {},
"stateCode": "86",
"position": "manager",
"roles": [
{
"id": 149507744,
"name": "总监",
"groupName": "职务"
}
]
}
参数 | 说明 |
---|---|
errcode | 返回码 |
errmsg | 对返回码的文本描述内容 |
userid | 员工在当前企业内的唯一标识,也称staffId。可由企业在创建时指定,并代表一定含义比如工号,创建后不可修改 |
unionid | 员工在当前开发者企业账号范围内的唯一标识,系统生成,固定值,不会改变 |
name | 员工名字 |
tel | 分机号(仅限企业内部开发调用) |
workPlace | 办公地点 |
remark | 备注 |
mobile | 手机号码 |
员工的电子邮箱 | |
orgEmail | 员工的企业邮箱,如果员工已经开通了企业邮箱,接口会返回,否则不会返回 |
active | 是否已经激活,true表示已激活,false表示未激活 |
orderInDepts | 在对应的部门中的排序,Map结构的json字符串,key是部门的id,value是人员在这个部门的排序值 |
isAdmin | 是否为企业的管理员,true表示是,false表示不是 |
isBoss | 是否为企业的老板,true表示是,false表示不是 |
isLeaderInDepts | 在对应的部门中是否为主管:Map结构的json字符串,key是部门的id,value是人员在这个部门中是否为主管,true表示是,false表示不是 |
isHide | 是否号码隐藏,true表示隐藏,false表示不隐藏 |
department | 成员所属部门id列表 |
position | 职位信息 |
avatar | 头像url |
hiredDate | 入职时间。Unix时间戳 (在OA后台通讯录中的员工基础信息中维护过入职时间才会返回) |
jobnumber | 员工工号 |
extattr | 扩展属性,可以设置多种属性(手机上最多显示10个扩展属性,具体显示哪些属性,请到OA管理后台->设置->通讯录信息设置和OA管理后台->设置->手机端显示信息设置)。该字段的值支持链接类型填写,同时链接支持变量通配符自动替换,目前支持通配符有:userid,corpid。示例: 工位地址](http://www.dingtalk.com/?userid=#userid#&corpid=#corpid#)) |
isSenior | 是否是高管 |
stateCode | 国家地区码 |
roles | 用户所在角色列表 |
└ id | 角色id |
└ name | 角色名称 |
└ groupName | 角色组名称 |
realAuthed | 是否实名认证 |
八、完整代码
1、方式一 前端部分
<a href="https://oapi.dingtalk.com/connect/qrconnect?appid=xxx&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=http://127.0.0.1:5002/datarun/dingding_back">钉钉扫码登录</a>
2、方式二 前端部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>钉钉扫码登录</title>
<script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
</head>
<body>
<div>
<div id="login_container" style="text-align: center; margin-top: 100px"></div>
</div>
<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=appid&response_type=code&scope=snsapi_login&state=STATE&redirect_uri='+url)
*/
var url = encodeURIComponent('http://127.0.0.1:6001/progress/dingding_back');
var goto = encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoadxp9stueg3rk3zir&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: encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoadxp9stueg3rk3zir&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=' + url), //请参考注释里的方式*/
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;
window.location.href="https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=xxx&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI&loginTmpCode="+loginTmpCode
//获取到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);
}
</script>
</body>
</html>
3、Flask后端部分
@app.route('/dingding_back')
def dingding_back():
"""
# 钉钉回调方法
:return:
"""
# 获取code
code = request.args.get('code')
# 获取unionid
unionid = get_user_unionid(code)
# 获取access_token,有效期2小时
access_token = get_access_token()
# 获取userid
userid = get_user_userid(access_token, unionid)
# 获取包含用户字典信息
user_dict = get_user(access_token, userid)
name = user_dict.get('name')
session[config.FRONT_USER_NAME] = name
# mobile = user_dict.get('mobile')
print("name为{}".format(name))
return redirect(url_for('progress.projects', applicant=name))
# return name
def get_access_token():
"""
获取access_token
:return: access_token
"""
appkey = 'dingyuxsnqbneg8jcqoe'
appsecret = 'UcSPtkMpAVKssWeYmhcmWdxu3R9TyGAuYjmXXT3NC8UUxmaDHaKZbZWTc9MxY-8p'
token_url = "https://oapi.dingtalk.com/gettoken?appkey=%s&appsecret=%s" % (appkey, appsecret)
res = requests.get(token_url)
res_dict = json.loads(res.text)
access_token = res_dict.get('access_token')
return access_token
def get_user_unionid(code):
"""
通过临时授权码得到unionid
:param code: 临时授权码
:return: unionid
"""
t = time.time()
# 时间戳
timestamp = str((int(round(t * 1000))))
# 替换成自己的appSecret
appSecret = 'xI6fssqEDhOFqi5pkWznNT0SFOKn0bJY2DxXNw4aKozjwdngPYawgVGa-XRn0uH3'
# 构造签名
signature = base64.b64encode(
hmac.new(appSecret.encode('utf-8'), timestamp.encode('utf-8'), digestmod=sha256).digest())
# 请求接口,换取钉钉用户名
payload = {'tmp_auth_code': code}
headers = {'Content-Type': 'application/json'}
res = requests.post('https://oapi.dingtalk.com/sns/getuserinfo_bycode?signature=' + urllib.parse.quote(
signature.decode("utf-8")) + "×tamp=" + timestamp + "&accessKey=dingoadxp9stueg3rk3zir",
data=json.dumps(payload), headers=headers) # accessKey替换成自己的appid
res_dict = json.loads(res.text)
unionid = res_dict.get('user_info').get('unionid')
print("unionid为{}".format(unionid))
return unionid
def get_user_userid(access_token, unionid):
"""
获取userid
:param access_token:
:param unionid:
:return: userid
"""
url = 'https://oapi.dingtalk.com/user/getUseridByUnionid?access_token={access_token}&unionid={unionid}'.format(
access_token=access_token, unionid=unionid)
res2 = requests.get(url)
res2_dict = json.loads(res2.text)
userid = res2_dict.get('userid')
return userid
def get_user(access_token, userid):
"""
获取用户详情
:param access_token:
:param userid:
:return: res_dict:包含用户字典信息
"""
user_url = "https://oapi.dingtalk.com/user/get?access_token=%s&userid=%s" % (access_token, userid)
res = requests.get(user_url)
res_dict = json.loads(res.text)
print("res_dict为{}".format(res_dict))
return res_dict
常见问题:
1、访问ip不在白名单之中,request ip=xxx.xxx.xxx.xxx
解决方法:登录自己的钉钉企业管理后台 —> 应用开发 —> 选择自己的项目 —> 开发管理 —> 增加服务器出口IP