2024 年最新使用 Python 部署腾讯云服务器搭建企业微信机器人应用详细教程

企业微信机器人是一种可以在企业微信工作群中执行特定任务的自动化工具。它具备丰富的功能,可以帮助企业提高团队协作效率,简化工作流程,并为员工提供更好的工作体验。

获取企业 ID 信息

企业信息页面链接地址:https://work.weixin.qq.com/wework_admin/frame#profile

在这里插入图片描述

创建企业微信机器人

后台应用管理页面链接地址:https://work.weixin.qq.com/wework_admin/frame#apps

点击创建应用

在这里插入图片描述

填写配置机器人应用信息

在这里插入图片描述

机器人功能配置

在这里插入图片描述

配置密钥 Secret

获取机器人应用 agentId 和 Secret 信息

在这里插入图片描述

发送 Secret 到企业微信查看详细 Secret

在这里插入图片描述

创建 config.ini 配置文件配置企业微信机器人应用的配置信息

corpId=【企业 ID】
corpSecret=【应用密钥】

获取 access_token

API 开发文档:https://developer.work.weixin.qq.com/resource/devtool

获取 access_token 是调用企业微信API接口的第一步,相当于创建了一个登录凭证,其它的业务 API 接口,都需要依赖于 access_token 来鉴权调用者身份。

因此开发者,在使用业务接口前,要明确 access_token 的颁发来源,使用 正确的access_token。

请求方式: GETHTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET

权限说明

每个应用有独立的 secret,获取到的 access_token 只能本应用使用,所以每个应用的 access_token 应该分开来获取。

在这里插入图片描述

注意事项

开发者需要缓存access_token,用于后续接口的调用(注意:不能频繁调用gettoken接口,否则会受到频率拦截)。当access_token失效或过期时,需要重新获取。

access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时),有效期内重复获取返回相同结果,过期后获取会返回新的access_token。
由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
access_token至少保留512字节的存储空间。
企业微信可能会出于运营需要,提前使access_token失效,开发者应实现access_token失效时重新获取的逻辑。

返回结果

在这里插入图片描述

实现代码展示

import os  
import requests  # 从环境变量中读取 corpid 和 corpsecret  
corpid = os.environ.get('corpid')  
corpsecret = os.environ.get('corpsecret')  # 发送 GET 请求获取 access_token  
url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}"  
response = requests.get(url)  # 检查响应状态  
if response.status_code == 200:  data = response.json()  if data.get('errcode') == 0:  # 将 access_token 写入文件  with open("access_token.txt", "w") as file:  file.write(data['access_token'])  else:  # 输出错误信息  print(data)  
else:  # 输出请求失败信息  print(f"Request failed with status code {response.status_code}")

接收消息服务器配置

为了能够让自建应用和企业微信进行双向通信,企业可以在应用的管理后台开启接收消息模式。开启接收消息模式的企业,需要提供可用的接收消息服务器 URL(建议使用 https)。

开启接收消息模式后,用户在应用里发送的消息会推送给企业后台。还可配置地理位置上报等事件消息,当事件触发时企业微信会把相应的数据推送到企业的后台。企业后台接收到消息后,可在回复该消息请求的响应包里带上新消息,企业微信会将该被动回复消息推送给用户。

配置消息服务器配置

在这里插入图片描述

企业微信在推送消息给企业时,会对消息内容做 AES 加密,以 XML 格式 POST 到企业应用的 URL 上。企业在被动响应时,也需要对数据加密,以 XML 格式返回给企业微信。

配置说明展示

属性说明
URL企业后台接收企业微信推送请求的访问协议和地址 支持 http 或 https 协议(为了提高安全性,建议使用https)
Token企业任意填写(用于生成签名)
EncodingAESKey用于消息体的加密

加解密方案说明

文档地址:https://developer.work.weixin.qq.com/document/path/90968

术语说明

msg_signature: 消息签名,用于验证请求是否来自企业微信(防止攻击者伪造)。

EncodingAESKey:用于消息体的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,是AESKey的Base64编码。解码后即为32字节长的AESKey

AESKey=Base64_Decode(EncodingAESKey +=)

AESKey:AES 算法的密钥,长度为 32 字节。AES 采用 CBC 模式,数据采用 PKCS#7 填充至 32 字节的倍数;IV 初始向量大小为 16 字节,取 AESKey 前 16 字节,详见:http://tools.ietf.org/html/rfc2315

msg:为消息体明文,格式为XML

msg_encrypt:明文消息msg加密处理后的Base64编码。

初始化加解密类

WXBizMsgCrypt wxcpt(sToken,sEncodingAESKey,sReceiveId);

要求传参数sToken,sEncodingAESKey,sReceiveId。

sToken,sEncodingAESKey即设置接收消息的参数章节所述配置的Token、EncodingAESKey。

验证 URL 函数

① 签名校验 ② 解密数据包,得到明文消息内容。

int VerifyURL(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sEchoStr, string &sReplyEchoStr);

在这里插入图片描述

解密函数

① 签名校验 ② 解密数据包,得到明文消息结构体。

int DecryptMsg(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sPostData, string &sMsg);

在这里插入图片描述

加密函数

① 加密明文消息结构体 ② 生成签名 ③ 构造被动响应包

int EncryptMsg(const string &sReplyMsg, const string &sTimeStamp, const string &sNonce, string &sEncryptMsg);

在这里插入图片描述

项目结构目录

在这里插入图片描述

配置文件 config.ini

[wechat]
AgentId = 企业微信应用ID
Secret = 企业微信应用Secret
CorpID = 企业微信ID
Token = 企业微信应用Token
EncodingAESKey = 企业微信应用EncodingAESKey[dev]
debug = false

requirements.txt

blinker==1.6.3
certifi==2023.7.22
charset-normalizer==3.3.1
click==8.1.7
colorama==0.4.6
configobj==5.0.8
Flask==3.0.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
lxml==4.9.3
MarkupSafe==2.1.3
Naked==0.1.32
pycryptodomex==3.19.0
PyYAML==6.0.1
requests==2.31.0
shellescape==3.8.1
six==1.16.0
urllib3==2.0.7
Werkzeug==3.0.0

加解密算法算法库

鉴于加解密算法相对复杂,企业微信提供了算法库。

./callback/ierror.py

WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseXml_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateCorpid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011

./callback/WXBizMsgCrypt3.py

import logging
import base64
import random
import hashlib
import time
import struct
from Cryptodome.Cipher import AES
import xml.etree.cElementTree as ET
import socketfrom callback import ierror"""
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
"""class FormatException(Exception):passdef throw_exception(message, exception_class=FormatException):"""my define raise exception function"""raise exception_class(message)class SHA1:"""计算企业微信的消息签名接口"""def getSHA1(self, token, timestamp, nonce, encrypt):"""用SHA1算法生成安全签名@param token:  票据@param timestamp: 时间戳@param encrypt: 密文@param nonce: 随机字符串@return: 安全签名"""try:sortlist = [token, timestamp, nonce, encrypt]sortlist.sort()sha = hashlib.sha1()sha.update("".join(sortlist).encode())return ierror.WXBizMsgCrypt_OK, sha.hexdigest()except Exception as e:logger = logging.getLogger()logger.error(e)return ierror.WXBizMsgCrypt_ComputeSignature_Error, Noneclass XMLParse:"""提供提取消息格式中的密文及生成回复消息格式的接口"""# xml消息模板AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""def extract(self, xmltext):"""提取出xml数据包中的加密消息@param xmltext: 待提取的xml字符串@return: 提取出的加密消息字符串"""try:xml_tree = ET.fromstring(xmltext)encrypt = xml_tree.find("Encrypt")return ierror.WXBizMsgCrypt_OK, encrypt.textexcept Exception as e:logger = logging.getLogger()logger.error(e)return ierror.WXBizMsgCrypt_ParseXml_Error, Nonedef generate(self, encrypt, signature, timestamp, nonce):"""生成xml消息@param encrypt: 加密后的消息密文@param signature: 安全签名@param timestamp: 时间戳@param nonce: 随机字符串@return: 生成的xml字符串"""resp_dict = {'msg_encrypt': encrypt,'msg_signaturet': signature,'timestamp': timestamp,'nonce': nonce,}resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dictreturn resp_xmlclass PKCS7Encoder():"""提供基于PKCS7算法的加解密接口"""block_size = 32def encode(self, text):""" 对需要加密的明文进行填充补位@param text: 需要进行填充补位操作的明文@return: 补齐明文字符串"""text_length = len(text)# 计算需要填充的位数amount_to_pad = self.block_size - (text_length % self.block_size)if amount_to_pad == 0:amount_to_pad = self.block_size# 获得补位所用的字符pad = chr(amount_to_pad)return text + (pad * amount_to_pad).encode()def decode(self, decrypted):"""删除解密后明文的补位字符@param decrypted: 解密后的明文@return: 删除补位字符后的明文"""pad = ord(decrypted[-1])if pad < 1 or pad > 32:pad = 0return decrypted[:-pad]class Prpcrypt(object):"""提供接收和推送给企业微信消息的加解密接口"""def __init__(self, key):# self.key = base64.b64decode(key+"=")self.key = key# 设置加解密模式为AESCBC模式self.mode = AES.MODE_CBCdef encrypt(self, text, receiveid):"""对明文进行加密@param text: 需要加密的明文@return: 加密得到的字符串"""# 16位随机字符串添加到明文开头text = text.encode()text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode()# 使用自定义的填充方式对明文进行补位填充pkcs7 = PKCS7Encoder()text = pkcs7.encode(text)# 加密cryptor = AES.new(self.key, self.mode, self.key[:16])try:ciphertext = cryptor.encrypt(text)# 使用BASE64对加密后的字符串进行编码return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)except Exception as e:logger = logging.getLogger()logger.error(e)return ierror.WXBizMsgCrypt_EncryptAES_Error, Nonedef decrypt(self, text, receiveid):"""对解密后的明文进行补位删除@param text: 密文@return: 删除填充补位后的明文"""try:cryptor = AES.new(self.key, self.mode, self.key[:16])# 使用BASE64对密文进行解码,然后AES-CBC解密plain_text = cryptor.decrypt(base64.b64decode(text))except Exception as e:logger = logging.getLogger()logger.error(e)return ierror.WXBizMsgCrypt_DecryptAES_Error, Nonetry:pad = plain_text[-1]# 去掉补位字符串# pkcs7 = PKCS7Encoder()# plain_text = pkcs7.encode(plain_text)# 去除16位随机字符串content = plain_text[16:-pad]xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])xml_content = content[4: xml_len + 4]from_receiveid = content[xml_len + 4:]except Exception as e:logger = logging.getLogger()logger.error(e)return ierror.WXBizMsgCrypt_IllegalBuffer, Noneif from_receiveid.decode('utf8') != receiveid:return ierror.WXBizMsgCrypt_ValidateCorpid_Error, Nonereturn 0, xml_contentdef get_random_str(self):""" 随机生成16位字符串@return: 16位字符串"""return str(random.randint(1000000000000000, 9999999999999999)).encode()class WXBizMsgCrypt(object):# 构造函数def __init__(self, sToken, sEncodingAESKey, sReceiveId):try:self.key = base64.b64decode(sEncodingAESKey + "=")assert len(self.key) == 32except:throw_exception("[error]: EncodingAESKey unvalid !", FormatException)# return ierror.py.WXBizMsgCrypt_IllegalAesKey,Noneself.m_sToken = sTokenself.m_sReceiveId = sReceiveId# 验证URL# @param sMsgSignature: 签名串,对应URL参数的msg_signature# @param sTimeStamp: 时间戳,对应URL参数的timestamp# @param sNonce: 随机串,对应URL参数的nonce# @param sEchoStr: 随机串,对应URL参数的echostr# @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效# @return:成功0,失败返回对应的错误码def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):sha1 = SHA1()ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)if ret != 0:return ret, Noneif not signature == sMsgSignature:return ierror.WXBizMsgCrypt_ValidateSignature_Error, Nonepc = Prpcrypt(self.key)ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId)return ret, sReplyEchoStrdef EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):# 将企业回复用户的消息加密打包# @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串# @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间# @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce# sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,# return:成功0,sEncryptMsg,失败返回对应的错误码Nonepc = Prpcrypt(self.key)ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)encrypt = encrypt.decode('utf8')if ret != 0:return ret, Noneif timestamp is None:timestamp = str(int(time.time()))# 生成安全签名sha1 = SHA1()ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)if ret != 0:return ret, NonexmlParse = XMLParse()return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):# 检验消息的真实性,并且获取解密后的明文# @param sMsgSignature: 签名串,对应URL参数的msg_signature# @param sTimeStamp: 时间戳,对应URL参数的timestamp# @param sNonce: 随机串,对应URL参数的nonce# @param sPostData: 密文,对应POST请求的数据#  xml_content: 解密后的原文,当return返回0时有效# @return: 成功0,失败返回对应的错误码# 验证安全签名xmlParse = XMLParse()ret, encrypt = xmlParse.extract(sPostData)if ret != 0:return ret, Nonesha1 = SHA1()ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)if ret != 0:return ret, Noneif not signature == sMsgSignature:return ierror.WXBizMsgCrypt_ValidateSignature_Error, Nonepc = Prpcrypt(self.key)ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId)return ret, xml_content

原理介绍(可略)

from WXBizMsgCrypt import WXBizMsgCrypt
import xml.etree.cElementTree as ET
import sysif __name__ == "__main__":# 假设企业在企业微信后台上设置的参数如下sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo"sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt"sCorpID = "ww1436e0e65a779aee"'''------------使用示例一:验证回调URL---------------*企业开启回调模式时,企业号会向验证url发送一个get请求 假设点击验证时,企业收到类似请求:* GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D * HTTP/1.1 Host: qy.weixin.qq.com接收到该请求时,企业应	1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),这一步注意作URL解码。2.验证消息体签名的正确性 3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信第2,3步可以用企业微信提供的库函数VerifyURL来实现。'''wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID)# sVerifyMsgSig=HttpUtils.ParseUrl("msg_signature")# ret = wxcpt.VerifyAESKey()# print retsVerifyMsgSig = "012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb"# sVerifyTimeStamp=HttpUtils.ParseUrl("timestamp")sVerifyTimeStamp = "1476416373"# sVerifyNonce=HttpUitls.ParseUrl("nonce")sVerifyNonce = "47744683"# sVerifyEchoStr=HttpUtils.ParseUrl("echostr")sVerifyEchoStr = "fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA=="ret, sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr)if (ret != 0):print"ERR: VerifyURL ret: " + str(ret)sys.exit(1)# 验证URL成功,将sEchoStr返回给企业号# HttpUtils.SetResponse(sEchoStr)'''------------使用示例二:对用户回复的消息解密---------------用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档假设企业收到企业微信的回调消息如下:POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1Host: qy.weixin.qq.comContent-Length: 613<xml> <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt><AgentID><![CDATA[218]]></AgentID></xml>企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)2.验证消息体签名的正确性。 3.将post请求的数据进行xml解析,并将<Encrypt>标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档第2,3步可以用企业微信提供的库函数DecryptMsg来实现。'''# sReqMsgSig = HttpUtils.ParseUrl("msg_signature")sReqMsgSig = "0c3914025cb4b4d68103f6bfc8db550f79dcf48e"sReqTimeStamp = "1476422779"sReqNonce = "1597212914"sReqData = "<xml><ToUserName><![CDATA[ww1436e0e65a779aee]]></ToUserName>\n<Encrypt><![CDATA[Kl7kjoSf6DMD1zh7rtrHjFaDapSCkaOnwu3bqLc5tAybhhMl9pFeK8NslNPVdMwmBQTNoW4mY7AIjeLvEl3NyeTkAgGzBhzTtRLNshw2AEew+kkYcD+Fq72Kt00fT0WnN87hGrW8SqGc+NcT3mu87Ha3dz1pSDi6GaUA6A0sqfde0VJPQbZ9U+3JWcoD4Z5jaU0y9GSh010wsHF8KZD24YhmZH4ch4Ka7ilEbjbfvhKkNL65HHL0J6EYJIZUC2pFrdkJ7MhmEbU2qARR4iQHE7wy24qy0cRX3Mfp6iELcDNfSsPGjUQVDGxQDCWjayJOpcwocugux082f49HKYg84EpHSGXAyh+/oxwaWbvL6aSDPOYuPDGOCI8jmnKiypE+]]></Encrypt>\n<AgentID><![CDATA[1000002]]></AgentID>\n</xml>"ret, sMsg = wxcpt.DecryptMsg(sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce)printret, sMsgif (ret != 0):print"ERR: DecryptMsg ret: " + str(ret)sys.exit(1)# 解密成功,sMsg即为xml格式的明文# TODO: 对明文的处理# For example:xml_tree = ET.fromstring(sMsg)content = xml_tree.find("Content").textprintcontent# ...# ...'''------------使用示例三:企业回复用户消息的加密---------------企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的xml串。假设企业需要回复用户的明文如下:<xml><ToUserName><![CDATA[mycreate]]></ToUserName><FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[this is a test]]></Content><MsgId>1234567890123456</MsgId><AgentID>128</AgentID></xml>为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。2.将明文加密得到密文。   3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。   4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业号。以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。'''sRespData = "<xml><ToUserName>ww1436e0e65a779aee</ToUserName><FromUserName>ChenJiaShun</FromUserName><CreateTime>1476422779</CreateTime><MsgType>text</MsgType><Content>你好</Content><MsgId>1456453720</MsgId><AgentID>1000002</AgentID></xml>"ret, sEncryptMsg = wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp)if (ret != 0):print"ERR: EncryptMsg ret: " + str(ret)sys.exit(1)# ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号# TODO:# HttpUitls.SetResponse(sEncryptMsg)

验证 URL 有效性

文档地址:https://developer.work.weixin.qq.com/document/10514

当点击“保存”提交以上信息时,企业微信会发送一条验证消息到填写的 URL,发送方法为 GET。企业的接收消息服务器接收到验证请求后,需要作出正确的响应才能通过 URL 验证。

企业在获取请求时需要做Urldecode处理,否则会验证不成功
你可以访问接口调试工具进行调试,依次选择 建立连接 > 测试回调模式。

假设接收消息地址设置为:http://api.3dept.com/,企业微信将向该地址发送如下验证请求:

请求方式:GET
请求地址:http://api.3dept.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFSS&timestamp=13500001234&nonce=123412323&echostr=ENCRYPT_STR

在这里插入图片描述
企业后台收到请求后,需要做如下操作:

对收到的请求做Urldecode处理
通过参数msg_signature对请求进行校验,确认调用者的合法性。
解密echostr参数得到消息内容(即msg字段)
在1秒内原样返回明文消息内容(不能加引号,不能带bom头,不能带换行符)

Python 实现验证 URL 有效性源码

from flask import Flask, request
from callback.WXBizMsgCrypt3 import WXBizMsgCrypt
from lxml import etree
import timefrom configobj import ConfigObj
from receive.PassiveRecovery import PassiveRecoveryconfig = ConfigObj('./config.ini', encoding='utf-8')
debug_mode = config['dev'].as_bool('debug')app = Flask(__name__)@app.route('/', methods=['GET'])
def receive_callback():# 获取参数msg_signature = request.args.get('msg_signature')timestamp = request.args.get('timestamp')nonce = request.args.get('nonce')echostr = request.args.get('echostr')print("msg_signature", msg_signature)print("timestamp", timestamp)print("nonce", nonce)print("echostr", echostr)    print(config['wechat']['Token'], config['wechat']['EncodingAESKey'], config['wechat']['CorpID'])# 创建 WXBizMsgCrypt 对象wxcpt = WXBizMsgCrypt(config['wechat']['Token'], config['wechat']['EncodingAESKey'], config['wechat']['CorpID'])ret, sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)print("sEchoStr", sEchoStr)if ret != 0:print(str(ret))return 'ERR: VerifyURL ret: ' + str(ret)return sEchoStrif __name__ == '__main__':app.run(host="0.0.0.0", debug=debug_mode, port=8888)

使用接收消息

开启接收消息模式后,企业微信会将消息发送给企业填写的URL,企业后台需要做正确的响应。

接收消息协议的说明

企业微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。如果企业在调试中,发现成员无法收到被动回复的消息,可以检查是否消息处理超时。
当接收成功后,http头部返回200表示接收ok,其他错误码企业微信后台会一律当做失败并发起重试。
关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime排重。
假如企业无法保证在五秒内处理并回复,或者不想回复任何内容,可以直接返回200(即以空串为返回包)。企业后续可以使用主动发消息接口进行异步回复。

接收消息请求的说明
假设企业的接收消息的URL设置为http://api.3dept.com。

请求方式:POST
请求地址 :http://api.3dept.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFSS&timestamp=13500001234&nonce=123412323

接收数据格式

<xml> <ToUserName><![CDATA[toUser]]></ToUserName><AgentID><![CDATA[toAgentID]]></AgentID><Encrypt><![CDATA[msg_encrypt]]></Encrypt>
</xml>

在这里插入图片描述
企业收到消息后,需要作如下处理:

对msg_signature进行校验
解密Encrypt,得到明文的消息结构体(消息结构体后面章节会详说)
如果需要被动回复消息,构造被动响应包
正确响应本次请求

被动响应包的数据格式

<xml><Encrypt><![CDATA[msg_encrypt]]></Encrypt><MsgSignature><![CDATA[msg_signature]]></MsgSignature><TimeStamp>timestamp</TimeStamp><Nonce><![CDATA[nonce]]></Nonce>
</xml>

在这里插入图片描述
Python 实现接收回复消息源码

from flask import Flask, request
from callback.WXBizMsgCrypt3 import WXBizMsgCrypt
from lxml import etree
import timefrom configobj import ConfigObj
from receive.PassiveRecovery import PassiveRecoveryconfig = ConfigObj('./config.ini', encoding='utf-8')
debug_mode = config['dev'].as_bool('debug')app = Flask(__name__)@app.route('/', methods=['POST'])
def callback_message():# 获取参数msg_signature = request.args.get('msg_signature')timestamp = request.args.get('timestamp')nonce = request.args.get('nonce')# 获取 POST 的原始数据sReqData = request.data# 创建 WXBizMsgCrypt 对象wxcpt = WXBizMsgCrypt(config['wechat']['Token'], config['wechat']['EncodingAESKey'], config['wechat']['CorpID'])ret, sMsg = wxcpt.DecryptMsg(sReqData, msg_signature, timestamp, nonce)if ret != 0:return 'ERR: DecryptMsg ret: ' + str(ret)# 解析 XMLxml_tree = etree.fromstring(sMsg)print(xml_tree)from_user = xml_tree.find("FromUserName").textto_user = xml_tree.find("ToUserName").textmsg_type = xml_tree.find("MsgType").textprint(from_user)print(to_user)print(msg_type)# 消息被动回复if msg_type == 'text':content = xml_tree.find("Content").textprint(content)reply_xml = PassiveRecovery(to_user, from_user, msg_type, msg_content=content).text()else:media_id = xml_tree.find("MediaId").textprint(media_id)reply_xml = PassiveRecovery(to_user, from_user, msg_type, media_id=media_id).image()# 加密回复的 XMLret, sEncryptMsg = wxcpt.EncryptMsg(reply_xml, nonce, timestamp)if ret != 0:return 'ERR: EncryptMsg ret: ' + str(ret)return sEncryptMsgif __name__ == '__main__':app.run(debug=debug_mode, port=8888)

企业微信服务器 IP 段

企业微信在回调企业指定的URL时,是通过特定的IP发送出去的。如果企业需要做防火墙配置,那么可以通过这个接口获取到所有相关的IP段。

请求方式:GETHTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN

在这里插入图片描述
返回结果:

{"errcode": 0,"errmsg": "ok","ip_list": ["101.226.103.*", "101.226.62.*"]
}

在这里插入图片描述

源码部署服务器

接收消息 ./receive/PassiveRecovery.py

import timeclass PassiveRecovery(object):# 传入数据 to_user, from_user, msg_type, msg_content, media_iddef __init__(self, to_user, from_user, msg_type, msg_content=None, media_id=None):self.to_user = to_userself.from_user = from_userself.msg_type = msg_typeself.msg_content = msg_contentself.media_id = media_iddef text(self):return f"""<xml><ToUserName><![CDATA[{self.to_user}]]></ToUserName><FromUserName><![CDATA[{self.from_user}]]></FromUserName><CreateTime>{int(time.time())}</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[{self.msg_content}]]></Content></xml>"""def image(self):return f"""<xml><ToUserName><![CDATA[{self.to_user}]]></ToUserName><FromUserName><![CDATA[{self.from_user}]]></FromUserName><CreateTime>{int(time.time())}</CreateTime><MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[{self.media_id}]]></MediaId></Image></xml>"""def video(self):return f"""<xml><ToUserName><![CDATA[{self.to_user}]]></ToUserName><FromUserName><![CDATA[{self.from_user}]]></FromUserName><CreateTime>{int(time.time())}</CreateTime><MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[{self.media_id}]]></MediaId><Title><![CDATA[title]]></Title><Description><![CDATA[description]]></Description></Video></xml>"""

main.py

from flask import Flask, request
from callback.WXBizMsgCrypt3 import WXBizMsgCrypt
from lxml import etree
import timefrom configobj import ConfigObj
from receive.PassiveRecovery import PassiveRecoveryconfig = ConfigObj('./config.ini', encoding='utf-8')
debug_mode = config['dev'].as_bool('debug')app = Flask(__name__)@app.route('/', methods=['GET'])
def receive_callback():# 获取参数msg_signature = request.args.get('msg_signature')timestamp = request.args.get('timestamp')nonce = request.args.get('nonce')echostr = request.args.get('echostr')# 创建 WXBizMsgCrypt 对象wxcpt = WXBizMsgCrypt(config['wechat']['Token'], config['wechat']['EncodingAESKey'], config['wechat']['CorpID'])ret, sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)if ret != 0:return 'ERR: VerifyURL ret: ' + str(ret)return sEchoStr@app.route('/', methods=['POST'])
def callback_message():# 获取参数msg_signature = request.args.get('msg_signature')timestamp = request.args.get('timestamp')nonce = request.args.get('nonce')# 获取 POST 的原始数据sReqData = request.data# 创建 WXBizMsgCrypt 对象wxcpt = WXBizMsgCrypt(config['wechat']['Token'], config['wechat']['EncodingAESKey'], config['wechat']['CorpID'])ret, sMsg = wxcpt.DecryptMsg(sReqData, msg_signature, timestamp, nonce)if ret != 0:return 'ERR: DecryptMsg ret: ' + str(ret)# 解析 XMLxml_tree = etree.fromstring(sMsg)print(xml_tree)from_user = xml_tree.find("FromUserName").textto_user = xml_tree.find("ToUserName").textmsg_type = xml_tree.find("MsgType").textprint(from_user)print(to_user)print(msg_type)# 消息被动回复if msg_type == 'text':content = xml_tree.find("Content").textprint(content)reply_xml = PassiveRecovery(to_user, from_user, msg_type, msg_content=content).text()else:media_id = xml_tree.find("MediaId").textprint(media_id)reply_xml = PassiveRecovery(to_user, from_user, msg_type, media_id=media_id).image()# 加密回复的 XMLret, sEncryptMsg = wxcpt.EncryptMsg(reply_xml, nonce, timestamp)if ret != 0:return 'ERR: EncryptMsg ret: ' + str(ret)return sEncryptMsgif __name__ == '__main__':app.run(debug=debug_mode, port=8888)

参考项目:https://github.com/Waite0603/enterprise_wechat_bot

执行 main.py

python main.py

使用 Gunicorn 运行 Flask

gevent 是一个基于 libevent 的 Python 协程库,用于实现高性能的并发服务器。与多线程或多进程相比,使用 gevent 可以更有效地处理大量的并发连接,因为它通过协程实现非阻塞 I/O,从而避免了线程切换的开销。

安装必要的库:

首先确保你已经安装了 Flask 和 gevent。你可以使用 pip 来安装它们:

pip install Flask gevent

使用 Gunicorn + gevent worker

对于生产环境,更常见的做法是使用 Gunicorn 作为反向代理服务器,并配置它使用 gevent worker。这样,你可以利用 Gunicorn 的许多高级功能,如进程管理、日志记录等,同时仍然享受 gevent 带来的性能优势。

安装 Gunicorn

pip install gunicorn

使用命令启动你的 Flask 应用:

gunicorn -w 4 -k gevent 'app:main'

这里,-w 4 指定了 4 个 worker 进程,-k gevent 指定了使用 gevent worker。‘app:main’ 是你的 Flask 应用的位置,其中 app 是 Python 模块名,后面的 main 是 Flask 应用实例的变量名。

配置 nginx 服务器

域名解析:http://wxbot.willwaking.com -> 服务器 IP

在这里插入图片描述

注意:防止和静态路由冲突需要配置 nginx 请求转发。

location /wxbot/ {  proxy_pass http://localhost:8888/;  proxy_set_header Host $host;  proxy_set_header X-Real-IP $remote_addr;  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  proxy_set_header X-Forwarded-Proto $scheme;  
}

测试结果:

在这里插入图片描述
配置成功:

在这里插入图片描述

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

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

相关文章

Xshell Mobaxterm等终端工具连接不上服务器,显示 SSH服务器拒绝密码。请再试一次。解决办法

问题解决办法&#xff1a; &#xff08;1&#xff09;需要查看配置SSH密钥时&#xff0c;输入的password密码和当前users_name cd /home/: 查看当前系统下的用户名 注意上图中的登录名是服务器端linux下自己设置的user_name用户名&#xff1a; 所以需要将fl改为&#xff1a…

CCIE-10-IPv6-TS

目录 实验条件网络拓朴 环境配置开始Troubleshooting问题1. R25和R22邻居关系没有建立问题2. 去往R25网络的下一跳地址不存在、不可用问题3. 去往目标网络的下一跳地址不存在、不可用 实验条件 网络拓朴 环境配置 在我的资源里可以下载&#xff08;就在这篇文章的开头也可以下…

《Java面试自救指南》(专题三)数据库

文章目录 一条sql语句的查询流程有哪些数据库存储引擎&#xff0c;各自的区别数据库的三大范式事务的四大特性&#xff08;含隔离级别&#xff09;MySQL四种隔离机制的底层实现&#xff08;如何解决幻读 &#xff09;MySQL有哪几种锁&#xff0c;分别怎么实现数据库中有哪些索引…

Kubernetes学习笔记8

Kubernetes集群客户端工具kubectl 我们已经能够部署Kubernetes了&#xff0c;那么我们如何使用Kubernetes集群运行企业的应用程序呢&#xff1f;那么&#xff0c;我们就需要使用命令行工具kubectl。 学习目标&#xff1a; 了解kubectl 命令帮助方法 了解kubectl子命令使用分…

传统海外仓的管理模式有什么缺点?使用位像素海外仓系统的海外仓有什么优势?

传统的海外仓管理模式主要需要大量的人工操作和相对简单的信息化手段进行仓库的日常运营。因此&#xff0c;传统海外仓的运作比较依赖仓库员工的手工记录、核对和处理各种仓储和物流信息。 然而&#xff0c;传统海外仓管理模式通常存在一些缺点&#xff1a; 效率低下 因为需…

算法之美:缓存数据淘汰算法分析及分解实现

在设计一个系统的时候&#xff0c;由于数据库的读取速度远小于内存的读取速度&#xff0c;那么为加快读取速度&#xff0c;需先将一部分数据加入到内存中&#xff08;该动作称为缓存&#xff09;&#xff0c;但是内存容量又是有限的&#xff0c;当缓存的数据大于内存容量时&…

《乡土中国》中国基层传统社会里的一种体系,支配着社会生活的各方面 - 三余书屋 3ysw.net

乡土中国 大家好&#xff0c;今天我们要解读的是费孝通先生的经典著作《乡土中国》。这本书的中文版大约有10万字&#xff0c;我将用30分钟左右的时间为你解读书中的精髓。为什么说中国的根基在于乡土社会&#xff1f;我们应该从哪些方面来理解乡土社会的特征及其重要性&#…

Three 之 three.js (webgl)GLSL-Card 中文手册相关知识

Three 之 three.js &#xff08;webgl&#xff09;GLSL-Card 中文手册相关知识 目录 Three 之 three.js &#xff08;webgl&#xff09;GLSL-Card 中文手册相关知识 一、简单介绍 二、GLSL 中文手册 1、基本类型 2、基本结构和数组 3、向量的分量访问 4、运算符 5、基础…

南京观海微电子---Vitis HLS设计流程(实例演示)——Vitis HLS教程

1. 前言 课时2我们介绍了Vitis HLS的设计流程&#xff0c;如下图所示&#xff1a; 算法或软件的设计和仿真都基于C/C&#xff0c;通过HLS平台导出打包好的IP RTL代码&#xff0c;最后将该打包的IP加入到主工程使用。 本课时&#xff0c;我们通过一个具体的实例&#xff0c;演示…

Dapr(三) Dapr核心组件的使用一

结合前两期 Dapr(一) 基于云原生了解Dapr(Dapr(一) 基于云原生了解Dapr-CSDN博客) Dapr(二) 分布式应用运行时搭建及服务调用(Dapr(二) 分布式应用运行时搭建及服务调用-CSDN博客) 下篇推出dapr服务注册与发现&#xff0c;dapr组件绑定&#xff0c;dapr Actor功能。 目录 1.…

中颖51芯片学习2. IO端口操作

一、SH79F9476 I/O端口介绍 1. 特性 SH79F9476提供了30/26位可编程双向 I/O 端口&#xff1b;端口数据在寄存器Px中&#xff1b;端口控制寄存器PxCRy是控制端口作为输入还是输出&#xff1b;端口作为输入时&#xff0c;每个I/O端口均带有PxPCRy控制的内部上拉电阻。有些I/O引…

超详细!211页网络协议与管理,看完终于明白了(建议收藏)

与其说计算机改变了世界&#xff0c;不如说是计算机网络改变了世界。作为计算机网络通信实体之间的语言&#xff0c;网络通信协议对计算机正常通信起着极大的作用。 那么到底什么是网络协议与管理呢&#xff1f;今天给大家分享一份211页网络协议与管理文档&#xff0c;包含概念…

碧桂园服务净利降两成,关联交易收入仅占2.9%,发力增值服务充电桩日进超10万

自2018年分拆上市以来&#xff0c;碧桂园服务经历过非常高速的发展&#xff0c;曾是物管市场的“并购王”&#xff0c;但从2023年开始&#xff0c;希望从外延式的增长向内生式增长转型&#xff0c;将往期的经验与教训&#xff0c;通过投后管理沉淀下来&#xff0c;向高质量发展…

nginx多https证书配置精简

其实有很多方式&#xff0c;网上看到一个这个方法&#xff0c;给大家介绍一下。 首先&#xff0c;开启支持-TLS SNI support Nginx开启单IP多SSL证书支持-TLS SNI support Nginx支持单IP多域名SSL证书需要OpenSSL支持&#xff0c;首先需要编译安装一个高版本的openssl。 检查…

04 Python进阶:MySQL-PyMySQL

什么是 PyMySQL&#xff1f; PyMySQL 是一个用于 Python 的纯 Python MySQL 客户端库&#xff0c;提供了与 MySQL 数据库进行交互的功能。PyMySQL 允许 Python 开发人员连接到 MySQL 数据库服务器&#xff0c;并执行诸如查询、插入、更新和删除等数据库操作。 以下是 PyMySQL …

第29篇:秒表计时器

Q&#xff1a;本期我们采用计数器来实现秒表计时器&#xff0c;循环进行0~9计时。 A&#xff1a;在数码管HEX0上循环从0到9计数&#xff0c;间隔时间为1s&#xff0c;使用计数器实现1s时间间隔。 DE2-115开发板提供了50MHz时钟&#xff0c;触发器直接以50MHz信号作为同步时钟…

过亿级别的用户数据如何检查用户名是否存在?

目录 引言用户名存在性检查的挑战用户规模庞大带来的性能挑战数据一致性与并发性问题防止恶意行为的挑战 常见的解决方案基于数据库的方案基于缓存的方案基于分布式系统的方案基于搜索引擎的方案 案例分析与实践经验分享社交媒体平台的用户名检查方案 引言 随着互联网的普及和数…

PS从入门到精通视频各类教程整理全集,包含素材、作业等(9)复发

PS从入门到精通视频各类教程整理全集&#xff0c;包含素材、作业等 最新PS以及插件合集&#xff0c;可在我以往文章中找到 由于阿里云盘有分享次受限制和文件大小限制&#xff0c;今天先分享到这里&#xff0c;后续持续更新 第一课 ——第三课素材文件 https://www.alipan.c…

怎么在UE过场动画中加入振动效果

我们已经学会了怎么在游戏中加入振动效果&#xff0c;比较典型的交互场景如&#xff1a;在开枪时让手柄同步振动&#xff0c;实现起来真的很简单&#xff0c;就是定义场景和事件&#xff0c;然后在游戏事件发生时播放特定的振动资源文件&#xff0c;跟播放音效是极其相似的&…

探索Linux的挂载操作

在Linux这个强大的操作系统中&#xff0c;挂载操作是一个基本而重要的概念。它涉及到文件系统、设备和数据访问&#xff0c;对于理解Linux的工作方式至关重要。那么&#xff0c;挂载操作究竟是什么&#xff0c;为什么我们需要它&#xff0c;如果没有它&#xff0c;我们将面临什…