├── .gitattributes ├── config.py ├── ierror.py ├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── func.py ├── main.py └── WXBizMsgCrypt.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # sToken和sEncodingAESKey来自于企业微信创建的应用 2 | # sCorpID为企业微信号 3 | 4 | sToken = "" 5 | sEncodingAESKey = "" 6 | sCorpID = "" -------------------------------------------------------------------------------- /ierror.py: -------------------------------------------------------------------------------- 1 | WXBizMsgCrypt_OK = 0 2 | WXBizMsgCrypt_ValidateSignature_Error = -40001 3 | WXBizMsgCrypt_ParseXml_Error = -40002 4 | WXBizMsgCrypt_ComputeSignature_Error = -40003 5 | WXBizMsgCrypt_IllegalAesKey = -40004 6 | WXBizMsgCrypt_ValidateCorpid_Error = -40005 7 | WXBizMsgCrypt_EncryptAES_Error = -40006 8 | WXBizMsgCrypt_DecryptAES_Error = -40007 9 | WXBizMsgCrypt_IllegalBuffer = -40008 10 | WXBizMsgCrypt_EncodeBase64_Error = -40009 11 | WXBizMsgCrypt_DecodeBase64_Error = -40010 12 | WXBizMsgCrypt_GenReturnXml_Error = -40011 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 lyleshaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI-WeWork-Robot 2 | 3 | > 基于FastAPI创建的、使用企业微信交互的机器人系统 4 | 5 | ## 起因 6 | 7 | 我从去年想要拥有一个TODOlist开始,一步步的拓展这个想法:从单纯的列出任务,到任务到期提醒...一点点的有其他新的想法。 8 | 9 | 关于推送方式,我最早是希望通过发送邮件,然后通过微信企业邮的邮件提醒达到推送效果,而后发现了server酱,又开始使用这种方式实现微信推送,期间又接触了某个可以白嫖的短信api,但是免费一个帐号大概400条(配置还很麻烦),最终因为学校的企业微信最近推送了好几条消息...意识到企业微信是目前最适合的推送方式。 10 | 11 | 当然,单纯了做了任务提醒的功能后,发现企业微信的api还挺好玩的,于是一点点看文档写了其他一些小功能(参考func.py文件) 12 | 13 | ## 配置及使用 14 | 15 | > requirements:fastapi,uvicorn 16 | 17 | ### 企业微信配置 18 | 19 | 首先,请配置config.py文件按要求填写: 20 | 21 | ``` 22 | # sToken和sEncodingAESKey来自于企业微信创建的应用 23 | # sCorpID为企业微信号 24 | 25 | sToken = "" 26 | sEncodingAESKey = "" 27 | sCorpID = "" 28 | ``` 29 | 30 | ### 中间件配置 31 | 32 | 如果想手动设置中间件(默认为不限制访问),请参考FastAPI文档设置main.py文件中的如下代码 33 | 34 | ``` 35 | app.add_middleware( 36 | CORSMiddleware, 37 | allow_origins=['*'], 38 | allow_credentials=True, 39 | allow_methods=["*"], 40 | allow_headers=["*"], 41 | ) 42 | ``` 43 | 44 | ### 使用 45 | 46 | 请先启动main.py程序 47 | 48 | 然后按照[企业微信的要求](https://work.weixin.qq.com/api/doc/90001/90143/91116)完成URL验证。 49 | 50 | 而后即可正常使用。 51 | 52 | ## 自定义功能 53 | 54 | 如果您需要自定义企业微信被动回复功能,可以对func.py文件中的handle_msg函数进行修改。 55 | 56 | handle_msg有两个参数 57 | 58 | + to_user_id: str # 用户名 59 | + recived_msg: str # 用户发送的信息 60 | 61 | 返回值即为被动回复的消息内容 62 | 63 | ## 反馈&联系我 64 | 65 | 本项目基于[企业微信官方python库](https://github.com/sbzhu/weworkapi_python)进行修改。 66 | 67 | 如果有其他问题,可以通过如下方式联系我 68 | 69 | + [lyle@hdu.edu.cn](mailto:lyle@hdu.edu.cn) 70 | + [lyleshaw.com](https://lyleshaw.com/) -------------------------------------------------------------------------------- /func.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import os 4 | 5 | # 将信息写入csv表格 6 | def write_msg(to_user_id: str, key: str, value: str): 7 | file_name = to_user_id + '.csv' 8 | if os.path.exists(file_name)!=1: 9 | f = open(file_name,mode='a+') 10 | f.write("Owner,Key,Value") 11 | f.close() 12 | data = pd.read_csv(file_name,encoding='gbk') 13 | data = data.values.tolist() 14 | for i in data: 15 | if i[1] == key: 16 | return [0,"error"] 17 | data.append([to_user_id,key,value]) 18 | data = pd.core.frame.DataFrame(data) 19 | data.rename(columns={0:'Owner',1:'Key',2:'Value'},inplace=True) 20 | data.to_csv(file_name,index=None,encoding='gbk') 21 | return [1,"success"] 22 | 23 | # 从csv表格中读取 24 | def read_msg(to_user_id: str, key: str): 25 | file_name = to_user_id + '.csv' 26 | if os.path.exists(file_name)!=1: 27 | f = open(file_name,mode='a+') 28 | f.write("Owner,Key,Value") 29 | f.close() 30 | data = pd.read_csv(file_name,encoding='gbk') 31 | data = data.values.tolist() 32 | for i in data: 33 | if str(i[1]) == key: 34 | return [1,i[2]] 35 | return [0,"not found"] 36 | 37 | # 处理消息函数 38 | # to_user_id为用户名 39 | # recived_msg为接收的消息 40 | def handle_msg(to_user_id: str, recived_msg: str): 41 | name = to_user_id 42 | if '存' in recived_msg: 43 | _, key, value = recived_msg.split(" ") 44 | ret = write_msg(to_user_id,key,value) 45 | if ret[0]==0: 46 | return name+"您好,您的关键词已重复,请重新输入" 47 | else: 48 | return name+"您好,您的关键词"+key+"已录入" 49 | elif '取' in recived_msg: 50 | _, key = recived_msg.split(" ") 51 | ret, msg = read_msg(to_user_id,key) 52 | if ret==0: 53 | return name+"您好,未找到您的关键词,请重新输入" 54 | else: 55 | return name+"您好,您的关键词"+key+"的内容是:\n" + str(msg) 56 | else: 57 | return recived_msg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import Depends, FastAPI, HTTPException 3 | import time 4 | import sys 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from pydantic import BaseModel 7 | from starlette.requests import Request 8 | from typing import TypeVar, Generic, Type, Any 9 | from xml.etree.ElementTree import fromstring 10 | import xml.etree.cElementTree as ET 11 | 12 | from WXBizMsgCrypt import WXBizMsgCrypt 13 | from config import sCorpID,sEncodingAESKey,sToken 14 | import func 15 | 16 | # 启动App 17 | app = FastAPI() 18 | 19 | # 设置中间件 20 | app.add_middleware( 21 | CORSMiddleware, 22 | allow_origins=['*'], 23 | allow_credentials=True, 24 | allow_methods=["*"], 25 | allow_headers=["*"], 26 | ) 27 | 28 | # 创建登录会话 29 | wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID) 30 | 31 | 32 | # 以下为接受XML格式数据部分 33 | T = TypeVar("T", bound=BaseModel) 34 | 35 | class Item(BaseModel): 36 | ToUserName: str 37 | AgentID: str 38 | Encrypt: str 39 | 40 | class XmlBody(Generic[T]): 41 | def __init__(self, model_class: Type[T]): 42 | self.model_class = model_class 43 | 44 | async def __call__(self, request: Request) -> T: 45 | # the following check is unnecessary if always using xml, 46 | # but enables the use of json too 47 | # print(request.headers.get("Content-Type")) 48 | if '/xml' in request.headers.get("Content-Type", ""): 49 | body = await request.body() 50 | doc = fromstring(body) 51 | dict_data = {} 52 | for node in doc.getchildren(): 53 | dict_data[node.tag] = node.text 54 | else: 55 | dict_data = await request.json() 56 | return self.model_class.parse_obj(dict_data) 57 | 58 | 59 | # 接受消息模版 60 | Recived_Temp = """ 61 | 62 | 63 | 64 | """ 65 | 66 | #发送消息模版 67 | Send_Temp = """ 68 | %(ToUserName)s 69 | %(FromUserName)s 70 | %(timestamp)s 71 | text 72 | %(content)s 73 | """ 74 | 75 | 76 | 77 | # 回调验证部分 78 | @app.get("/") 79 | async def Verify(msg_signature: str, timestamp: str, nonce: str, echostr: str): 80 | sVerifyMsgSig = msg_signature 81 | sVerifyTimeStamp = timestamp 82 | sVerifyNonce = nonce 83 | sVerifyEchoStr = echostr 84 | ret, sReplyEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr) 85 | if( ret!=0 ): 86 | print("ERR: DecryptMsg ret: " + str(ret)) 87 | sys.exit(1) 88 | return int(sReplyEchoStr) 89 | 90 | # 消息接收部分 91 | @app.post("/") 92 | async def main(msg_signature: str, timestamp: str, nonce: str, q: str = None, item: Item = Depends(XmlBody(Item))): 93 | Recived_dict = { 94 | 'ToUserName': item.ToUserName, 95 | 'AgentID': item.AgentID, 96 | 'Encrypt': item.Encrypt, 97 | } 98 | ReqData = Recived_Temp % Recived_dict 99 | ret,sMsg=wxcpt.DecryptMsg(sPostData=ReqData, sMsgSignature=msg_signature, sTimeStamp=timestamp, sNonce=nonce) 100 | if( ret!=0 ): 101 | print("ERR: DecryptMsg ret: " + str(ret)) 102 | sys.exit(1) 103 | xml_tree = ET.fromstring(sMsg) 104 | content_recived = xml_tree.find("Content").text 105 | FromUserName = xml_tree.find("FromUserName").text 106 | ToUserName = xml_tree.find("ToUserName").text 107 | 108 | # 消息处理部分 109 | content_send = func.handle_msg(to_user_id = FromUserName, recived_msg = content_recived) 110 | 111 | Send_dict = { 112 | "ToUserName": ToUserName, 113 | "FromUserName": FromUserName, 114 | "timestamp": timestamp, 115 | "content": content_send, 116 | } 117 | sRespData = Send_Temp % Send_dict 118 | ret,sEncryptMsg=wxcpt.EncryptMsg(sReplyMsg = sRespData, sNonce = nonce, timestamp = timestamp) 119 | return sEncryptMsg 120 | 121 | # 启动服务 122 | if __name__ == '__main__': 123 | import uvicorn 124 | uvicorn.run(app, host="0.0.0.0", port=8181) -------------------------------------------------------------------------------- /WXBizMsgCrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding:utf-8 -*- 3 | 4 | """ 对企业微信发送给企业后台的消息加解密示例代码. 5 | @copyright: Copyright (c) 1998-2014 Tencent Inc. 6 | 7 | """ 8 | # ------------------------------------------------------------------------ 9 | import logging 10 | import base64 11 | import random 12 | import hashlib 13 | import time 14 | import struct 15 | from Crypto.Cipher import AES 16 | import xml.etree.cElementTree as ET 17 | import socket 18 | 19 | import ierror 20 | 21 | 22 | """ 23 | 关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 24 | 请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 25 | 下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 26 | """ 27 | 28 | 29 | class FormatException(Exception): 30 | pass 31 | 32 | 33 | def throw_exception(message, exception_class=FormatException): 34 | """my define raise exception function""" 35 | raise exception_class(message) 36 | 37 | 38 | class SHA1: 39 | """计算企业微信的消息签名接口""" 40 | 41 | def getSHA1(self, token, timestamp, nonce, encrypt): 42 | """用SHA1算法生成安全签名 43 | @param token: 票据 44 | @param timestamp: 时间戳 45 | @param encrypt: 密文 46 | @param nonce: 随机字符串 47 | @return: 安全签名 48 | """ 49 | try: 50 | sortlist = [token, timestamp, nonce, encrypt] 51 | sortlist.sort() 52 | sha = hashlib.sha1() 53 | sha.update("".join(sortlist).encode()) 54 | return ierror.WXBizMsgCrypt_OK, sha.hexdigest() 55 | except Exception as e: 56 | logger = logging.getLogger() 57 | logger.error(e) 58 | return ierror.WXBizMsgCrypt_ComputeSignature_Error, None 59 | 60 | 61 | class XMLParse: 62 | """提供提取消息格式中的密文及生成回复消息格式的接口""" 63 | 64 | # xml消息模板 65 | AES_TEXT_RESPONSE_TEMPLATE = """ 66 | 67 | 68 | %(timestamp)s 69 | 70 | """ 71 | 72 | def extract(self, xmltext): 73 | """提取出xml数据包中的加密消息 74 | @param xmltext: 待提取的xml字符串 75 | @return: 提取出的加密消息字符串 76 | """ 77 | try: 78 | xml_tree = ET.fromstring(xmltext) 79 | encrypt = xml_tree.find("Encrypt") 80 | return ierror.WXBizMsgCrypt_OK, encrypt.text 81 | except Exception as e: 82 | logger = logging.getLogger() 83 | logger.error(e) 84 | return ierror.WXBizMsgCrypt_ParseXml_Error, None 85 | 86 | def generate(self, encrypt, signature, timestamp, nonce): 87 | """生成xml消息 88 | @param encrypt: 加密后的消息密文 89 | @param signature: 安全签名 90 | @param timestamp: 时间戳 91 | @param nonce: 随机字符串 92 | @return: 生成的xml字符串 93 | """ 94 | resp_dict = { 95 | 'msg_encrypt': encrypt, 96 | 'msg_signaturet': signature, 97 | 'timestamp': timestamp, 98 | 'nonce': nonce, 99 | } 100 | resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict 101 | return resp_xml 102 | 103 | 104 | class PKCS7Encoder(): 105 | """提供基于PKCS7算法的加解密接口""" 106 | 107 | block_size = 32 108 | 109 | def encode(self, text): 110 | """ 对需要加密的明文进行填充补位 111 | @param text: 需要进行填充补位操作的明文 112 | @return: 补齐明文字符串 113 | """ 114 | text_length = len(text) 115 | # 计算需要填充的位数 116 | amount_to_pad = self.block_size - (text_length % self.block_size) 117 | if amount_to_pad == 0: 118 | amount_to_pad = self.block_size 119 | # 获得补位所用的字符 120 | pad = chr(amount_to_pad) 121 | return text + (pad * amount_to_pad).encode() 122 | 123 | def decode(self, decrypted): 124 | """删除解密后明文的补位字符 125 | @param decrypted: 解密后的明文 126 | @return: 删除补位字符后的明文 127 | """ 128 | pad = ord(decrypted[-1]) 129 | if pad < 1 or pad > 32: 130 | pad = 0 131 | return decrypted[:-pad] 132 | 133 | 134 | class Prpcrypt(object): 135 | """提供接收和推送给企业微信消息的加解密接口""" 136 | 137 | def __init__(self, key): 138 | 139 | # self.key = base64.b64decode(key+"=") 140 | self.key = key 141 | # 设置加解密模式为AES的CBC模式 142 | self.mode = AES.MODE_CBC 143 | 144 | def encrypt(self, text, receiveid): 145 | """对明文进行加密 146 | @param text: 需要加密的明文 147 | @return: 加密得到的字符串 148 | """ 149 | # 16位随机字符串添加到明文开头 150 | text = text.encode() 151 | text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode() 152 | 153 | # 使用自定义的填充方式对明文进行补位填充 154 | pkcs7 = PKCS7Encoder() 155 | text = pkcs7.encode(text) 156 | # 加密 157 | cryptor = AES.new(self.key, self.mode, self.key[:16]) 158 | try: 159 | ciphertext = cryptor.encrypt(text) 160 | # 使用BASE64对加密后的字符串进行编码 161 | return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) 162 | except Exception as e: 163 | logger = logging.getLogger() 164 | logger.error(e) 165 | return ierror.WXBizMsgCrypt_EncryptAES_Error, None 166 | 167 | def decrypt(self, text, receiveid): 168 | """对解密后的明文进行补位删除 169 | @param text: 密文 170 | @return: 删除填充补位后的明文 171 | """ 172 | try: 173 | cryptor = AES.new(self.key, self.mode, self.key[:16]) 174 | # 使用BASE64对密文进行解码,然后AES-CBC解密 175 | plain_text = cryptor.decrypt(base64.b64decode(text)) 176 | except Exception as e: 177 | logger = logging.getLogger() 178 | logger.error(e) 179 | return ierror.WXBizMsgCrypt_DecryptAES_Error, None 180 | try: 181 | pad = plain_text[-1] 182 | # 去掉补位字符串 183 | # pkcs7 = PKCS7Encoder() 184 | # plain_text = pkcs7.encode(plain_text) 185 | # 去除16位随机字符串 186 | content = plain_text[16:-pad] 187 | xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0]) 188 | xml_content = content[4: xml_len + 4] 189 | from_receiveid = content[xml_len + 4:] 190 | except Exception as e: 191 | logger = logging.getLogger() 192 | logger.error(e) 193 | return ierror.WXBizMsgCrypt_IllegalBuffer, None 194 | 195 | if from_receiveid.decode('utf8') != receiveid: 196 | return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None 197 | return 0, xml_content 198 | 199 | def get_random_str(self): 200 | """ 随机生成16位字符串 201 | @return: 16位字符串 202 | """ 203 | return str(random.randint(1000000000000000, 9999999999999999)).encode() 204 | 205 | 206 | class WXBizMsgCrypt(object): 207 | # 构造函数 208 | def __init__(self, sToken, sEncodingAESKey, sReceiveId): 209 | try: 210 | self.key = base64.b64decode(sEncodingAESKey + "=") 211 | assert len(self.key) == 32 212 | except: 213 | throw_exception("[error]: EncodingAESKey unvalid !", FormatException) 214 | # return ierror.WXBizMsgCrypt_IllegalAesKey,None 215 | self.m_sToken = sToken 216 | self.m_sReceiveId = sReceiveId 217 | 218 | # 验证URL 219 | # @param sMsgSignature: 签名串,对应URL参数的msg_signature 220 | # @param sTimeStamp: 时间戳,对应URL参数的timestamp 221 | # @param sNonce: 随机串,对应URL参数的nonce 222 | # @param sEchoStr: 随机串,对应URL参数的echostr 223 | # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 224 | # @return:成功0,失败返回对应的错误码 225 | 226 | def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): 227 | sha1 = SHA1() 228 | ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) 229 | if ret != 0: 230 | return ret, None 231 | if not signature == sMsgSignature: 232 | return ierror.WXBizMsgCrypt_ValidateSignature_Error, None 233 | pc = Prpcrypt(self.key) 234 | ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId) 235 | return ret, sReplyEchoStr 236 | 237 | def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): 238 | # 将企业回复用户的消息加密打包 239 | # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 240 | # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 241 | # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce 242 | # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, 243 | # return:成功0,sEncryptMsg,失败返回对应的错误码None 244 | pc = Prpcrypt(self.key) 245 | ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) 246 | encrypt = encrypt.decode('utf8') 247 | if ret != 0: 248 | return ret, None 249 | if timestamp is None: 250 | timestamp = str(int(time.time())) 251 | # 生成安全签名 252 | sha1 = SHA1() 253 | ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) 254 | if ret != 0: 255 | return ret, None 256 | xmlParse = XMLParse() 257 | return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) 258 | 259 | def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): 260 | # 检验消息的真实性,并且获取解密后的明文 261 | # @param sMsgSignature: 签名串,对应URL参数的msg_signature 262 | # @param sTimeStamp: 时间戳,对应URL参数的timestamp 263 | # @param sNonce: 随机串,对应URL参数的nonce 264 | # @param sPostData: 密文,对应POST请求的数据 265 | # xml_content: 解密后的原文,当return返回0时有效 266 | # @return: 成功0,失败返回对应的错误码 267 | # 验证安全签名 268 | xmlParse = XMLParse() 269 | ret, encrypt = xmlParse.extract(sPostData) 270 | if ret != 0: 271 | return ret, None 272 | sha1 = SHA1() 273 | ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) 274 | if ret != 0: 275 | return ret, None 276 | if not signature == sMsgSignature: 277 | return ierror.WXBizMsgCrypt_ValidateSignature_Error, None 278 | pc = Prpcrypt(self.key) 279 | ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId) 280 | return ret, xml_content --------------------------------------------------------------------------------