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
模块,执行一下命令
搭建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.webimport tornado.ioloopfrom WXBizMsgCrypt import WXBizMsgCryptimport sysimport xml.etree.cElementTree as ETimport xml.etree.ElementTreeclass 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) 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)
认证建立连接的大概流程就是企业微信用自己的加密算法加密一个字符串通过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) : 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)
信息交互的大概流程是企业微信用自己的加密算法加密用户发送的消息字符串,通过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.webimport tornado.ioloopfrom WXBizMsgCrypt import WXBizMsgCryptimport sysimport xml.etree.cElementTree as ETimport xml.etree.ElementTreeclass 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