From f26438128a4ef57e847e2e76527fdb7431e95590 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=A8=BE=E6=98=8E=E5=8D=8E?= <565209960@qq.com>
Date: Mon, 19 Dec 2016 17:07:56 +0800
Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=88=9D=E5=A7=8B=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
authentication/Sample.py | 34 +++++
authentication/WXBizMsgCrypt.py | 263 ++++++++++++++++++++++++++++++++
authentication/__init__.py | 0
authentication/ierror.py | 20 +++
wxapp/__init__.py | 0
wxapp/wxapp.py | 19 +++
wxpublic.py | 16 ++
8 files changed, 353 insertions(+)
create mode 100644 authentication/Sample.py
create mode 100644 authentication/WXBizMsgCrypt.py
create mode 100644 authentication/__init__.py
create mode 100644 authentication/ierror.py
create mode 100644 wxapp/__init__.py
create mode 100644 wxapp/wxapp.py
create mode 100644 wxpublic.py
diff --git a/.gitignore b/.gitignore
index 72364f9..de6971b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,3 +87,4 @@ ENV/
# Rope project settings
.ropeproject
+/.idea
diff --git a/authentication/Sample.py b/authentication/Sample.py
new file mode 100644
index 0000000..a8dec63
--- /dev/null
+++ b/authentication/Sample.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# ########################################################################
+# Author: jonyqin
+# Created Time: Thu 11 Sep 2014 03:55:41 PM CST
+# File Name: demo.py
+# Description: WXBizMsgCrypt 使用demo文件
+#########################################################################
+from WXBizMsgCrypt import WXBizMsgCrypt
+
+if __name__ == "__main__":
+ """
+ 1.第三方回复加密消息给公众平台;
+ 2.第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
+ """
+ encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"
+ to_xml = """ 1407743423 """
+ token = "spamtest"
+ nonce = "1320562132"
+ appid = "wx2c2769f8efd9abc2"
+ #测试加密接口
+ encryp_test = WXBizMsgCrypt(token, encodingAESKey, appid)
+ ret, encrypt_xml = encryp_test.EncryptMsg(to_xml, nonce)
+ print ret, encrypt_xml
+
+
+ #测试解密接口
+ timestamp = "1409735669"
+ msg_sign = "5d197aaffba7e9b25a30732f161a50dee96bd5fa"
+
+ from_xml = """14097356686054768590064713728"""
+ decrypt_test = WXBizMsgCrypt(token, encodingAESKey, appid)
+ ret, decryp_xml = decrypt_test.DecryptMsg(from_xml, msg_sign, timestamp, nonce)
+ print ret, decryp_xml
diff --git a/authentication/WXBizMsgCrypt.py b/authentication/WXBizMsgCrypt.py
new file mode 100644
index 0000000..7ecae1e
--- /dev/null
+++ b/authentication/WXBizMsgCrypt.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python
+# -*- encoding:utf-8 -*-
+
+""" 对公众平台发送给公众账号的消息加解密示例代码.
+@copyright: Copyright (c) 1998-2014 Tencent Inc.
+
+"""
+# ------------------------------------------------------------------------
+
+import base64
+import string
+import random
+import hashlib
+import time
+import struct
+from Crypto.Cipher import AES
+import xml.etree.cElementTree as ET
+import sys
+import socket
+
+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 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")
+ touser_name = xml_tree.find("ToUserName")
+ return ierror.WXBizMsgCrypt_OK, encrypt.text, touser_name.text
+ except Exception, e:
+ #print e
+ return ierror.WXBizMsgCrypt_ParseXml_Error, None, 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
+
+ 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, appid):
+ """对明文进行加密
+ @param text: 需要加密的明文
+ @return: 加密得到的字符串
+ """
+ # 16位随机字符串添加到明文开头
+ text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + appid
+ # 使用自定义的填充方式对明文进行补位填充
+ 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, appid):
+ """对解密后的明文进行补位删除
+ @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]
+ xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
+ xml_content = content[4: xml_len + 4]
+ from_appid = content[xml_len + 4:]
+ except Exception, e:
+ #print e
+ return ierror.WXBizMsgCrypt_IllegalBuffer, None
+ if from_appid != appid:
+ return ierror.WXBizMsgCrypt_ValidateAppid_Error, None
+ return 0, xml_content
+
+ def get_random_str(self):
+ """ 随机生成16位字符串
+ @return: 16位字符串
+ """
+ rule = string.letters + string.digits
+ str = random.sample(rule, 16)
+ return "".join(str)
+
+
+class WXBizMsgCrypt(object):
+ #构造函数
+ #@param sToken: 公众平台上,开发者设置的Token
+ # @param sEncodingAESKey: 公众平台上,开发者设置的EncodingAESKey
+ # @param sAppId: 企业号的AppId
+ def __init__(self, sToken, sEncodingAESKey, sAppId):
+ try:
+ self.key = base64.b64decode(sEncodingAESKey + "=")
+ assert len(self.key) == 32
+ except:
+ throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
+ #return ierror.WXBizMsgCrypt_IllegalAesKey)
+ self.token = sToken
+ self.appid = sAppId
+
+ 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.appid)
+ if ret != 0:
+ return ret, None
+ if timestamp is None:
+ timestamp = str(int(time.time()))
+ # 生成安全签名
+ sha1 = SHA1()
+ ret, signature = sha1.getSHA1(self.token, 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, touser_name = xmlParse.extract(sPostData)
+ if ret != 0:
+ return ret, None
+ sha1 = SHA1()
+ ret, signature = sha1.getSHA1(self.token, 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.appid)
+ return ret, xml_content
+
diff --git a/authentication/__init__.py b/authentication/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/authentication/ierror.py b/authentication/ierror.py
new file mode 100644
index 0000000..af7fb28
--- /dev/null
+++ b/authentication/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_ValidateAppid_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/wxapp/__init__.py b/wxapp/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/wxapp/wxapp.py b/wxapp/wxapp.py
new file mode 100644
index 0000000..446c27a
--- /dev/null
+++ b/wxapp/wxapp.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from authentication.WXBizMsgCrypt import WXBizMsgCrypt
+
+appid = 'c41c084feb0d18dd1937ba989f667b42'
+token = 'ICcxs5844qOY24rTc1c696X5btR551m2'
+encodingAESKey = 'rEotmyYHNsLLXM1olf3ntRx2PXCYXK6eQ3CxWCJegSV'
+wxcpt = WXBizMsgCrypt(token, encodingAESKey, appid)
+
+class WxApp(object):
+ @staticmethod
+ def is_valid(request):
+ print '==========开始认证==============='
+ print '==========URL参数==============='
+ signature = request.values['signature']
+ timestamp = request.values['timestamp']
+ nonce = request.values['nonce']
+ verifyEchoStr = request.values["echostr"]
+ ret, decryp_xml = wxcpt.VerifyURL(signature, timestamp, nonce, verifyEchoStr)
+ return decryp_xml
diff --git a/wxpublic.py b/wxpublic.py
new file mode 100644
index 0000000..a4509cf
--- /dev/null
+++ b/wxpublic.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+from flask import Flask, request
+from wxapp.wxapp import WxApp
+
+app = Flask(__name__)
+
+@app.route('/hello')
+def hello_world():
+ return 'Hello World!'
+
+@app.route('/')
+def wx_main():
+ return WxApp.is_valid(request)
+
+if __name__ == '__main__':
+ app.run(port=80)