[Python] 企业内部应用接入钉钉登录,端内免登录+浏览器授权登录

Python 为企业网站应用接入钉钉鉴权,实现钉钉客户端内自动免登授权,浏览器中手动钉钉授权登录两种逻辑。

操作步骤

  1. 企业内部获得 开发者权限,没有的话先申请。

  2. 访问 钉钉开放平台-应用开发 创建一个 企业内部应用-钉钉应用

  3. 打开应用详情页,获取 Client IDClient SecretCorpId 备用,获取方式如下图所示。

  4. 编写代码,搭建相应服务(见下方示例代码)


示例代码(以Flask作为后端):

- templates/auth.html
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>auth</title>
    <script src="https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.25/dingtalk.open.js"></script>
    <script>
        // 检查是否在钉钉环境中
        function isDingTalk() {
            return /DingTalk/.test(navigator.userAgent);
        }

        if (isDingTalk()) {
            dd.ready(function () {
                dd.runtime.permission.requestAuthCode({
                    corpId: "dingxxxxxxxxxx", // 企业id
                    onSuccess: function (info) {
                        console.log(info);
                        location.href = "/demo/oauth_redirect?code=" + info.code + "&url=" + location.href;
                    }
                });
            });
        } else {
            location.href = "/demo/oauth_redirect?url=" + location.href;
        }
    </script>
</head>
<body>
</body>
</html>
  • 需修改:corpId: "dingxxxxxxxxxx" 替换为真实的CorpId
  • 代码逻辑:若在钉钉端内,则借助钉钉免登码完成登录。反之,则跳转钉钉授权页面进行授权登录(授权页面重定向由后端控制,当然直接写在前端也可以)
- app.py
python 复制代码
# -*- coding: utf-8 -*-
# Author: 薄荷你玩
import glob
import html
import json
import os
import random
import re
import time
import traceback
from datetime import datetime
from typing import List, Union
from flask import Flask, request, jsonify, Response, render_template, make_response, session
from utils import dingtalk_api

app = Flask(__name__, static_folder='static')

# 设置一个密钥用于加密会话数据
app.secret_key = '123456'

@app.after_request
def add_cors_headers(response):
    response.headers['Access-Control-Allow-Origin'] = '*'  # 允许所有来源的跨域请求
    response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'  # 允许的 HTTP 方法
    response.headers['Access-Control-Allow-Headers'] = '*'  # 允许的请求头
    return response

@app.route("/demo")
def demo():
    if 'user' in session:
        return render_template('index.html', user=session['user'])
	    # <span style="float: right; display: flex;  align-items: center; gap: 5px;">你好,{{user.name}} <img src="{{user.avatar}}" width="25"/></span>
    return render_template('auth.html')

@app.route("/demo/oauth_redirect")
def demo_oauth_redirect():
    code = request.args.get("code")
    url = request.args.get("url")
    if not code:
        # 重定向到钉钉授权登录页
        redirect_uri = url.split("demo")[0] + "demo/oauth-web"
        client_id = "dingyyyyyyyyyy"  # Client ID
        return app.redirect(f"https://login.dingtalk.com/oauth2/auth?redirect_uri={redirect_uri}&response_type=code&client_id={client_id}&scope=openid&state={url}&prompt=consent")
    else:
        user_info = dingtalk_api.x_get_user_info_by_app_code(code)
        if user_info['success']:
            session['user'] = user_info['data']
            return app.redirect(url)
        else:
            return user_info["msg"]

@app.route("/demo/oauth-web")
def demo_oauth_web():
    """ 钉钉回调URL,配置到钉钉开发平台 """
    code = request.args.get("code")
    state = request.args.get("state")
    user_info = dingtalk_api.x_get_user_info_by_web_code(code)
    if user_info['success']:
        session['user'] = user_info['data']
        return app.redirect(state)
    return user_info["msg"]

@app.errorhandler(500)
def internal_server_error(error):
    # 获取完整的 traceback 信息
    traceback_info = traceback.format_exc()

    # 返回具体的错误内容和完整的 traceback
    response = result_map(500, False, str(error), traceback_info)
    return jsonify(response), 500

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000)
  • 需修改:client_id = "dingyyyyyyyyyy" 替换为真实的Client ID
- utils/dingtalk_api.py
python 复制代码
# -*- coding: utf-8 -*-
# Author: 薄荷你玩
# Date: 2025/04/07

import requests

DINGTALK_DOMAIN = "https://api.dingtalk.com"
CorpId = "dingxxxxxxxxxx"  # 企业ID
ClientId = "dingyyyyyyyyyy"  # Client ID
ClientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"  # Client Secret


def user_info(name, avatar, unionid):
    return {
        "success": True,
        "data": {
            "name": name,
            "avatar": avatar,
            "unionid": unionid
        }
    }


def get_user_token_by_web_code(code):
    """
    获取用户Token--access_token,根据web端钉钉授权code
    :param code:
    :return:
    """
    url = DINGTALK_DOMAIN + f"/v1.0/oauth2/userAccessToken"
    headers = {
        "Content-Type": "application/json"
    }
    data = {
        "clientId": ClientId,
        "clientSecret": ClientSecret,
        "code": code,
        "refreshToken": "",
        "grantType": "authorization_code"
    }

    response = requests.post(url, json=data, headers=headers)
    res = response.json()
    print(res)
    return res


def get_user_info_by_access_token(access_token):
    """
    获取用户通讯录个人信息
    :param access_token:
    :return:
    """
    url = DINGTALK_DOMAIN + f"/v1.0/contact/users/me"
    headers = {
        "Content-Type": "application/json",
        "x-acs-dingtalk-access-token": access_token
    }
    response = requests.get(url, headers=headers)
    res = response.json()
    print(res)
    return res


def x_get_user_info_by_web_code(code):
    res = get_user_token_by_web_code(code)
    if "accessToken" in res.keys():
        res = get_user_info_by_access_token(res["accessToken"])
        if "nick" in res.keys():
            return user_info(name=res['nick'], avatar=res['avatarUrl'], unionid=res['unionId'])
    return {"success": False, "msg": res}


# 钉钉企业内部免登
def get_access_token():
    url = DINGTALK_DOMAIN + f"/v1.0/oauth2/{CorpId}/token"
    headers = {
        "Content-Type": "application/json"
    }
    data = {
        "client_id": ClientId,
        "client_secret": ClientSecret,
        "grant_type": "client_credentials"
    }

    response = requests.post(url, json=data, headers=headers)
    res = response.json()
    print(res)
    return res


def get_user_id_by_code(access_token, code):
    """通过免登码获取用户userid(v2)"""
    url = f"https://oapi.dingtalk.com/topapi/v2/user/getuserinfo?access_token={access_token}"
    headers = {
        "Content-Type": "application/json"
    }
    data = {
        "code": code
    }

    response = requests.post(url, json=data, headers=headers)
    res = response.json()
    print(res)
    return res


def get_user_info_by_user_id(access_token, userid):
    """通过免登码获取用户userid(v2)"""
    url = f"https://oapi.dingtalk.com/topapi/v2/user/get?access_token={access_token}"
    headers = {
        "Content-Type": "application/json"
    }
    data = {
        "userid": userid
    }
    response = requests.post(url, json=data, headers=headers)
    res = response.json()
    print(res)
    return res


def x_get_user_info_by_app_code(code):
    access_token = get_access_token()['access_token']
    res = get_user_id_by_code(access_token, code)
    if "result" in res.keys():
        user_id = res["result"]["userid"]
        res = get_user_info_by_user_id(access_token, user_id)
        if "result" in res.keys():
            return user_info(name=res['result']['name'], avatar=res['result']['avatar'],
                             unionid=res['result']['unionid'])
    return {"success": False, "msg": res}


if __name__ == '__main__':
    # res = x_get_user_info_by_app_code("{钉钉端内-免登码}")
    res = x_get_user_info_by_web_code("{钉钉web授权码}")
    print(res)
  • 需修改:

    python 复制代码
    CorpId = "dingxxxxxxxxxx"  # 企业ID
    ClientId = "dingyyyyyyyyyy"  # Client ID
    ClientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"  # Client Secret

    替换为真实的ID或秘钥


  1. 配置回调域名,如下图所示,填写用户授权后的回调地址(如:http://192.168.2.1:5000/demo/oauth-web),实际使用中换成正式的服务域名。
  2. 配置完成后,钉钉内访问 /demo (如:http://192.168.2.1:5000/demo)即可自动登录(获取姓名和头像等信息);浏览器访问会自动跳转钉钉授权登录页面,授权后完成登录。
相关推荐
xiangyun6129 分钟前
【408数据结构 08】队列:循环队列判空判满,408年年考,一次讲清
c语言·开发语言·数据结构·c++·算法
xiangyun6141 分钟前
【408数据结构 06】双链表、循环链表、静态链表
c语言·开发语言·数据结构·c++·算法
OKkankan2 小时前
LangChain 能力详解!:输出解析、RAG、向量数据库与 Retriever 检索器
数据结构·python·langchain·ai应用
白远山2 小时前
家政服务家政社区派单实战:调度系统架构设计与实现指南
java·开发语言·小程序·架构·需求分析
山哥ol3 小时前
【Geany 环境配置与中文乱码解决参考】
python
DevRay3 小时前
Java 21虚拟线程深度实战:告别线程池焦虑,百万并发吞吐量直接翻倍(原理+代码+避坑)
java·开发语言
free-elcmacom3 小时前
C++学习<1>程序分区
开发语言·c++
会飞锦鲤3 小时前
基于 Mask R-CNN 的药片缺陷检测系统
人工智能·pytorch·python·神经网络·resnet-50
醉颜凉3 小时前
Java 必看:如何彻底避免 HashMap 多线程死循环问题?
java·开发语言
默 语3 小时前
Java新手入门:从零开始安装JDK并配置环境变量
java·开发语言·python·mysql·group by·1024程序员节·数据去重