Skip to content

Commit 4b21659

Browse files
authored
add callback
1 parent fd6b1cd commit 4b21659

4 files changed

Lines changed: 409 additions & 0 deletions

File tree

callback/Readme.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ע������
2+
1.WXBizMsgCrypt.py�ļ���װ��WXBizMsgCrypt�ӿ��࣬�ṩ���û�������ҵ΢�ŵ������ӿڣ�Sample.py�ļ��ṩ�����ʹ���������ӿڵ�ʾ����ierror.py�ṩ�˴����롣
3+
2.WXBizMsgCrypt��װ��VerifyURL, DecryptMsg, EncryptMsg�����ӿڣ��ֱ����ڿ�������֤�ص�url���յ��û��ظ���Ϣ�Ľ����Լ������߻ظ���Ϣ�ļ��ܹ��̡�ʹ�÷������Բο�Sample.py�ļ���
4+
3.�ӽ���Э����ο���ҵ΢�Źٷ��ĵ���
5+
4.�������õ���pycrypto�������⣬�뿪�������а�װ�˿���ʹ�á�

callback/Sample.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#########################################################################
4+
# Author: jonyqin
5+
# Created Time: Thu 11 Sep 2014 03:55:41 PM CST
6+
# File Name: Sample.py
7+
# Description: WXBizMsgCrypt 使用demo文件
8+
#########################################################################
9+
from WXBizMsgCrypt import WXBizMsgCrypt
10+
import xml.etree.cElementTree as ET
11+
import sys
12+
13+
if __name__ == "__main__":
14+
#假设企业在企业微信后台上设置的参数如下
15+
sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo"
16+
sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt"
17+
sCorpID = "ww1436e0e65a779aee"
18+
'''
19+
------------使用示例一:验证回调URL---------------
20+
*企业开启回调模式时,企业号会向验证url发送一个get请求
21+
假设点击验证时,企业收到类似请求:
22+
* GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
23+
* HTTP/1.1 Host: qy.weixin.qq.com
24+
25+
接收到该请求时,企业应 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
26+
这一步注意作URL解码。
27+
2.验证消息体签名的正确性
28+
3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
29+
第2,3步可以用企业微信提供的库函数VerifyURL来实现。
30+
'''
31+
wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID)
32+
#sVerifyMsgSig=HttpUtils.ParseUrl("msg_signature")
33+
ret = wxcpt.VerifyAESKey()
34+
print ret
35+
sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb"
36+
#sVerifyTimeStamp=HttpUtils.ParseUrl("timestamp")
37+
sVerifyTimeStamp="1476416373"
38+
#sVerifyNonce=HttpUitls.ParseUrl("nonce")
39+
sVerifyNonce="47744683"
40+
#sVerifyEchoStr=HttpUtils.ParseUrl("echostr")
41+
sVerifyEchoStr="fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA=="
42+
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
43+
if(ret!=0):
44+
print "ERR: VerifyURL ret: " + str(ret)
45+
sys.exit(1)
46+
#验证URL成功,将sEchoStr返回给企业号
47+
#HttpUtils.SetResponse(sEchoStr)
48+
49+
'''
50+
------------使用示例二:对用户回复的消息解密---------------
51+
用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
52+
假设企业收到企业微信的回调消息如下:
53+
POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1
54+
Host: qy.weixin.qq.com
55+
Content-Length: 613
56+
<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>
57+
<AgentID><![CDATA[218]]></AgentID>
58+
</xml>
59+
60+
企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
61+
2.验证消息体签名的正确性。 3.将post请求的数据进行xml解析,并将<Encrypt>标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
62+
第2,3步可以用企业微信提供的库函数DecryptMsg来实现。
63+
'''
64+
# sReqMsgSig = HttpUtils.ParseUrl("msg_signature")
65+
sReqMsgSig = "0c3914025cb4b4d68103f6bfc8db550f79dcf48e"
66+
sReqTimeStamp = "1476422779"
67+
sReqNonce = "1597212914"
68+
sReqData = "<xml><ToUserName><![CDATA[ww1436e0e65a779aee]]></ToUserName>\n<Encrypt><![CDATA[Kl7kjoSf6DMD1zh7rtrHjFaDapSCkaOnwu3bqLc5tAybhhMl9pFeK8NslNPVdMwmBQTNoW4mY7AIjeLvEl3NyeTkAgGzBhzTtRLNshw2AEew+kkYcD+Fq72Kt00fT0WnN87hGrW8SqGc+NcT3mu87Ha3dz1pSDi6GaUA6A0sqfde0VJPQbZ9U+3JWcoD4Z5jaU0y9GSh010wsHF8KZD24YhmZH4ch4Ka7ilEbjbfvhKkNL65HHL0J6EYJIZUC2pFrdkJ7MhmEbU2qARR4iQHE7wy24qy0cRX3Mfp6iELcDNfSsPGjUQVDGxQDCWjayJOpcwocugux082f49HKYg84EpHSGXAyh+/oxwaWbvL6aSDPOYuPDGOCI8jmnKiypE+]]></Encrypt>\n<AgentID><![CDATA[1000002]]></AgentID>\n</xml>"
69+
ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce)
70+
print ret,sMsg
71+
if( ret!=0 ):
72+
print "ERR: DecryptMsg ret: " + str(ret)
73+
sys.exit(1)
74+
# 解密成功,sMsg即为xml格式的明文
75+
# TODO: 对明文的处理
76+
# For example:
77+
xml_tree = ET.fromstring(sMsg)
78+
content = xml_tree.find("Content").text
79+
print content
80+
# ...
81+
# ...
82+
83+
'''
84+
------------使用示例三:企业回复用户消息的加密---------------
85+
企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的xml串。
86+
假设企业需要回复用户的明文如下:
87+
<xml>
88+
<ToUserName><![CDATA[mycreate]]></ToUserName>
89+
<FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName>
90+
<CreateTime>1348831860</CreateTime>
91+
<MsgType><![CDATA[text]]></MsgType>
92+
<Content><![CDATA[this is a test]]></Content>
93+
<MsgId>1234567890123456</MsgId>
94+
<AgentID>128</AgentID>
95+
</xml>
96+
97+
为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
98+
2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业号。
99+
以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
100+
'''
101+
sRespData = "<xml><ToUserName>ww1436e0e65a779aee</ToUserName><FromUserName>ChenJiaShun</FromUserName><CreateTime>1476422779</CreateTime><MsgType>text</MsgType><Content>你好</Content><MsgId>1456453720</MsgId><AgentID>1000002</AgentID></xml>"
102+
ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp)
103+
if( ret!=0 ):
104+
print "ERR: EncryptMsg ret: " + str(ret)
105+
sys.exit(1)
106+
#ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号
107+
#TODO:
108+
#HttpUitls.SetResponse(sEncryptMsg)

callback/WXBizMsgCrypt.py

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
#!/usr/bin/env python
2+
#-*- encoding:utf-8 -*-
3+
4+
""" 对企业微信发送给企业后台的消息加解密示例代码.
5+
@copyright: Copyright (c) 1998-2014 Tencent Inc.
6+
7+
"""
8+
# ------------------------------------------------------------------------
9+
10+
import base64
11+
import string
12+
import random
13+
import hashlib
14+
import time
15+
import struct
16+
from Crypto.Cipher import AES
17+
import xml.etree.cElementTree as ET
18+
import sys
19+
import socket
20+
reload(sys)
21+
import ierror
22+
sys.setdefaultencoding('utf-8')
23+
24+
"""
25+
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
26+
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
27+
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
28+
"""
29+
class FormatException(Exception):
30+
pass
31+
32+
def throw_exception(message, exception_class=FormatException):
33+
"""my define raise exception function"""
34+
raise exception_class(message)
35+
36+
class SHA1:
37+
"""计算企业微信的消息签名接口"""
38+
39+
def getSHA1(self, token, timestamp, nonce, encrypt):
40+
"""用SHA1算法生成安全签名
41+
@param token: 票据
42+
@param timestamp: 时间戳
43+
@param encrypt: 密文
44+
@param nonce: 随机字符串
45+
@return: 安全签名
46+
"""
47+
try:
48+
sortlist = [token, timestamp, nonce, encrypt]
49+
sortlist.sort()
50+
sha = hashlib.sha1()
51+
sha.update("".join(sortlist))
52+
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
53+
except Exception,e:
54+
print e
55+
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
56+
57+
58+
class XMLParse:
59+
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
60+
61+
# xml消息模板
62+
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
63+
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
64+
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
65+
<TimeStamp>%(timestamp)s</TimeStamp>
66+
<Nonce><![CDATA[%(nonce)s]]></Nonce>
67+
</xml>"""
68+
69+
def extract(self, xmltext):
70+
"""提取出xml数据包中的加密消息
71+
@param xmltext: 待提取的xml字符串
72+
@return: 提取出的加密消息字符串
73+
"""
74+
try:
75+
xml_tree = ET.fromstring(xmltext)
76+
encrypt = xml_tree.find("Encrypt")
77+
touser_name = xml_tree.find("ToUserName")
78+
return ierror.WXBizMsgCrypt_OK, encrypt.text, touser_name.text
79+
except Exception,e:
80+
print e
81+
return ierror.WXBizMsgCrypt_ParseXml_Error,None,None
82+
83+
def generate(self, encrypt, signature, timestamp, nonce):
84+
"""生成xml消息
85+
@param encrypt: 加密后的消息密文
86+
@param signature: 安全签名
87+
@param timestamp: 时间戳
88+
@param nonce: 随机字符串
89+
@return: 生成的xml字符串
90+
"""
91+
resp_dict = {
92+
'msg_encrypt' : encrypt,
93+
'msg_signaturet': signature,
94+
'timestamp' : timestamp,
95+
'nonce' : nonce,
96+
}
97+
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
98+
return resp_xml
99+
100+
101+
class PKCS7Encoder():
102+
"""提供基于PKCS7算法的加解密接口"""
103+
104+
block_size = 32
105+
def encode(self, text):
106+
""" 对需要加密的明文进行填充补位
107+
@param text: 需要进行填充补位操作的明文
108+
@return: 补齐明文字符串
109+
"""
110+
text_length = len(text)
111+
# 计算需要填充的位数
112+
amount_to_pad = self.block_size - (text_length % self.block_size)
113+
if amount_to_pad == 0:
114+
amount_to_pad = self.block_size
115+
# 获得补位所用的字符
116+
pad = chr(amount_to_pad)
117+
return text + pad * amount_to_pad
118+
119+
def decode(self, decrypted):
120+
"""删除解密后明文的补位字符
121+
@param decrypted: 解密后的明文
122+
@return: 删除补位字符后的明文
123+
"""
124+
pad = ord(decrypted[-1])
125+
if pad<1 or pad >32:
126+
pad = 0
127+
return decrypted[:-pad]
128+
129+
130+
class Prpcrypt(object):
131+
"""提供接收和推送给企业微信消息的加解密接口"""
132+
133+
def __init__(self,key):
134+
135+
#self.key = base64.b64decode(key+"=")
136+
self.key = key
137+
# 设置加解密模式为AES的CBC模式
138+
self.mode = AES.MODE_CBC
139+
140+
141+
def encrypt(self,text,corpid):
142+
"""对明文进行加密
143+
@param text: 需要加密的明文
144+
@return: 加密得到的字符串
145+
"""
146+
# 16位随机字符串添加到明文开头
147+
text = self.get_random_str() + struct.pack("I",socket.htonl(len(text))) + text + corpid
148+
# 使用自定义的填充方式对明文进行补位填充
149+
pkcs7 = PKCS7Encoder()
150+
text = pkcs7.encode(text)
151+
# 加密
152+
cryptor = AES.new(self.key,self.mode,self.key[:16])
153+
try:
154+
ciphertext = cryptor.encrypt(text)
155+
# 使用BASE64对加密后的字符串进行编码
156+
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
157+
except Exception,e:
158+
print e
159+
return ierror.WXBizMsgCrypt_EncryptAES_Error,None
160+
161+
def decrypt(self,text,corpid):
162+
"""对解密后的明文进行补位删除
163+
@param text: 密文
164+
@return: 删除填充补位后的明文
165+
"""
166+
try:
167+
cryptor = AES.new(self.key,self.mode,self.key[:16])
168+
# 使用BASE64对密文进行解码,然后AES-CBC解密
169+
plain_text = cryptor.decrypt(base64.b64decode(text))
170+
except Exception,e:
171+
print e
172+
return ierror.WXBizMsgCrypt_DecryptAES_Error,None
173+
try:
174+
pad = ord(plain_text[-1])
175+
# 去掉补位字符串
176+
#pkcs7 = PKCS7Encoder()
177+
#plain_text = pkcs7.encode(plain_text)
178+
# 去除16位随机字符串
179+
content = plain_text[16:-pad]
180+
xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
181+
xml_content = content[4 : xml_len+4]
182+
from_corpid = content[xml_len+4:]
183+
except Exception,e:
184+
print e
185+
return ierror.WXBizMsgCrypt_IllegalBuffer,None
186+
if from_corpid != corpid:
187+
return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None
188+
return 0,xml_content
189+
190+
def get_random_str(self):
191+
""" 随机生成16位字符串
192+
@return: 16位字符串
193+
"""
194+
rule = string.letters + string.digits
195+
str = random.sample(rule, 16)
196+
return "".join(str)
197+
198+
class WXBizMsgCrypt(object):
199+
#构造函数
200+
#@param sToken: 企业微信后台,开发者设置的Token
201+
# @param sEncodingAESKey: 企业微信后台,开发者设置的EncodingAESKey
202+
# @param sCorpId: 企业号的CorpId
203+
def __init__(self,sToken,sEncodingAESKey,sCorpId):
204+
try:
205+
self.key = base64.b64decode(sEncodingAESKey+"=")
206+
assert len(self.key) == 32
207+
except:
208+
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
209+
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
210+
self.m_sToken = sToken
211+
self.m_sCorpid = sCorpId
212+
213+
#验证URL
214+
#@param sMsgSignature: 签名串,对应URL参数的msg_signature
215+
#@param sTimeStamp: 时间戳,对应URL参数的timestamp
216+
#@param sNonce: 随机串,对应URL参数的nonce
217+
#@param sEchoStr: 随机串,对应URL参数的echostr
218+
#@param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
219+
#@return:成功0,失败返回对应的错误码
220+
221+
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
222+
sha1 = SHA1()
223+
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
224+
if ret != 0:
225+
return ret, None
226+
if not signature == sMsgSignature:
227+
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
228+
pc = Prpcrypt(self.key)
229+
ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sCorpid)
230+
return ret,sReplyEchoStr
231+
232+
def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None):
233+
#将企业回复用户的消息加密打包
234+
#@param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
235+
#@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
236+
#@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
237+
#sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
238+
#return:成功0,sEncryptMsg,失败返回对应的错误码None
239+
pc = Prpcrypt(self.key)
240+
ret,encrypt = pc.encrypt(sReplyMsg, self.m_sCorpid)
241+
if ret != 0:
242+
return ret,None
243+
if timestamp is None:
244+
timestamp = str(int(time.time()))
245+
# 生成安全签名
246+
sha1 = SHA1()
247+
ret,signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
248+
if ret != 0:
249+
return ret,None
250+
xmlParse = XMLParse()
251+
return ret,xmlParse.generate(encrypt, signature, timestamp, sNonce)
252+
253+
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
254+
# 检验消息的真实性,并且获取解密后的明文
255+
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
256+
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
257+
# @param sNonce: 随机串,对应URL参数的nonce
258+
# @param sPostData: 密文,对应POST请求的数据
259+
# xml_content: 解密后的原文,当return返回0时有效
260+
# @return: 成功0,失败返回对应的错误码
261+
# 验证安全签名
262+
xmlParse = XMLParse()
263+
ret,encrypt,touser_name = xmlParse.extract(sPostData)
264+
if ret != 0:
265+
return ret, None
266+
sha1 = SHA1()
267+
ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
268+
if ret != 0:
269+
return ret, None
270+
if not signature == sMsgSignature:
271+
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
272+
pc = Prpcrypt(self.key)
273+
ret,xml_content = pc.decrypt(encrypt,self.m_sCorpid)
274+
return ret,xml_content
275+
276+

0 commit comments

Comments
 (0)