Flask项目对接钉钉第三方扫码登录


一、注册钉钉开放平台

通过https://oa.dingtalk.com/register_new.htm注册钉钉开放平台

注册成功后,可以自己创建个公司团队

二、创建扫码登录应用

官方文档如下:https://ding-doc.dingtalk.com/doc#/serverapi2/kymkv6

登录钉钉开发者后台>应用开发>移动接入应用>登录

创建完后会生成appIdappSecret

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微应用>创建应用

创建完后会生成AppKeyAppSecret

四、通过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&timestamp=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")) + "&timestamp=" + 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 手机号码
email 员工的电子邮箱
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")) + "&timestamp=" + 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


文章作者: 星凌映雪
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 星凌映雪 !
评论
 上一篇
Docker-Compose部署Flask+MySQL+OpenLDAP练习 Docker-Compose部署Flask+MySQL+OpenLDAP练习
目的:将Flask、MySQL、OpenLDAP分别装在三个容器中,使用docker-compose命令进行启动和关闭。 系统环境:Ubuntu20.04、Docker19.03.13、 Docker-Compose1.27.4 一、创建D
2021-02-24
下一篇 
MongoDB的下载、安装及使用 MongoDB的下载、安装及使用
一、下载下载地址:https://www.mongodb.com/try/download/community 选择要下载的版本,匹配对应的系统 二、安装选择next 选择next 选择custom 选择安装路径 选择next
  目录