钉钉配置事件订阅(Python)

news2024/11/18 7:51:05

钉钉配置事件订阅

0.需求分析

需要实现钉钉企业通讯录同步至企业微信通讯录,这就需要用到钉钉的事件与回调

1.配置应用

登陆开放平台

https://open-dev.dingtalk.com/

去企业内部开发里面,先创建个应用,后面都借用这个应用来调接口

在这里插入图片描述

创建完成应用后进入应用,找到下面红框内的数据,后面会用到

在这里插入图片描述
进入应用中的事件与回调,自动生成aes_keytoken,然后保存就好这两个数据

在这里插入图片描述

2.服务开发

2.1请求验证

配置上面1.配置应用中的事件订阅下的请求网址如http://yourserver/api/callback时,需要把这个网址的接口开发好。钉钉会发送一个类似下面这样的请求:

一个POST类型的http请求,携带了部分url参数和json的加密参数

请求的路径参数包括signaturemsg_signaturetimestampnonce,请求体只有一个encryptjson

curl -X 'POST' \
  'http://yourserver:80/api/callback?signature=369beedea8d1c8d1ad18936e827d29d0c8415baf&msg_signature=369beedea8d1c8d1ad18936e827d29d0c8415baf&timestamp=1660634610203&nonce=kBms4hUF' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "encrypt": "4Q4JHq88OCR3P+8v2mcFHLT6dmaaYAckaUBVk1spJnCx7u9raGZVAxVUIuQg3loL8LjIQj+5YC3+HJcehTsJXu1qMOv5TKdb4+koO55g8WCYZP/vebg2RZQC2gBlN2zv"
}'

钉钉服务器会向配置好的接口服务中发送请求,用来验证双方通信的真实性,接口服务端可通过解密encrypt中的数据来验证是否是来自钉钉服务器,而返回的加密的success字符串能让钉钉服务器验证是否是来自用户,属于双向验证

2.2接口开发

2.2.1技术选型

对于创建接口服务这种需求,钉钉开放官网文档有些示例,比如Java等,都是可以实现的,因为需求很简单,故采用胶水语言Python进行开发

Python中想做接口服务,有两种选择,一是Flask轻量化接口服务,二是Django(Python Web框架)。经过简单比较后,选择Flask轻量化接口服务

主要代码如下:

DingDingCallback

# !/usr/bin/python3
# encoding:utf-8
'''
dingding通讯录同步至企业微信
采用Flask写一个实时监听的接口
收到钉钉的通讯录变更请求后,修改请求中的数据直接请求企业微信通讯录相关的API
'''
import flask
import json
import DingTalkCrypto

# 钉钉事件订阅aeskey
aes_key = "xxxxxxxxxxxxxT329ssbVn5Bo"
# 钉钉事件订阅token
token = "xxxxxxxxxxxIgBpKsBE"
# 钉钉appkey
app_key = "xxxxxxxxxxxxxxx"


# 实例化api,把当前这个python文件当作一个服务,__name__代表当前这个python文件
api = flask.Flask(__name__)


# 'index'是接口路径,methods不写,默认get请求
@api.route('/sync/test', methods=['get'])
# get方式访问
def index():
    ren = {'msg': '成功访问首页', 'msg_code': 200}
    print("测试接口成功请求!!!")
    # json.dumps 序列化时对中文默认使用的ascii编码.想输出中文需要指定ensure_ascii=False
    return json.dumps(ren, ensure_ascii=False)


# post方式访问(josn格式参数)
@api.route('/sync', methods=['post'])
def loginjosn():
    # 1.通过flask获取请求中的参数列表
    args = flask.request.args
    # 2.获取需要解密的参数
    signature = args.get('signature')  # 实际打印中signature和msg_signature是一样的
    msg_signature = args.get('msg_signature')
    timestamp = args.get('timestamp')
    nonce = args.get('nonce')
    # 3.获取post请求中的json数据
    encrypt = flask.request.json.get('encrypt')
    print(encrypt)

    # 4.调用加密解密工具类
    # DingCallbackCrypto3是官方提供的demo: https://github.com/open-dingtalk/dingtalk-callback-Crypto
    # 参数说明:
    '''
    1.token为应用中事件订阅下的签名token的数据
    2.aes_key为应用中事件订阅下的加密aes_key的数据
    3.app_key为应用的应用信息中的AppKey的数据
    注意:(这块需要具体问题具体分析,可以参考官方文档)
        1.开发者后台配置的订阅事件为应用级事件推送,此时app_key参数为应用的APP_KEY
        2.当使用HTTP回调注册接口方式接收钉钉推送的订阅事件时,是以企业为维度推送的,app_key为CorpId
    '''
    dingCrypto = DingTalkCrypto.DingTalkCrypto3(token, aes_key, app_key)
    # 5.解密回调事件
    decrypt_msg  = dingCrypto.getDecryptMsg(msg_signature, timestamp, nonce, encrypt)
    print(decrypt_msg)  # 打印结果: {"EventType":"check_url"}
    # 6.必须返回一个加密的success,让钉钉服务器进行确认
    success_map = dingCrypto.getEncryptedMap("success")
    return success_map


if __name__ == '__main__':
    api.run(port=6666, debug=True, host='0.0.0.0')  # 启动服务
    # debug=True,改了代码后,不用重启,它会自动重启
    # 'host='0.0.0.0'可以被所有请求访问到

DingTalkCrypto

需要注意的是:

众所周知,Crypto是个老坑包了,如果下载不了Crypto或者说用不了Crypto类库,那么需要下载pycryptodome

pycryptopycrytodomecrypto是一个东西,crypto在python上面的名字是pycrypto,它是一个第三方库,但是已经停止更新四年了(截止到2023-02-16),所以不建议安装这个库

下载pycryptodome库之前需要把之前安装的crypto库都卸载干净

pip uninstall pycrypto
pip uninstall cryptography
pip uninstall crypto
pip uninstall pycryptodome
pip install pycryptodome
# -*- coding: utf-8 -*-
# 依赖Crypto类库
# API说明
# getEncryptedMap 生成回调处理成功后success加密后返回给钉钉的json数据
# decrypt  用于从钉钉接收到回调请求后

import time
import io, base64, binascii, hashlib, string, struct
from random import choice
from Crypto.Cipher import AES

"""
@param token          钉钉开放平台上,开发者设置的token
@param encodingAesKey 钉钉开放台上,开发者设置的EncodingAESKey
@param corpId         企业自建应用-事件订阅, 使用appKey
                      企业自建应用-注册回调地址, 使用corpId
                      第三方企业应用, 使用suiteKey
"""
class DingTalkCrypto3:
    def __init__(self, token, encodingAesKey, key):
        self.encodingAesKey = encodingAesKey
        self.key = key
        self.token = token
        self.aesKey = base64.b64decode(self.encodingAesKey + '=')

    # 生成回调处理完成后的success加密数据
    def getEncryptedMap(self, content):
        encryptContent = self.encrypt(content)
        timeStamp = str(int(time.time()))
        nonce = self.generateRandomKey(16)
        sign = self.generateSignature(nonce, timeStamp, self.token,encryptContent)
        return {'msg_signature':sign,'encrypt':encryptContent,'timeStamp':timeStamp,'nonce':nonce}

    # 解密钉钉发送的数据
    def getDecryptMsg(self, msg_signature, timeStamp, nonce,  content):
        """
        解密
        :param content:
        :return:
        """
        sign = self.generateSignature(nonce, timeStamp, self.token,content)
        print(sign, msg_signature)
        if msg_signature != sign:
            raise ValueError('signature check error')

        content = base64.decodebytes(content.encode('UTF-8'))  # 钉钉返回的消息体

        iv = self.aesKey[:16]  # 初始向量
        aesDecode = AES.new(self.aesKey, AES.MODE_CBC, iv)
        decodeRes = aesDecode.decrypt(content)
        #pad = int(binascii.hexlify(decodeRes[-1]),16)
        pad = int(decodeRes[-1])
        if pad > 32:
            raise ValueError('Input is not padded or padding is corrupt')
        decodeRes = decodeRes[:-pad]
        l = struct.unpack('!i', decodeRes[16:20])[0]
        # 获取去除初始向量,四位msg长度以及尾部corpid
        nl = len(decodeRes)

        if decodeRes[(20+l):].decode() != self.key:
            raise ValueError('corpId 校验错误')
        return decodeRes[20:(20+l)].decode()

    def encrypt(self, content):
        """
        加密
        :param content:
        :return:
        """
        msg_len = self.length(content)
        content = ''.join([self.generateRandomKey(16) , msg_len.decode() , content , self.key])
        contentEncode = self.pks7encode(content)
        iv = self.aesKey[:16]
        aesEncode = AES.new(self.aesKey, AES.MODE_CBC, iv)
        aesEncrypt = aesEncode.encrypt(contentEncode.encode())
        return base64.encodebytes(aesEncrypt).decode('UTF-8')

    # 生成回调返回使用的签名值
    def generateSignature(self, nonce, timestamp, token, msg_encrypt):
        print(type(nonce), type(timestamp), type(token), type(msg_encrypt))
        v = msg_encrypt
        signList = ''.join(sorted([nonce, timestamp, token, v]))
        return hashlib.sha1(signList.encode()).hexdigest()

    def length(self, content):
        """
        将msg_len转为符合要求的四位字节长度
        :param content:
        :return:
        """
        l = len(content)
        return struct.pack('>l', l)

    def pks7encode(self, content):
        """
        安装 PKCS#7 标准填充字符串
        :param text: str
        :return: str
        """
        l = len(content)
        output = io.StringIO()
        val = 32 - (l % 32)
        for _ in range(val):
            output.write('%02x' % val)
        # print "pks7encode",content,"pks7encode", val, "pks7encode", output.getvalue()
        return content + binascii.unhexlify(output.getvalue()).decode()

    def pks7decode(self, content):
        nl = len(content)
        val = int(binascii.hexlify(content[-1]), 16)
        if val > 32:
            raise ValueError('Input is not padded or padding is corrupt')

        l = nl - val
        return content[:l]

    def generateRandomKey(self, size,
                          chars=string.ascii_letters + string.ascii_lowercase + string.ascii_uppercase + string.digits):
        """
        生成加密所需要的随机字符串
        :param size:
        :param chars:
        :return:
        """
        return ''.join(choice(chars) for i in range(size))


if __name__ == '__main__':
    dingCrypto = DingTalkCrypto3("xxxxxxxx", "xxxxxxxxxxxxxxxxxx", "xxxxxxxxxxx")
    success = dingCrypto.encrypt("success")
    print(success)

2.3接口验证

写好接口服务后放到有公网IP的地方运行(直接在python或者conda虚拟python环境下运行py文件、打包成可执行文件执行均可)

将服务接口复制到钉钉应用中的事件与回调中的请求网址中,点击保存即可

需要注意的是:

在开发过程中会测试事件订阅是否完成,高频率的刷新网页可能会导致事件订阅的aes_key和token被刷新或者没刷新但是因为缓存的问题,会有偏差,需要注意事件订阅中的两个值和接口服务代码中的两个值要一一对应起来,如果对应有误的话,保存的时候会报错

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/349878.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自定义分库分表组件(实现分库分表的组件)——java

系列文章目录 文章目录系列文章目录前言一、所需技术二、技术总结1. ThreadLocal2.HashMap三、实现1、定义路由注解自定义注解格式要求元注解: 用于描述注解的注解在程序使用(解析)注解:获取注解中定义的属性值小例子:注解定义一个简单的测试…

AI算法创新赛-人车目标检测竞赛总结03

团队简介AI0000032 团队成员均为从事计算机视觉领域的企业员工,热爱技术,勇于挑战,致力于更通用目标检测算法的研究与落地。团队由三人组成,队长何正海 主要负责整体方案设计与模型的量化工作,余洋主要负责模型训练与调…

YOLOV5输出的txt里面有什么猫腻(用于图像分类竞赛中提升图像信息密度)

背景概括: kaggle最近举办了一场医学乳腺癌检测的比赛(图像分类) 比赛官网地址 给的数据是dcm的专业的医学格式,自己通过DICOM库转为png后,发现该图像胸部不同的患者乳腺大小不一,简言之乳腺的CT有效图在…

MySQL性能调优与设计——MySQL中的索引

MySQL中的索引 InnoDB存储引擎支持以下几种常见索引:B树索引、全文索引、哈希索引,其中比较关键的是B树索引。 B树索引 InnoDB中的索引自然也是按照B树来组织的,B树的叶子节点用来存放数据。 聚集索引/聚簇索引 InnoDB中使用了聚集索引&…

LeetCode 141. 环形链表

原题链接 难度:easy\color{Green}{easy}easy 题目描述 给你一个链表的头节点 headheadhead ,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 nextnextnext 指针再次到达,则链表中存在环。 为了表示给定链表中的…

python编写webapi读取mdb数据使用json格式响应客户端请求

想做个自动应答机器人,通过webapi提供服务,原理:判断关键字,到数据库查询相关内容,以json格式反馈给客户端。 1、创建autoreply数据库,创建reply表,表中包含kename(短文本&#xff…

积水在线监测仪——积水点、易涝点水位监测设备

一、设备概述 积水在线监测仪是一款用于城市积水点、易涝点等场景的水位监测设备,设备采用电池供电,无需另外供电,安装方便,使用简单。可以时监测水点、易涝点水位情况,当水位数据超过阈值后触发告警上传,…

MybatisPlus实现分页效果并解决错误:cant found IPage for args!

前言 早就知道MybatisPlus对分页进行了处理,但是一直没有实战用过,用的是自己封装的一个分页组件,虽不说麻烦吧,但是也不是特别简单。 写起来还是比较复杂,但是最近这个组件有了点小小的bug,我决定是时候…

2023年进入互联网行业好找工作吗?

俗话说:选择大于努力。年后求职小高峰,大家在找工作的时候选择肯定也多了。 说真,不是人人都有铁饭,普通家庭的孩子想要在2023年进入互联网行业去找工作可能吗? 01 有一点大家要清楚,2022年是进入过一个寒…

【Linux】变量定义规则、shell 格式、空格注意事项汇总

文章目录1. 空格问题号用于赋值用于比较2. 变量2.1 变量命名的格式要求2.2、shell变量中的注意事项2.3、变量的使用方法2.4、变量的类型(1)自定义变量(2)环境变量(3)位置变量(4)预定…

【高并发-用户中心】读多写少的系统如何优化

本博客纯属个人总结,非原创。喜欢技术交流的,可关注博主,武汉有后端开发群,可支持内推,了解武汉行情等。 如何对读多写少的系统进行高并发优化? 比如:用户中心是一个读多写少的系统&#xff0…

Linux定时备份MySql数据库

一、创建文件 cd / mkdir mysqlbackup vi mysqlbackup.sh然后将下面的代码更改后复制上去即可。 #!/bin/bash mysqldump -uroot -ppassword database > /mysqlbackup/database__$(date %Y%m%d_%H%M%S).sqlpassword指的是MySql的密码,database指的是所要备份的…

【C++算法】dfs深度优先搜索(上) ——【全面深度剖析+经典例题展示】

💃🏼 本人简介:男 👶🏼 年龄:18 📕 ps:七八天没更新了欸,这几天刚搞完元宇宙,上午一直练🚗,下午背四级单词和刷题来着,还在忙一些学弟…

leaflet: 禁止拖拽、禁止zoom(双击、滚轮、键盘)、禁止tap(076)

第076个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中设定各种禁止状态,这里设置了禁止拖拽、禁止zoom(双击、滚轮、键盘)、禁止tap。 直接复制下面的 vue+leaflet源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共73行)相关A…

linux高级命令之多进程的使用

多进程的使用学习目标能够使用多进程完成多任务1 导入进程包#导入进程包import multiprocessing2. Process进程类的说明Process([group [, target [, name [, args [, kwargs]]]]])group:指定进程组,目前只能使用Nonetarget:执行的目标任务名…

电商导购CPS,京东联盟如何跟单实现用户和订单绑定

前言 大家好,我是小悟 做过自媒体的小伙伴都知道,不管是发图文还是发短视频,直播也好,可以带货。在你的内容里面挂上商品,你自己都不需要囤货,如果用户通过这个商品下单成交了,自媒体平台就会…

【刷题笔记】--搜索二维矩阵 II

题目: 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性: 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 示例 1: 输入:matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16…

设计师都在看的全球设计网站,你居然还不知道!

设计师需要拥有无限的创意和熟练的技巧,并且对行业的前景和客户的心理有一定的了解。要能达到“陌生化”之前,肯定是有知识储备,专业能力的前提要求,以及创新能力。 今天为大家整理了多个优秀全球设计网站,这些博客内…

大家都在聊的自动化办公到底是什么?

自动化办公无非是excel、ppt、word、邮件、文件处理、数据分析处理、爬虫这些,下面就详细介绍一下!文章最后分享了很不错的python学习教程,适合零基础初学的小伙伴,希望可以对你有所帮助!! excel自动化 我…

linux基本功系列之grep命令

文章目录前言一. grep命令介绍二. 语法格式及常用选项三. 参考案例3.1 搜索文件中以root开头的文件3.2 搜索文件中出现的root3.3 搜索除了匹配行之外的行3.4 匹配的部分使用颜色显示3.5 只输出文件中匹配到的地方3.6 输出包含匹配字符串的行,并显示所在的行数3.7 统…