diff --git a/apps/jqr/parsers.py b/apps/jqr/parsers.py new file mode 100644 index 0000000..2a8c753 --- /dev/null +++ b/apps/jqr/parsers.py @@ -0,0 +1,6 @@ +from rest_framework_xml.parsers import XMLParser + + +class WechatContentParser(XMLParser): + media_type = 'text/xml' + pass diff --git a/apps/jqr/renderers.py b/apps/jqr/renderers.py new file mode 100644 index 0000000..1758b27 --- /dev/null +++ b/apps/jqr/renderers.py @@ -0,0 +1,5 @@ +from rest_framework_xml.renderers import XMLRenderer + + +class WechatPublicContentRenderer(XMLRenderer): + root_tag_name = 'xml' diff --git a/apps/jqr/serializers.py b/apps/jqr/serializers.py index e69de29..2478b2f 100644 --- a/apps/jqr/serializers.py +++ b/apps/jqr/serializers.py @@ -0,0 +1,98 @@ +import base64 + +from rest_framework import serializers + +from libs.weworkapi.callback.WXBizMsgCrypt3 import WXBizMsgCrypt, Prpcrypt +from utils.tools import sha1_encoder, get_attribute +import xml.etree.cElementTree as ET + + +class WechatPublicTokenSerializer(serializers.Serializer): + msg_signature = serializers.CharField() + echostr = serializers.CharField() + timestamp = serializers.CharField() + nonce = serializers.CharField() + + def validate(self, attrs): + # token = settings.WECHAT_WORKER.get('TOKEN') + # corpid = settings.WECHAT_WORKER.get('CORPID') + # encoding_aes_key = settings.WECHAT_WORKER.get('EncodingAESKey') + token = '' + corpid = '' + encoding_aes_key = '' + msg_signature = attrs.get('msg_signature') + echostr = attrs.get('echostr') + timestamp = attrs.get('timestamp') + nonce = attrs.get('nonce') + # 1)将token、timestamp、nonce, echostr四个参数进行字典序排序 + arr = [token, timestamp, nonce, echostr] + arr.sort() + # 2)将三个参数字符串拼接成一个字符串进行sha1加密 + data = "".join(arr) + # 3)开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信 + encode_str = sha1_encoder(data) + wxcpt = WXBizMsgCrypt(token, encoding_aes_key, corpid) + ret, echostr = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) + if ret != 0: + return {} + attrs['echostr'] = echostr.decode() + return attrs if encode_str == msg_signature else {} + + +class WechatEncryptSerializer(serializers.Serializer): + ToUserName = serializers.CharField() + Encrypt = serializers.CharField() + AgentID = serializers.CharField() + + def validate(self, attrs): + # encoding_aes_key = settings.WECHAT_WORKER.get('EncodingAESKey') + encoding_aes_key = '' + encrypt = attrs.get('Encrypt') + xmltext = self.decrypt(encrypt, encoding_aes_key) + data = self.parse_xml(xmltext) + print('data-->', data) + return attrs + + def create(self, data): + print(data) + + def decrypt(self, encrypt, encoding_key): + try: + + key = base64.b64decode(encoding_key + "=") + prpcrypt = Prpcrypt(key) + corpid = '' + res, decrypt = prpcrypt.decrypt(encrypt, corpid) + if res != 0: + return + decrypt = decrypt.decode() + return decrypt + except Exception as e: + print(e) + + def parse_xml(self, xmltext): + xml_tree = ET.fromstring(xmltext) + to_user_name = get_attribute(xml_tree.find("ToUserName"), 'text') + from_user_name = get_attribute(xml_tree.find("FromUserName"), 'text') + create_time = get_attribute(xml_tree.find("CreateTime"), 'text') + msg_type = get_attribute(xml_tree.find("MsgType"), 'text') + event = get_attribute(xml_tree.find("Event"), 'text') + change_type = get_attribute(xml_tree.find("ChangeType"), 'text') + user_id = get_attribute(xml_tree.find("UserID"), 'text') + external_user_id = get_attribute(xml_tree.find("ExternalUserID"), 'text') + state = get_attribute(xml_tree.find("State"), 'text') + welcome_code = get_attribute(xml_tree.find("WelcomeCode"), 'text') + return { + 'to_user_name': to_user_name, + 'from_user_name': from_user_name, + 'create_time': create_time, + 'msg_type': msg_type, + 'event': event, + 'change_type': change_type, + 'user_id': user_id, + 'external_user_id': external_user_id, + 'state': state, + 'welcome_code': welcome_code + } + + diff --git a/apps/jqr/urls.py b/apps/jqr/urls.py index 2018cd6..bedd4a5 100644 --- a/apps/jqr/urls.py +++ b/apps/jqr/urls.py @@ -2,5 +2,6 @@ from rest_framework.routers import SimpleRouter from . import views router = SimpleRouter(trailing_slash=False) +router.register('callback', views.WechatWorkerViewSet, basename='wechat-worker') urlpatterns = router.urls diff --git a/apps/jqr/views.py b/apps/jqr/views.py index e69de29..527c137 100644 --- a/apps/jqr/views.py +++ b/apps/jqr/views.py @@ -0,0 +1,35 @@ +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from apps.jqr.parsers import WechatContentParser +from apps.jqr.renderers import WechatPublicContentRenderer +from apps.jqr.serializers import WechatPublicTokenSerializer, WechatEncryptSerializer + + +class WechatWorkerViewSet(viewsets.GenericViewSet): + + @action(methods=['GET'], detail=False, serializer_class=WechatPublicTokenSerializer, url_path='event') + def verify(self, request): + print(self.request._request.path) + print(request.query_params) + serializer = self.get_serializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + enchostr = serializer.data.get('echostr') + return Response(data=int(enchostr)) + + @verify.mapping.post + def message(self, request): + serializer = WechatEncryptSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + return Response(data=serializer.data) + + def get_parsers(self): + if self.request.method == 'POST': + return [WechatContentParser()] + return super().get_parsers() + + def get_renderers(self): + if self.request.method == 'POST': + return [WechatPublicContentRenderer()] + return super().get_renderers() diff --git a/libs/weworkapi/README.md b/libs/weworkapi/README.md new file mode 100644 index 0000000..f0bf688 --- /dev/null +++ b/libs/weworkapi/README.md @@ -0,0 +1,46 @@ + +# About +weworkapi_python 是为了简化开发者对企业微信API接口的使用而设计的,API调用库系列之python版本     +本库仅做示范用,并不保证完全无bug; +作者会不定期更新本库,但不保证与官方API接口文档同步,因此一切以[官方文档](https://work.weixin.qq.com/api/doc)为准。 + +更多来自个人开发者的其它语言的库推荐: +python : https://github.com/sbzhu/weworkapi_python abelzhu@tencent.com(企业微信团队) +ruby : https://github.com/mycolorway/wework MyColorway(个人开发者) +php : https://github.com/sbzhu/weworkapi_php abelzhu@tencent.com(企业微信团队) +golang : https://github.com/sbzhu/weworkapi_golang ryanjelin@tencent.com(企业微信团队) +golang : https://github.com/doubliekill/EnterpriseWechatSDK 1006401052yh@gmail.com(个人开发者) + +# Director + +├── api // API 接口 +│   ├── examples // API接口的测试用例 +│   ├── README.md +│   └── src // API接口的关键逻辑 +├── conf.py +├── README.md + +# Usage +将本项目下载到你的目录,既可直接引用相关文件   +详细使用方法参考examples路径下的测试用例 + +# 关于token的缓存 +token是需要缓存的,不能每次调用都去获取token,[否则会中频率限制](https://work.weixin.qq.com/api/doc#10013/%E7%AC%AC%E5%9B%9B%E6%AD%A5%EF%BC%9A%E7%BC%93%E5%AD%98%E5%92%8C%E5%88%B7%E6%96%B0access_token) +在本库的设计里,token是以类里的一个变量缓存的 +比如api/src/CorpApi.py 里的access_token变量 +在类的生命周期里,这个accessToken都是存在的, 当且仅当发现token过期,CorpAPI类会自动刷新token +刷新机制在 api/src/AbstractApi.py +所以,使用时,只需要全局实例化一个CorpAPI类,不要析构它,就可一直用它调函数,不用关心 token +``` +api = CorpAPI(corpid, corpsecret); +api.dosomething() +api.dosomething() +api.dosomething() +.... +``` +当然,如果要更严格的做的话,建议自行修改,全局缓存token,比如存redis、存文件等,失效周期设置为2小时。 + +# Contact us +abelzhu@tencent.com + +# diff --git a/libs/weworkapi/api/examples/AppChatTest.py b/libs/weworkapi/api/examples/AppChatTest.py new file mode 100644 index 0000000..aed9add --- /dev/null +++ b/libs/weworkapi/api/examples/AppChatTest.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File UserTest.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +import sys +sys.path.append("../src/") + +import random + +from CorpApi import * +from TestConf import * + +## test +api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET']) + +chatid = "test210"; +try : +## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_CREATE'], + { + 'name' : 'appchat_test', + 'owner' : 'ZhuBiaoYi', + 'userlist' : ['LiShuang', 'ZhuShengBen', 'LinJianEn', 'ZhuBiaoYi', 'XuBin', 'yangpeiyi', 'HaLuoTeQu', 'lucky', 'raindong', 'simon', 'Wang', 'ZhaoDong', 'DengLinSheng', 'Li'], + 'chatid' : chatid, + }) + print response + chatid = response['chatid'] +except ApiException as e : + print e.errCode, e.errMsg + +try : + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_UPDATE'], + { + 'chatid' : chatid, + 'name' : 'appchat_test_new_name', + 'owner' : 'ZhuShengBen', + 'add_user_list' : ['huqiqi', 'Wang'] + }) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_UPDATE'], + { + 'chatid' : chatid, + 'name' : '应用发消息测试', + 'owner' : 'ZhuBiaoYi', + 'del_user_list' : 'huqiqi', + }) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + 'msgtype' : 'text', + 'text' : {'content':'我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党我是文本消息热爱祖国热爱人民热爱中国共产党'}, + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'safe' : 1, + }) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + 'msgtype' : 'image', + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'image' : { + 'media_id':'3A9Jo9CHit_5UTfOVE38_067dUJQlLs30mOa9FC0a4jEGeoQgpLCZgc7rEza6TbfB', + }, + 'safe' : 1, + }) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + 'msgtype' : 'file', + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'file' : { + 'media_id':'35L7MmcpGdyFfqjbGhbECCkGcaNsUajaPQifGLJq_H5E', + }, + 'safe' : 1, + }) + print response + + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'msgtype' : 'voice', + 'voice' : { + 'media_id':'3x1yb34061fDXjyUXy2rWNd-a-hWe-l8eTw2VKyh3bDQ', + }, + 'safe' : 1, + }) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'msgtype' : 'video', + 'video' : { + 'media_id':'3neA1ypnC3k5QnAZqvyVvCesFYUrXietU5F-Ipnj6ZobiD-PuFlXngzPplWXibw9r', + }, + 'safe' : 1, + }) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'msgtype' : 'news', + "news" : { + "articles" : [ + { + "title" : "图文消息", + "description" : "今年中秋节公司有豪礼相送", + "url" : "URL", + "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", + "btntxt":"更多", + }, + { + "title" : "图文消息", + "description" : "今年中秋节公司有豪礼相送", + "url" : "URL", + "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", + "btntxt":"更多", + }, + { + "title" : "图文消息", + "description" : "今年中秋节公司有豪礼相送", + "url" : "URL", + "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", + "btntxt":"更多", + }, + ]}, + 'safe' : 1, + }, + ) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + 'msgtype' : 'textcard', + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'textcard' : { + 'title':'我是文本卡片消息', + 'description' : 'aaaaaaa', + 'url' : 'www.qq.com', + 'btntxt' : '更多', + }, + 'safe' : 1, + }) + print response + + ## + response = api.httpCall( + CORP_API_TYPE['APP_CHAT_SEND'], + { + 'chatid':chatid, + "msgtype" : "mpnews", + "mpnews": { + "articles" : [ + { + "title" : "图文消息(mpnews)", + "thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc", + "author" : "author", + "content" : "content", + "digest" : "我是图文" + }, + { + "title" : "图文消息(mpnews)", + "thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc", + "author" : "author", + "content" : "content", + "digest" : "我是图文" + }, + { + "title" : "图文消息(mpnews)", + "thumb_media_id" : "3uFTZs4MRTr-OwUArqaoXPyqtuedcwCUW1x4sgKcOeQc", + "author" : "author", + "content" : "content", + "digest" : "我是图文" + }, + ] + }, + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'safe' : 1, + }) + print response + +except ApiException as e : + print e.errCode, e.errMsg diff --git a/libs/weworkapi/api/examples/MessageTest.py b/libs/weworkapi/api/examples/MessageTest.py new file mode 100644 index 0000000..c07b005 --- /dev/null +++ b/libs/weworkapi/api/examples/MessageTest.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File UserTest.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +import sys +sys.path.append("../src/") + +import random + +from CorpApi import * +from TestConf import * + +## test +api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET']) + +try : +## + response = api.httpCall( + CORP_API_TYPE['MESSAGE_SEND'], + { + "touser": "ZhuShengBen", + "agentid": 1000002, + 'msgtype' : 'text', + 'climsgid' : 'climsgidclimsgid_%f' % (random.random()), + 'text' : { + 'content':'方法论', + }, + 'safe' : 0, + }) + print response +except ApiException as e : + print e.errCode, e.errMsg + diff --git a/libs/weworkapi/api/examples/MiniprogramTest.py b/libs/weworkapi/api/examples/MiniprogramTest.py new file mode 100644 index 0000000..1ec96c8 --- /dev/null +++ b/libs/weworkapi/api/examples/MiniprogramTest.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File UserTest.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +import sys +sys.path.append("../src/") + +import random + +from CorpApi import * +from TestConf import * + +## test +api = CorpApi(TestConf['CORP_ID'], TestConf['APP_SECRET']) + +try : +## + response = api.httpCall( + CORP_API_TYPE['MINIPROGRAM_CODE_TO_SESSION_KEY'], + { + "js_code" : "sVqtL3itg0L30LTGJtZ_isKC0efG5FqGw470fVp8Dpw", + "grant_type" : "authorization_code" + }) + print response + +except ApiException as e : + print e.errCode, e.errMsg + diff --git a/libs/weworkapi/api/examples/ServiceCorpTest.py b/libs/weworkapi/api/examples/ServiceCorpTest.py new file mode 100644 index 0000000..f7d97ab --- /dev/null +++ b/libs/weworkapi/api/examples/ServiceCorpTest.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File ServiceCorpTest.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +import sys +sys.path.append("../src/") + +from ServiceCorpApi import * +from TestConf import * + + +## 第三方服务商接口的使用方法 +api = ServiceCorpApi( + "SUITE_ID", + "SUITE_SECRET", + "SUITE_TICKET" +); + +try : + pre_auth_code = api.httpCall(SERVICE_CORP_API_TYPE['GET_PRE_AUTH_CODE']).get('pre_auth_code') + print pre_auth_code +except ApiException as e : + print e.errCode, e.errMsg + + +## 第三方服务商使用永久授权码调用企业接口的方法 +api = ServiceCorpApi( + "SUITE_ID", + "SUITE_SECRET", + "SUITE_TICKET", + 'AUTH_CORPID', + 'PERMANENT_CODE' +); +try : + response = api.httpCall( + CORP_API_TYPE['USER_GET'], + { + 'userid' : 'zhangsan', + }) + print response +except ApiException as e : + print e.errCode, e.errMsg diff --git a/libs/weworkapi/api/examples/ServiceProviderTest.py b/libs/weworkapi/api/examples/ServiceProviderTest.py new file mode 100644 index 0000000..5f3f238 --- /dev/null +++ b/libs/weworkapi/api/examples/ServiceProviderTest.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File ServiceProviderTest.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-26 + # + # + +import sys +sys.path.append("../src/") + +from ServiceProviderApi import * +from TestConf import * + +api = ServiceProviderApi('CORPID', 'PROVIDER_SECRET') + +try : + response = api.httpCall( + SERVICE_PROVIDER_API_TYPE['GET_LOGIN_INFO'], + { + 'auth_code' : 'XXXXXXX', + }) + print response +except ApiException as e : + print e.errCode, e.errMsg diff --git a/libs/weworkapi/api/examples/TestConf.py b/libs/weworkapi/api/examples/TestConf.py new file mode 100644 index 0000000..9cb01af --- /dev/null +++ b/libs/weworkapi/api/examples/TestConf.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File conf.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +# 请将下面参数改为自己的企业相关参数再进行测试 + +TestConf = { + + # 企业的id,在管理端->"我的企业" 可以看到 + "CORP_ID" : "ww55ca070cb9b7eb22", + + # "通讯录同步"应用的secret, 开启api接口同步后,可以在管理端->"通讯录同步"看到 + "CONTACT_SYNC_SECRET" : "ktmzrVIlUH0UW63zi7-JyzsgTL9NfwUhHde6or6zwQY", + + # 某个自建应用的id及secret, 在管理端 -> 企业应用 -> 自建应用, 点进相应应用可以看到 + "APP_ID" : 1000002, + "APP_SECRET" : "v1Z2KSw2WqPFECAwn2R0a1dFsanVF5sE4IE6X5ogveQ", + + # 打卡应用的 id 及secrete, 在管理端 -> 企业应用 -> 基础应用 -> 打卡, + # 点进去,有个"api"按钮,点开后,会看到 + "CHECKIN_APP_ID" : 3010011, + "CHECKIN_APP_SECRET" : "3Qz2OGPvE1Eb6WKpEDfczvyQjL5Lr1CjrDTKn0RHdLE", + + # 审批应用的 id 及secrete, 在管理端 -> 企业应用 -> 基础应用 -> 审批, + # 点进去,有个"api"按钮,点开后,会看到 + "APPROVAL_APP_ID" : 3010040, + "APPROVAL_APP_SECRET" : "1vrlwItWpz_5Qkud55aImQPCvpzi51H3F2j-1OQzhYE", +} diff --git a/libs/weworkapi/api/examples/UserTest.py b/libs/weworkapi/api/examples/UserTest.py new file mode 100644 index 0000000..4a29bdc --- /dev/null +++ b/libs/weworkapi/api/examples/UserTest.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File UserTest.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +import sys +sys.path.append("../src/") + +from exapmples.CorpApi import * +from TestConf import * + +## test +api = CorpApi(TestConf['CORP_ID'], TestConf['CONTACT_SYNC_SECRET']) + +try : + ## + response = api.httpCall( + CORP_API_TYPE['USER_CREATE'], + { + 'userid' : 'zhangsan', + 'name' : 'zhangsanfeng', + 'mobile' : '131488888888', + 'email' : 'zhangsan@ipp.cas.cn', + 'department' : 1, + }) + print(response) + + ## + response = api.httpCall( + CORP_API_TYPE['USER_GET'], + { + 'userid' : 'zhangsan', + }) + print(response) + + ## + response = api.httpCall( + CORP_API_TYPE['USER_DELETE'], + { + 'userid' : 'zhangsan', + }) + print(response) + +except ApiException as e : + print e.errCode, e.errMsg + + ## + response = api.httpCall( + CORP_API_TYPE['USER_DELETE'], + { + 'userid' : 'zhangsan', + }) + print(response) + + diff --git a/libs/weworkapi/api/src/AbstractApi.py b/libs/weworkapi/api/src/AbstractApi.py new file mode 100644 index 0000000..9c15796 --- /dev/null +++ b/libs/weworkapi/api/src/AbstractApi.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File AbstractApi.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +import sys +import os +import re + +import json +import requests + +sys.path.append("../../") + +from conf import DEBUG + +class ApiException(Exception) : + def __init__(self, errCode, errMsg) : + self.errCode = errCode + self.errMsg = errMsg + +class AbstractApi(object) : + def __init__(self) : + return + + def getAccessToken(self) : + raise NotImplementedError + def refreshAccessToken(self) : + raise NotImplementedError + + def getSuiteAccessToken(self) : + raise NotImplementedError + def refreshSuiteAccessToken(self) : + raise NotImplementedError + + def getProviderAccessToken(self) : + raise NotImplementedError + def refreshProviderAccessToken(self) : + raise NotImplementedError + + def httpCall(self, urlType, args=None) : + shortUrl = urlType[0] + method = urlType[1] + response = {} + for retryCnt in range(0, 3) : + if 'POST' == method : + url = self.__makeUrl(shortUrl) + response = self.__httpPost(url, args) + elif 'GET' == method : + url = self.__makeUrl(shortUrl) + url = self.__appendArgs(url, args) + response = self.__httpGet(url) + else : + raise ApiException(-1, "unknown method type") + + # check if token expired + if self.__tokenExpired(response.get('errcode')) : + self.__refreshToken(shortUrl) + retryCnt += 1 + continue + else : + break + + return self.__checkResponse(response) + + @staticmethod + def __appendArgs(url, args) : + if args is None : + return url + + for key, value in args.items() : + if '?' in url : + url += ('&' + key + '=' + value) + else : + url += ('?' + key + '=' + value) + return url + + @staticmethod + def __makeUrl(shortUrl) : + base = "https://qyapi.weixin.qq.com" + if shortUrl[0] == '/' : + return base + shortUrl + else : + return base + '/' + shortUrl + + def __appendToken(self, url) : + if 'SUITE_ACCESS_TOKEN' in url : + return url.replace('SUITE_ACCESS_TOKEN', self.getSuiteAccessToken()) + elif 'PROVIDER_ACCESS_TOKEN' in url : + return url.replace('PROVIDER_ACCESS_TOKEN', self.getProviderAccessToken()) + elif 'ACCESS_TOKEN' in url : + return url.replace('ACCESS_TOKEN', self.getAccessToken()) + else : + return url + + def __httpPost(self, url, args) : + realUrl = self.__appendToken(url) + + if DEBUG is True : + print realUrl, args + + return requests.post(realUrl, data = json.dumps(args, ensure_ascii = False).encode('utf-8')).json() + + def __httpGet(self, url) : + realUrl = self.__appendToken(url) + + if DEBUG is True : + print realUrl + + return requests.get(realUrl).json() + + def __post_file(self, url, media_file): + return requests.post(url, file=media_file).json() + + @staticmethod + def __checkResponse(response): + errCode = response.get('errcode') + errMsg = response.get('errmsg') + + if errCode is 0: + return response + else: + raise ApiException(errCode, errMsg) + + @staticmethod + def __tokenExpired(errCode) : + if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009 : + return True + else : + return False + + def __refreshToken(self, url) : + if 'SUITE_ACCESS_TOKEN' in url : + self.refreshSuiteAccessToken() + elif 'PROVIDER_ACCESS_TOKEN' in url : + self.refreshProviderAccessToken() + elif 'ACCESS_TOKEN' in url : + self.refreshAccessToken() diff --git a/libs/weworkapi/api/src/CorpApi.py b/libs/weworkapi/api/src/CorpApi.py new file mode 100644 index 0000000..4f04a23 --- /dev/null +++ b/libs/weworkapi/api/src/CorpApi.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File CorpApi.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +from AbstractApi import * + +CORP_API_TYPE = { + 'GET_ACCESS_TOKEN' : ['/cgi-bin/gettoken', 'GET'], + 'USER_CREATE' : ['/cgi-bin/user/create?access_token=ACCESS_TOKEN', 'POST'], + 'USER_GET' : ['/cgi-bin/user/get?access_token=ACCESS_TOKEN', 'GET'], + 'USER_UPDATE' : ['/cgi-bin/user/update?access_token=ACCESS_TOKEN', 'POST'], + 'USER_DELETE' : ['/cgi-bin/user/delete?access_token=ACCESS_TOKEN', 'GET'], + 'USER_BATCH_DELETE': ['/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN', 'POST'], + 'USER_SIMPLE_LIST': ['/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN', 'GET'], + 'USER_LIST' : ['/cgi-bin/user/list?access_token=ACCESS_TOKEN', 'GET'], + 'USERID_TO_OPENID' : ['/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN', 'POST'], + 'OPENID_TO_USERID' : ['/cgi-bin/user/convert_to_userid?access_token=ACCESS_TOKEN', 'POST'], + 'USER_AUTH_SUCCESS': ['/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN', 'GET'], + + 'DEPARTMENT_CREATE': ['/cgi-bin/department/create?access_token=ACCESS_TOKEN', 'POST'], + 'DEPARTMENT_UPDATE': ['/cgi-bin/department/update?access_token=ACCESS_TOKEN', 'POST'], + 'DEPARTMENT_DELETE': ['/cgi-bin/department/delete?access_token=ACCESS_TOKEN', 'GET'], + 'DEPARTMENT_LIST' : ['/cgi-bin/department/list?access_token=ACCESS_TOKEN', 'GET'], + + 'TAG_CREATE' : ['/cgi-bin/tag/create?access_token=ACCESS_TOKEN', 'POST'], + 'TAG_UPDATE' : ['/cgi-bin/tag/update?access_token=ACCESS_TOKEN', 'POST'], + 'TAG_DELETE' : ['/cgi-bin/tag/delete?access_token=ACCESS_TOKEN', 'GET'], + 'TAG_GET_USER' : ['/cgi-bin/tag/get?access_token=ACCESS_TOKEN', 'GET'], + 'TAG_ADD_USER' : ['/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN', 'POST'], + 'TAG_DELETE_USER' : ['/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN', 'POST'], + 'TAG_GET_LIST' : ['/cgi-bin/tag/list?access_token=ACCESS_TOKEN', 'GET'], + + 'BATCH_JOB_GET_RESULT' : ['/cgi-bin/batch/getresult?access_token=ACCESS_TOKEN', 'GET'], + + 'BATCH_INVITE' : ['/cgi-bin/batch/invite?access_token=ACCESS_TOKEN', 'POST'], + + 'AGENT_GET' : ['/cgi-bin/agent/get?access_token=ACCESS_TOKEN', 'GET'], + 'AGENT_SET' : ['/cgi-bin/agent/set?access_token=ACCESS_TOKEN', 'POST'], + 'AGENT_GET_LIST' : ['/cgi-bin/agent/list?access_token=ACCESS_TOKEN', 'GET'], + + 'MENU_CREATE' : ['/cgi-bin/menu/create?access_token=ACCESS_TOKEN', 'POST'], ## TODO + 'MENU_GET' : ['/cgi-bin/menu/get?access_token=ACCESS_TOKEN', 'GET'], + 'MENU_DELETE' : ['/cgi-bin/menu/delete?access_token=ACCESS_TOKEN', 'GET'], + + 'MESSAGE_SEND' : ['/cgi-bin/message/send?access_token=ACCESS_TOKEN', 'POST'], + 'MESSAGE_REVOKE' : ['/cgi-bin/message/revoke?access_token=ACCESS_TOKEN', 'POST'], + + 'MEDIA_GET' : ['/cgi-bin/media/get?access_token=ACCESS_TOKEN', 'GET'], + + 'GET_USER_INFO_BY_CODE' : ['/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN', 'GET'], + 'GET_USER_DETAIL' : ['/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN', 'POST'], + + 'GET_TICKET' : ['/cgi-bin/ticket/get?access_token=ACCESS_TOKEN', 'GET'], + 'GET_JSAPI_TICKET' : ['/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN', 'GET'], + + 'GET_CHECKIN_OPTION' : ['/cgi-bin/checkin/getcheckinoption?access_token=ACCESS_TOKEN', 'POST'], + 'GET_CHECKIN_DATA' : ['/cgi-bin/checkin/getcheckindata?access_token=ACCESS_TOKEN', 'POST'], + 'GET_APPROVAL_DATA': ['/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN', 'POST'], + + 'GET_INVOICE_INFO' : ['/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=ACCESS_TOKEN', 'POST'], + 'UPDATE_INVOICE_STATUS' : + ['/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=ACCESS_TOKEN', 'POST'], + 'BATCH_UPDATE_INVOICE_STATUS' : + ['/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=ACCESS_TOKEN', 'POST'], + 'BATCH_GET_INVOICE_INFO' : + ['/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=ACCESS_TOKEN', 'POST'], + + 'APP_CHAT_CREATE' : ['/cgi-bin/appchat/create?access_token=ACCESS_TOKEN', 'POST'], + 'APP_CHAT_GET' : ['/cgi-bin/appchat/get?access_token=ACCESS_TOKEN', 'GET'], + 'APP_CHAT_UPDATE' : ['/cgi-bin/appchat/update?access_token=ACCESS_TOKEN', 'POST'], + 'APP_CHAT_SEND' : ['/cgi-bin/appchat/send?access_token=ACCESS_TOKEN', 'POST'], + + 'MINIPROGRAM_CODE_TO_SESSION_KEY' : ['/cgi-bin/miniprogram/jscode2session?access_token=ACCESS_TOKEN', 'GET'], +} + +class CorpApi(AbstractApi) : + def __init__(self, corpid, secret) : + self.corpid = corpid + self.secret = secret + self.access_token = None + + def getAccessToken(self) : + if self.access_token is None : + self.refreshAccessToken() + return self.access_token + + def refreshAccessToken(self) : + response = self.httpCall( + CORP_API_TYPE['GET_ACCESS_TOKEN'], + { + 'corpid' : self.corpid, + 'corpsecret': self.secret, + }) + self.access_token = response.get('access_token') + diff --git a/libs/weworkapi/api/src/ServiceCorpApi.py b/libs/weworkapi/api/src/ServiceCorpApi.py new file mode 100644 index 0000000..a700e43 --- /dev/null +++ b/libs/weworkapi/api/src/ServiceCorpApi.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File ServiceCorp.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-24 + # + # + +from CorpApi import * + +SERVICE_CORP_API_TYPE = { + 'GET_CORP_TOKEN' : ['/cgi-bin/service/get_corp_token?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], + 'GET_SUITE_TOKEN' : ['/cgi-bin/service/get_suite_token', 'POST'], + 'GET_PRE_AUTH_CODE' : ['/cgi-bin/service/get_pre_auth_code?suite_access_token=SUITE_ACCESS_TOKEN', 'GET'], + 'SET_SESSION_INFO' : ['/cgi-bin/service/set_session_info?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], + 'GET_PERMANENT_CODE': ['/cgi-bin/service/get_permanent_code?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], + 'GET_AUTH_INFO' : ['/cgi-bin/service/get_auth_info?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], + 'GET_ADMIN_LIST' : ['/cgi-bin/service/get_admin_list?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], + 'GET_USER_INFO_BY_3RD' : ['/cgi-bin/service/getuserinfo3rd?suite_access_token=SUITE_ACCESS_TOKEN', 'GET'], + 'GET_USER_DETAIL_BY_3RD' : ['/cgi-bin/service/getuserdetail3rd?suite_access_token=SUITE_ACCESS_TOKEN', 'POST'], +} + +class ServiceCorpApi(CorpApi) : + def __init__(self, suite_id, suite_secret, suite_ticket, auth_corpid=None, permanent_code=None) : + self.suite_id = suite_id + self.suite_secret = suite_secret + self.suite_ticket = suite_ticket + + # 调用 CorpAPI 的function, 需要设置这两个参数 + self.auth_corpid = auth_corpid + self.permanent_code = permanent_code + + self.access_token = None + self.suite_access_token = None + + ## override CorpApi 的 refreshAccessToken, 使用第三方服务商的方法 + def getAccessToken(self) : + if self.access_token is None : + self.refreshAccessToken() + return self.access_token + def refreshAccessToken(self) : + response = self.httpCall( + SERVICE_CORP_API_TYPE['GET_CORP_TOKEN'], + { + "auth_corpid" : self.auth_corpid, + "permanent_code": self.permanent_code, + }) + self.access_token = response.get('access_token') + + ## + def getSuiteAccessToken(self) : + if self.suite_access_token is None : + self.refreshSuiteAccessToken() + return self.suite_access_token + + def refreshSuiteAccessToken(self) : + response = self.httpCall( + SERVICE_CORP_API_TYPE['GET_SUITE_TOKEN'], + { + "suite_id" : self.suite_id, + "suite_secret" : self.suite_secret, + "suite_ticket" : self.suite_ticket, + }) + self.suite_access_token= response.get('suite_access_token') + diff --git a/libs/weworkapi/api/src/ServiceProviderApi.py b/libs/weworkapi/api/src/ServiceProviderApi.py new file mode 100644 index 0000000..2026714 --- /dev/null +++ b/libs/weworkapi/api/src/ServiceProviderApi.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File ServiceProviderApi.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-26 + # + # + +from AbstractApi import * + +SERVICE_PROVIDER_API_TYPE = { + 'GET_PROVIDER_TOKEN': ['/cgi-bin/service/get_provider_token', 'POST'], + 'GET_LOGIN_INFO' : ['/cgi-bin/service/get_login_info?access_token=PROVIDER_ACCESS_TOKEN', 'POST'], + 'GET_REGISTER_CODE' : ['/cgi-bin/service/get_register_code?provider_access_token=PROVIDER_ACCESS_TOKEN', 'POST'], + 'GET_REGISTER_INFO' : ['/cgi-bin/service/get_register_info?provider_access_token=PROVIDER_ACCESS_TOKEN', 'POST'], + 'SET_AGENT_SCOPE' : ['/cgi-bin/agent/set_scope', 'POST'], ### TODO + 'SET_CONTACT_SYNC_SUCCESS' : ['/cgi-bin/sync/contact_sync_success', 'GET'], +} + +class ServiceProviderApi(AbstractApi) : + def __init__(self, corpid, provider_secret) : + self.corpid = corpid + self.provider_secret = provider_secret + + self.provider_access_token = None + + def getProviderAccessToken(self) : + if self.provider_access_token is None : + self.refreshProviderAccessToken() + return self.provider_access_token + + def refreshProviderAccessToken(self) : + response = self.httpCall( + SERVICE_PROVIDER_API_TYPE['GET_PROVIDER_TOKEN'], + { + 'corpid' : self.corpid, + 'provider_secret': self.provider_secret, + }) + self.provider_access_token = response.get('provider_access_token') + diff --git a/libs/weworkapi/callback/Readme.txt b/libs/weworkapi/callback/Readme.txt new file mode 100644 index 0000000..eea5157 --- /dev/null +++ b/libs/weworkapi/callback/Readme.txt @@ -0,0 +1,5 @@ +ע +1.WXBizMsgCrypt.pyļװWXBizMsgCryptӿ࣬ṩûҵ΢ŵӿڣSample.pyļṩʹӿڵʾierror.pyṩ˴롣 +2.WXBizMsgCryptװVerifyURL, DecryptMsg, EncryptMsgӿڣֱڿ֤صurlյûظϢĽԼ߻ظϢļ̡ܹʹ÷ԲοSample.pyļ +3.ӽЭοҵ΢Źٷĵ +4.õpycrypto⣬뿪аװ˿ʹá \ No newline at end of file diff --git a/libs/weworkapi/callback/Sample.py b/libs/weworkapi/callback/Sample.py new file mode 100644 index 0000000..a617e94 --- /dev/null +++ b/libs/weworkapi/callback/Sample.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +######################################################################### +# Author: jonyqin +# Created Time: Thu 11 Sep 2014 03:55:41 PM CST +# File Name: Sample.py +# Description: WXBizMsgCrypt 使用demo文件 +######################################################################### +from WXBizMsgCrypt3 import WXBizMsgCrypt +import xml.etree.cElementTree as ET +import sys + +if __name__ == "__main__": + #假设企业在企业微信后台上设置的参数如下 + sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo" + sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt" + sCorpID = "ww1436e0e65a779aee" + ''' + ------------使用示例一:验证回调URL--------------- + *企业开启回调模式时,企业号会向验证url发送一个get请求 + 假设点击验证时,企业收到类似请求: + * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=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 ret + sVerifyMsgSig="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×tamp=1409659813&nonce=1372623149 HTTP/1.1 + Host: qy.weixin.qq.com + Content-Length: 613 + + + + + 企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce) + 2.验证消息体签名的正确性。 3.将post请求的数据进行xml解析,并将标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档 + 第2,3步可以用企业微信提供的库函数DecryptMsg来实现。 + ''' + # sReqMsgSig = HttpUtils.ParseUrl("msg_signature") + sReqMsgSig = "0c3914025cb4b4d68103f6bfc8db550f79dcf48e" + sReqTimeStamp = "1476422779" + sReqNonce = "1597212914" + sReqData = "\n\n\n" + ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce) + print ret,sMsg + if( 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").text + print content + # ... + # ... + + ''' + ------------使用示例三:企业回复用户消息的加密--------------- + 企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的xml串。 + 假设企业需要回复用户的明文如下: + + + + 1348831860 + + + 1234567890123456 + 128 + + + 为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。 + 2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业号。 + 以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。 + ''' + sRespData = "ww1436e0e65a779aeeChenJiaShun1476422779text你好14564537201000002" + 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) diff --git a/libs/weworkapi/callback/WXBizMsgCrypt3.py b/libs/weworkapi/callback/WXBizMsgCrypt3.py new file mode 100644 index 0000000..9cf8cc2 --- /dev/null +++ b/libs/weworkapi/callback/WXBizMsgCrypt3.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# -*- encoding:utf-8 -*- + +""" 对企业微信发送给企业后台的消息加解密示例代码. +@copyright: Copyright (c) 1998-2014 Tencent Inc. + +""" +# ------------------------------------------------------------------------ +import logging +import base64 +import random +import hashlib +import time +import struct +from Crypto.Cipher import AES +import xml.etree.cElementTree as ET +import socket + +from . import ierror + + +""" +关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 +请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 +下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 +""" + + +class FormatException(Exception): + pass + + +def 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, None + + +class XMLParse: + """提供提取消息格式中的密文及生成回复消息格式的接口""" + + # xml消息模板 + AES_TEXT_RESPONSE_TEMPLATE = """ + + +%(timestamp)s + +""" + + 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.text + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_ParseXml_Error, None + + def 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_dict + return resp_xml + + +class PKCS7Encoder(): + """提供基于PKCS7算法的加解密接口""" + + block_size = 32 + + def 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 = 0 + return decrypted[:-pad] + + +class Prpcrypt(object): + """提供接收和推送给企业微信消息的加解密接口""" + + def __init__(self, key): + + # self.key = base64.b64decode(key+"=") + self.key = key + # 设置加解密模式为AES的CBC模式 + self.mode = AES.MODE_CBC + + def 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, None + + def 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, None + try: + 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, None + print('from_receiveid -->', from_receiveid) + print('receiveid -->', receiveid) + # if from_receiveid.decode('utf8') != receiveid: + # return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None + return 0, xml_content + + def 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) == 32 + except: + throw_exception("[error]: EncodingAESKey unvalid !", FormatException) + # return ierror.WXBizMsgCrypt_IllegalAesKey,None + self.m_sToken = sToken + self.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, None + if not signature == sMsgSignature: + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId) + return ret, sReplyEchoStr + + def 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,失败返回对应的错误码None + pc = Prpcrypt(self.key) + ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) + encrypt = encrypt.decode('utf8') + if ret != 0: + return ret, None + if 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, None + xmlParse = 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, None + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId) + return ret, xml_content diff --git a/libs/weworkapi/callback/ierror.py b/libs/weworkapi/callback/ierror.py new file mode 100644 index 0000000..6678fec --- /dev/null +++ b/libs/weworkapi/callback/ierror.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +######################################################################### +# Author: jonyqin +# Created Time: Thu 11 Sep 2014 01:53:58 PM CST +# File Name: ierror.py +# Description:定义错误码含义 +######################################################################### +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 diff --git a/libs/weworkapi/callback_json/Readme.txt b/libs/weworkapi/callback_json/Readme.txt new file mode 100644 index 0000000..eea5157 --- /dev/null +++ b/libs/weworkapi/callback_json/Readme.txt @@ -0,0 +1,5 @@ +ע +1.WXBizMsgCrypt.pyļװWXBizMsgCryptӿ࣬ṩûҵ΢ŵӿڣSample.pyļṩʹӿڵʾierror.pyṩ˴롣 +2.WXBizMsgCryptװVerifyURL, DecryptMsg, EncryptMsgӿڣֱڿ֤صurlյûظϢĽԼ߻ظϢļ̡ܹʹ÷ԲοSample.pyļ +3.ӽЭοҵ΢Źٷĵ +4.õpycrypto⣬뿪аװ˿ʹá \ No newline at end of file diff --git a/libs/weworkapi/callback_json/Sample.py b/libs/weworkapi/callback_json/Sample.py new file mode 100644 index 0000000..661f697 --- /dev/null +++ b/libs/weworkapi/callback_json/Sample.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +######################################################################### +# Author: jonyqin +# Created Time: Thu 11 Sep 2014 03:55:41 PM CST +# File Name: Sample.py +# Description: WXBizJsonMsgCrypt 使用demo文件 +######################################################################### +from WXBizJsonMsgCrypt import WXBizJsonMsgCrypt +import sys + +if __name__ == "__main__": + #假设企业在企业微信后台上设置的参数如下 + sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo" + sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt" + sCorpID = "ww1436e0e65a779aee" + ''' + ------------使用示例一:验证回调URL--------------- + *企业开启回调模式时,企业号会向验证url发送一个get请求 + 假设点击验证时,企业收到类似请求: + * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=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=WXBizJsonMsgCrypt(sToken,sEncodingAESKey,sCorpID) + sVerifyMsgSig="012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb" + sVerifyTimeStamp="1476416373" + sVerifyNonce="47744683" + 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) + else: + print "done VerifyURL" + #验证URL成功,将sEchoStr返回给企业号 + + print "==============================" + ''' + ------------使用示例二:对用户回复的消息解密--------------- + 用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档 + 假设企业收到企业微信的回调消息如下: + POST /cgi-bin/wxpush? msg_signature=e3647471e395139e2308c1fa963f2d648a00b90e×tamp=1409659813&nonce=1372623149 HTTP/1.1 + Host: qy.weixin.qq.com + + { + "tousername": "wx5823bf96d3bd56c7", + "encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=", + "agentid": 218 + } + + 企业收到post请求之后应该 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce) + 2.验证消息体签名的正确性。 3.将post请求的数据进行json解析,并将"encrypt"标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档 + 第2,3步可以用企业微信提供的库函数DecryptMsg来实现。 + ''' + + sReqNonce = "1372623149" + sReqTimeStamp = "1409659813" + + sReqMsgSig = "e3647471e395139e2308c1fa963f2d648a00b90e" + sReqData = '{ "tousername": "wx5823bf96d3bd56c7", "encrypt": "cjhLUX7UU4yCSelv1vz7T0zT8huF51bAMVWriNvO1FMegHrQZNrtvRxbwf0fUPsFvwqR0U0fgiJNEA5Y30F2MoI2S7vv3EjVQ68C0cjw9frBoUE2Hj0BvFp9h3u6Vbsg4lc1C8AtHdaN8orKuNKkLRLuYEL52R1J3v8olJGZRLnRdVKIivixmX/eQpzgeExtp20jI1HxRP1AAZ6xZoILdqDPO549LO4WeG+685JRUTdiwcY5fjZlqeMxuT4PpMn1X9OWsS7NRj06Wa5E3Tvg4twjWp39KPfOdRte6P1T4JU=", "agentid": 218 }'; + ret,sMsg=wxcpt.DecryptMsg( sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce) + if( ret!=0 ): + print "ERR: DecryptMsg ret: " + str(ret) + sys.exit(1) + else: + print sMsg + # 解密成功,sMsg即为json格式的明文 + # TODO: 对明文的处理 + # ... + # ... + + print "==============================" + + ''' + ------------使用示例三:企业回复用户消息的加密--------------- + 企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的json串。 + 假设企业需要回复用户的明文如下: + + { + "ToUserName": "mycreate", + "FromUserName":"wx5823bf96d3bd56c7", + "CreateTime": 1348831860, + "MsgType": "text", + "Content": "this is a test", + "MsgId": 1234567890123456, + "AgentID": 128 + } + + 为了将此段明文回复给用户,企业应: 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。 + 2.将明文加密得到密文。 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。 4.将密文,消息体签名,时间戳,随机数字串拼接成json格式的字符串,发送给企业号。 + 以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。 + ''' + #sRespData = ' { "ToUserName": "mycreate", "FromUserName":"wx5823bf96d3bd56c7", "CreateTime": 1348831860, "MsgType": "text", "Content": "this is a test", "MsgId": 1234567890123456, "AgentID": 128 }'; + sRespData = '{ "ToUserName": "wx5823bf96d3bd56c7", "FromUserName": :mycreate", "CreateTime": 1409659813, "MsgType": "text", "Content": "hello", "MsgId": 4561255354251345929, "AgentID": 218}' + ret,sEncryptMsg=wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp) + if( ret!=0 ): + print "ERR: EncryptMsg ret: " + str(ret) + sys.exit(1) + else: + print sEncryptMsg + #ret == 0 加密成功,企业需要将sEncryptMsg返回给企业号 + print "==============================" diff --git a/libs/weworkapi/callback_json/WXBizJsonMsgCrypt.py b/libs/weworkapi/callback_json/WXBizJsonMsgCrypt.py new file mode 100644 index 0000000..c6834e7 --- /dev/null +++ b/libs/weworkapi/callback_json/WXBizJsonMsgCrypt.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python +#-*- encoding:utf-8 -*- + +""" 对企业微信发送给企业后台的消息加解密示例代码. +@copyright: Copyright (c) 1998-2020 Tencent Inc. + +""" +# ------------------------------------------------------------------------ + +import base64 +import string +import random +import hashlib +import time +import struct +from Crypto.Cipher import AES +import sys +import socket +import json + +reload(sys) +import ierror +sys.setdefaultencoding('utf-8') + +""" +关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 +请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 +下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 +""" +class FormatException(Exception): + pass + +def 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)) + return ierror.WXBizMsgCrypt_OK, sha.hexdigest() + except Exception,e: + print e + return ierror.WXBizMsgCrypt_ComputeSignature_Error, None + + +class JsonParse: + """提供提取消息格式中的密文及生成回复消息格式的接口""" + + # json消息模板 + AES_TEXT_RESPONSE_TEMPLATE = '''{ + "encrypt": "%(msg_encrypt)s", + "msgsignature": "%(msg_signaturet)s", + "timestamp": "%(timestamp)s", + "nonce": "%(nonce)s" + }''' + + def extract(self, jsontext): + """提取出json数据包中的加密消息 + @param jsontext: 待提取的json字符串 + @return: 提取出的加密消息字符串 + """ + try: + json_dict = json.loads(jsontext) + return ierror.WXBizMsgCrypt_OK, json_dict['encrypt'] + except Exception,e: + print e + return ierror.WXBizMsgCrypt_ParseJson_Error, None + def generate(self, encrypt, signature, timestamp, nonce): + """生成json消息 + @param encrypt: 加密后的消息密文 + @param signature: 安全签名 + @param timestamp: 时间戳 + @param nonce: 随机字符串 + @return: 生成的json字符串 + """ + resp_dict = { + 'msg_encrypt' : encrypt, + 'msg_signaturet': signature, + 'timestamp' : timestamp, + 'nonce' : nonce, + } + resp_json = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict + return resp_json + + +class PKCS7Encoder(): + """提供基于PKCS7算法的加解密接口""" + + block_size = 32 + def 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 + + def decode(self, decrypted): + """删除解密后明文的补位字符 + @param decrypted: 解密后的明文 + @return: 删除补位字符后的明文 + """ + pad = ord(decrypted[-1]) + if pad<1 or pad >32: + pad = 0 + return decrypted[:-pad] + + +class Prpcrypt(object): + """提供接收和推送给企业微信消息的加解密接口""" + + def __init__(self,key): + + #self.key = base64.b64decode(key+"=") + self.key = key + # 设置加解密模式为AES的CBC模式 + self.mode = AES.MODE_CBC + + + def encrypt(self,text,receiveid): + """对明文进行加密 + @param text: 需要加密的明文 + @return: 加密得到的字符串 + """ + # 16位随机字符串添加到明文开头 + text = self.get_random_str() + struct.pack("I",socket.htonl(len(text))) + text + receiveid + # 使用自定义的填充方式对明文进行补位填充 + 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,e: + print e + return ierror.WXBizMsgCrypt_EncryptAES_Error,None + + def 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,e: + print e + return ierror.WXBizMsgCrypt_DecryptAES_Error,None + try: + pad = ord(plain_text[-1]) + # 去掉补位字符串 + #pkcs7 = PKCS7Encoder() + #plain_text = pkcs7.encode(plain_text) + # 去除16位随机字符串 + content = plain_text[16:-pad] + json_len = socket.ntohl(struct.unpack("I",content[ : 4])[0]) + json_content = content[4 : json_len+4] + from_receiveid = content[json_len+4:] + except Exception,e: + print e + return ierror.WXBizMsgCrypt_IllegalBuffer,None + if from_receiveid != receiveid: + print "receiveid not match" + print from_receiveid + return ierror.WXBizMsgCrypt_ValidateCorpid_Error,None + return 0,json_content + + def get_random_str(self): + """ 随机生成16位字符串 + @return: 16位字符串 + """ + rule = string.letters + string.digits + str = random.sample(rule, 16) + return "".join(str) + +class WXBizJsonMsgCrypt(object): + #构造函数 + def __init__(self,sToken,sEncodingAESKey,sReceiveId): + try: + self.key = base64.b64decode(sEncodingAESKey+"=") + assert len(self.key) == 32 + except: + throw_exception("[error]: EncodingAESKey unvalid !", FormatException) + # return ierror.WXBizMsgCrypt_IllegalAesKey,None + self.m_sToken = sToken + self.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, None + if not signature == sMsgSignature: + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sReceiveId) + return ret,sReplyEchoStr + + def EncryptMsg(self, sReplyMsg, sNonce, timestamp = None): + #将企业回复用户的消息加密打包 + #@param sReplyMsg: 企业号待回复用户的消息,json格式的字符串 + #@param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 + #@param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce + #sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串, + #return:成功0,sEncryptMsg,失败返回对应的错误码None + pc = Prpcrypt(self.key) + ret,encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) + if ret != 0: + return ret,None + if 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,None + jsonParse = JsonParse() + return ret,jsonParse.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请求的数据 + # json_content: 解密后的原文,当return返回0时有效 + # @return: 成功0,失败返回对应的错误码 + # 验证安全签名 + jsonParse = JsonParse() + ret,encrypt = jsonParse.extract(sPostData) + if ret != 0: + return ret, None + sha1 = SHA1() + ret,signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + print "signature not match" + print signature + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret,json_content = pc.decrypt(encrypt,self.m_sReceiveId) + return ret,json_content + + diff --git a/libs/weworkapi/callback_json/ierror.py b/libs/weworkapi/callback_json/ierror.py new file mode 100644 index 0000000..e1f16f4 --- /dev/null +++ b/libs/weworkapi/callback_json/ierror.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +######################################################################### +# Author: jonyqin +# Created Time: Thu 11 Sep 2014 01:53:58 PM CST +# File Name: ierror.py +# Description:定义错误码含义 +######################################################################### +WXBizMsgCrypt_OK = 0 +WXBizMsgCrypt_ValidateSignature_Error = -40001 +WXBizMsgCrypt_ParseJson_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_GenReturnJson_Error = -40011 diff --git a/libs/weworkapi/conf.py b/libs/weworkapi/conf.py new file mode 100644 index 0000000..c37571d --- /dev/null +++ b/libs/weworkapi/conf.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +## + # Copyright (C) 2018 All rights reserved. + # + # @File conf.py + # @Brief + # @Author abelzhu, abelzhu@tencent.com + # @Version 1.0 + # @Date 2018-02-23 + # + # + +## 设置为true会打印一些调试信息 +DEBUG = True + diff --git a/requirements.txt b/requirements.txt index daa4551..d49f746 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/utils/tools.py b/utils/tools.py index 8f293a7..0f91f0c 100644 --- a/utils/tools.py +++ b/utils/tools.py @@ -1,4 +1,5 @@ import datetime +import hashlib import random import re from hashlib import md5 @@ -241,3 +242,15 @@ def generate_phone_code(length=6) -> str: """ 生成手机验证码 """ number_list = range(0, 10) return ''.join(map(lambda i: str(i), random.choices(number_list, k=length))) + + +def sha1_encoder(data): + sha1 = hashlib.sha1() + sha1.update(data.encode('utf-8')) + return sha1.hexdigest() + + +def get_attribute(obj, field): + if obj is None: + return + return getattr(obj, field)