tornado实现与企业微信消息的交互

tornado实现与企业微信消息的交互

现在企业微信开放了普通用户注册,大大方便了我们实现一些“脑洞大开”的想法。DevOps喊了好多年了,近期出现了两个DevOps的分支,一个是ChatOps,一个是AIOps。ChatOps是最近几年火起来的,其中比较出名是slack+hubot的双剑合璧。在slack频道中,发送的一些信息,根据hubot的配置规则,是可以将聊天信息转化为具体命令去服务器上执行的。基于此,国内使用率比较高的企业IM软件就是企业微信了。在企业中,我们可以创建自己的应用模块,在该模块中实现与用户的消息进行交互,本篇文章就来实现这一目标(本篇文章只介绍在已创建自建应用的情况下,与用户消息交互,不含注册企业微信号和创建企业号自建应用的教程,这个自己去百度.com一下就可以了,点一点,没什么难度)

注意:本篇文章不考虑tornado框架的完善性,只作为一个restful开发框架来做基本的演示

前期准备:

  • 一个企业微信账号(认不认证都可以)
  • 在企业微信账号管理后台中创建一个自建应用(企业应用–>自建应用)
  • 拿到企业账号的CorpID(我的企业–>企业信息)
  • 在企业微信平台获取一个Token(进入自建应用中–>接收消息–>启动API接收–>随机获取Token)
  • 在企业微信平台获取一个EncodingAESKey(进入自建应用中–>接收消息–>启动API接收–>随机获取EncodingAESKey)
  • 一个有公网IP的服务器(可以在阿里云等厂商购买)
  • 一个Python2.7的环境

下载企业微信提供的加解密模块

企业微信提供了Python版本的加解密模块,我们可以直接从企业微信官方网站上下载使用

下载完成后,你将得到以下文件:

  • ierror.py 错误状态码
  • Sample.py 示例代码
  • WXBizMsgCrypt.py 加解密核心模块
  • Readme.txt

为了使用WXBizMsgCrypt.py这个模块,需要先在Python环境中安装pycrypto模块,执行一下命令

1
> pip install pycrypto

搭建tornado运行框架

在与微信提供的模块的同级目录中,创建tornado服务应用app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import tornado.web
import tornado.ioloop
from WXBizMsgCrypt import WXBizMsgCrypt
import sys
import xml.etree.cElementTree as ET
import xml.etree.ElementTree

class MainHandler(tornado.web.RequestHandler):

def get(self): # 建立连接,认证使用
pass

def post(self): # 信息收发,交互使用
pass

application = tornado.web.Application([
(r"/", MainHandler),
])

if __name__ == "__main__":

try:
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
except KeyboardInterrupt as e:
print("exit")
exit(0)

此处的tornado框架只为演示,框架其他功能未完善

与企业微信认证接口并建立连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def get(self):
# 声明并赋值企业微信中的信息
sToken = "iXIPhJ35"
sEncodingAESKey = "LRB2JiYSMUsszYrcbTd9fWTLwlYEhrEjb7Z9coWRBoJ"
sCorpID = "yy4f13b9b0a629296d"
wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID) # 使用企业微信提供的模块创建加密解密对象

# 获取URL中的参数
sVerifyMsgSig=self.get_argument('msg_signature')
sVerifyTimeStamp=self.get_argument('timestamp')
sVerifyNonce=self.get_argument('nonce')
sVerifyEchoStr=self.get_argument('echostr')

# 使用解密加密对象进行URL认证
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
if ret != 0:
print "ERR: VerifyURL ret:" + ret
sys.exit(1)

# 将解密后的字符串发回给企业微信端
self.write(sEchoStr)

认证建立连接的大概流程就是企业微信用自己的加密算法加密一个字符串通过get请求的方式发送给你 –> 你的接口拿到这次get请求后,依据URL中的参数,反解出企业微信发来的加密字符串 –> 解密后,将该字符串返回给企业微信端 –> 企业微信收到明文的字符串后会对比自己的初始发送的字符串 –> 对比成功即验证成功,建立连接

与企业微信进行信息交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def post(self):
# 声明并赋值企业微信中的信息,这点与上面get请求的认证是一样的
sToken = "iXIPhJ35"
sEncodingAESKey = "LRB2JiYSMUsszYrcbTd9fWTLwlYEhrEjb7Z9coWRBoJ"
sCorpID = "yy4f13b9b0a629296d"
wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID) # 使用企业微信提供的模块创建加密解密对象

# 获取URL中的参数(注意上面的get函数中的密文是通过get请求直接加在url中传递的,而交互模式中,密文是单独post过来的)
sVerifyMsgSig = self.get_argument('msg_signature')
sVerifyTimeStamp = self.get_argument('timestamp')
sVerifyNonce = self.get_argument('nonce')
sReqData = self.request.body

# 将以上信息进行解密
ret, sMsg = wxcpt.DecryptMsg(sReqData, sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce)
if (ret != 0):
print "ERR: VerifyURL ret:"
sys.exit(1)

# 根据接收到的xml消息,实例化成xml对象
xml_tree = ET.fromstring(sMsg)

# 判断是否是发送消息的行为
if xml.etree.ElementTree.iselement(xml_tree.find("Content")):
content = xml_tree.find("Content").text # 获取用户发送的消息
# 判断是否是点击菜单栏的行为
elif xml.etree.ElementTree.iselement(xml_tree.find("EventKey")):
content = xml_tree.find("EventKey").text # 获取用户点击菜单的id
else:
content = "other type"

# 将我们收到的信息再发回给企业微信,需要从request的xml获取以下信息
user_id = xml_tree.find("FromUserName").text # 用户id
corp_id = xml_tree.find("ToUserName").text # 企业id
create_time = xml_tree.find("CreateTime").text # 时间戳(用request的时间戳即可)

# 以下为需要回复给企业微信用户的消息的具体内容
content = "1.1.1.1\n2.2.2.2\n3.3.3.3"

# 由于不是很熟悉xml的操作,也不明白为啥微信不用json,非要任性的用xml
# 在这里只能用字符串拼出一个xml文件出来了 -_- 不要喷
# 以下xml即为发送回企业微信的完整消息体,该消息体不能直接明文发送,需要加密后发送
sRespData = """<xml>
<ToUserName><![CDATA["""+user_id+"""]]></ToUserName>
<FromUserName><![CDATA["""+corp_id+"""]]></FromUserName>
<CreateTime>"""+create_time+"""</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA["""+content+"""]]></Content>
</xml>"""

# 将上面的明文消息体使用企业微信的加密模块进行加密操作
ret,sEncryptMsg = wxcpt.EncryptMsg(sRespData, sVerifyNonce, sVerifyTimeStamp)
if ret!=0:
print "ERR: EncryptMsg ret: " + str(ret)
sys.exit(1)

# 最后将密文回复给企业微信,即完成企业微信用户与企业自己服务器之间的信息交互
self.write(sEncryptMsg)

信息交互的大概流程是企业微信用自己的加密算法加密用户发送的消息字符串,通过post请求的方式发送给你 –> 你的接口拿到post请求后,依据URL中的参数,和post过来的密文,反解出企业微信发来的加密消息 –> 解密后,将回复给用户的消息重新包进xml消息中 –> 将完整的xml进行加密 –> 将密文回复给企业微信端

没有下一步了 就是这么简单

以下是完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import tornado.web
import tornado.ioloop
from WXBizMsgCrypt import WXBizMsgCrypt
import sys
import xml.etree.cElementTree as ET
import xml.etree.ElementTree

class MainHandler(tornado.web.RequestHandler):

def get(self):
sToken = "iXIPhJ35"
sEncodingAESKey = "LRB2JiYSMUsszYrcbTd9fWTLwlYEhrEjb7Z9coWRBoJ"
sCorpID = "yy4f13b9b0a629296d"
wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID)

sVerifyMsgSig=self.get_argument('msg_signature')
sVerifyTimeStamp=self.get_argument('timestamp')
sVerifyNonce=self.get_argument('nonce')
sVerifyEchoStr=self.get_argument('echostr')

ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
if ret != 0:
print "ERR: VerifyURL ret:" + ret
sys.exit(1)

self.write(sEchoStr)

def post(self):
sToken = "iXIPhJ35"
sEncodingAESKey = "LRB2JiYSMUsszYrcbTd9fWTLwlYEhrEjb7Z9coWRBoJ"
sCorpID = "yy4f13b9b0a629296d"
wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID)

sVerifyMsgSig = self.get_argument('msg_signature')
sVerifyTimeStamp = self.get_argument('timestamp')
sVerifyNonce = self.get_argument('nonce')
sReqData = self.request.body

ret, sMsg = wxcpt.DecryptMsg(sReqData, sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce)
if (ret != 0):
print "ERR: VerifyURL ret:"
sys.exit(1)

xml_tree = ET.fromstring(sMsg)

if xml.etree.ElementTree.iselement(xml_tree.find("Content")):
content = xml_tree.find("Content").text
elif xml.etree.ElementTree.iselement(xml_tree.find("EventKey")):
content = xml_tree.find("EventKey").text
else:
content = "other type"

user_id = xml_tree.find("FromUserName").text
corp_id = xml_tree.find("ToUserName").text
create_time = xml_tree.find("CreateTime").text

content = "1.1.1.1\n2.2.2.2\n3.3.3.3"

sRespData = """<xml>
<ToUserName><![CDATA["""+user_id+"""]]></ToUserName>
<FromUserName><![CDATA["""+corp_id+"""]]></FromUserName>
<CreateTime>"""+create_time+"""</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA["""+content+"""]]></Content>
</xml>"""

ret,sEncryptMsg = wxcpt.EncryptMsg(sRespData, sVerifyNonce, sVerifyTimeStamp)
if ret!=0:
print "ERR: EncryptMsg ret: " + str(ret)
sys.exit(1)

self.write(sEncryptMsg)

application = tornado.web.Application([
(r"/", MainHandler),
])

if __name__ == "__main__":

try:
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
except KeyboardInterrupt as e:
print("exit")
exit(0)

以上代码只是示例代码,细心的童鞋可能会发现get和post中有部分重复代码,而且该tornado框架本身也没有提供log/异步非阻塞等特性的支持,只是为了演示企业微信与自由服务器信息交互的流程与使用方法,无论你是使用Falcon还是Flask框架,逻辑都差不多,注意各个框架的回文函数即可,比如tornado是self.write(), Flask是直接return