splitaccount_tool.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. # coding=utf-8
  2. import os
  3. import uuid
  4. import requests
  5. import json
  6. import xmltodict
  7. import time
  8. from datetime import datetime
  9. from hashlib import md5
  10. from django.conf import settings
  11. from util.exceptions import CustomError
  12. from cryptography.exceptions import InvalidSignature, InvalidTag
  13. from cryptography.hazmat.backends import default_backend
  14. from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15
  15. from cryptography.hazmat.primitives.ciphers.aead import AESGCM
  16. from cryptography.hazmat.primitives.hashes import SHA1, SHA256
  17. from cryptography.hazmat.primitives.hmac import HMAC
  18. from cryptography.hazmat.primitives.serialization import load_pem_private_key
  19. from cryptography.x509 import load_pem_x509_certificate
  20. from base64 import b64decode, b64encode
  21. class SplitAccountTool(object):
  22. '''直连服务商 分账 文档https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml'''
  23. GET = 'GET'
  24. POST = 'POST'
  25. def __init__(self, appid, mchid, cert_serial_no, apiv3_key, proxy=None):
  26. self._mchid = mchid # 商户号
  27. self._appid = appid
  28. self._private_key = self._innt_private_key() # 商户证书私钥
  29. self._cert_serial_no = cert_serial_no # 商户证书序列号
  30. self._apiv3_key = apiv3_key # 商户APIv3密钥
  31. self._cert_dir = settings.PUBLIC_CERT_ROOT # 平台证书存放目录
  32. self._gate_way = 'https://api.mch.weixin.qq.com'
  33. self._proxy = proxy
  34. self._certificates = []
  35. self._init_certificates()
  36. def _innt_private_key(self):
  37. os_path = settings.PRIVATE_CERT_ROOT + "apiclient_key.pem"
  38. with open(os_path) as f:
  39. private_key = f.read()
  40. return load_private_key(private_key)
  41. def _init_certificates(self):
  42. if self._cert_dir and os.path.exists(self._cert_dir):
  43. for file_name in os.listdir(self._cert_dir):
  44. if not file_name.lower().endswith('.pem'):
  45. continue
  46. with open(self._cert_dir + file_name, encoding="utf-8") as f:
  47. certificate = load_certificate(f.read())
  48. now = datetime.utcnow()
  49. if certificate and now >= certificate.not_valid_before and now <= certificate.not_valid_after:
  50. self._certificates.append(certificate)
  51. if not self._certificates:
  52. self._update_certificates()
  53. if not self._certificates:
  54. raise CustomError('没有wechatpay平台证书,请仔细检查您的初始化参数!')
  55. def _update_certificates(self):
  56. path = '/v3/certificates'
  57. self._certificates.clear()
  58. code, message = self.request(path, skip_verify=True)
  59. if code != 200:
  60. return
  61. data = json.loads(message).get('data')
  62. for value in data:
  63. serial_no = value.get('serial_no')
  64. effective_time = value.get('effective_time')
  65. expire_time = value.get('expire_time')
  66. encrypt_certificate = value.get('encrypt_certificate')
  67. algorithm = nonce = associated_data = ciphertext = None
  68. if encrypt_certificate:
  69. algorithm = encrypt_certificate.get('algorithm')
  70. nonce = encrypt_certificate.get('nonce')
  71. associated_data = encrypt_certificate.get('associated_data')
  72. ciphertext = encrypt_certificate.get('ciphertext')
  73. if not (serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext):
  74. continue
  75. cert_str = aes_decrypt(
  76. nonce=nonce,
  77. ciphertext=ciphertext,
  78. associated_data=associated_data,
  79. apiv3_key=self._apiv3_key)
  80. certificate = load_certificate(cert_str)
  81. if not certificate:
  82. continue
  83. now = datetime.utcnow()
  84. if now < certificate.not_valid_before or now > certificate.not_valid_after:
  85. continue
  86. self._certificates.append(certificate)
  87. if not self._cert_dir:
  88. continue
  89. if not os.path.exists(self._cert_dir):
  90. os.makedirs(self._cert_dir)
  91. if not os.path.exists(self._cert_dir + serial_no + '.pem'):
  92. f = open(self._cert_dir + serial_no + '.pem', 'w')
  93. f.write(cert_str)
  94. f.close()
  95. def _verify_signature(self, headers, body):
  96. signature = headers.get('Wechatpay-Signature')
  97. timestamp = headers.get('Wechatpay-Timestamp')
  98. nonce = headers.get('Wechatpay-Nonce')
  99. serial_no = headers.get('Wechatpay-Serial')
  100. cert_found = False
  101. for cert in self._certificates:
  102. if int('0x' + serial_no, 16) == cert.serial_number:
  103. cert_found = True
  104. certificate = cert
  105. break
  106. if not cert_found:
  107. self._update_certificates()
  108. for cert in self._certificates:
  109. if int('0x' + serial_no, 16) == cert.serial_number:
  110. cert_found = True
  111. certificate = cert
  112. break
  113. if not cert_found:
  114. return False
  115. if not rsa_verify(timestamp, nonce, body, signature, certificate):
  116. return False
  117. return True
  118. def rsa_sign(self, private_key, sign_str):
  119. message = sign_str.encode('UTF-8')
  120. signature = private_key.sign(data=message, padding=PKCS1v15(), algorithm=SHA256())
  121. sign = b64encode(signature).decode('UTF-8').replace('\n', '')
  122. return sign
  123. def build_authorization(self, path, method, mchid, serial_no, private_key, data=None, nonce_str=None):
  124. '''
  125. 一、构建签名串
  126. 签名串一共有五行,每一行为一个参数。行尾以 \n(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n结束,也需要附加一个\n。
  127. 第一步,获取HTTP请求的方法(GET, POST, PUT)等
  128. 第二步,获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。
  129. 第三步,获取发起请求时的系统当前时间戳,即格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数,作为请求时间戳。微信支付会拒绝处理很久之前发起的请求,请商户保持自身系统的时间准确。
  130. 第四步,生成一个请求随机串,可参见生成随机数算法。这里,我们使用命令行直接生成一个。
  131. 第五步,获取请求中的请求报文主体(request body)。
  132. 请求方法为GET时,报文主体为空。
  133. 当请求方法为POST或PUT时,请使用真实发送的JSON报文。
  134. 图片上传API,请使用meta对应的JSON报文。
  135. 对于下载证书的接口来说,请求报文主体是一个空串。
  136. 二、计算签名值
  137. 绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
  138. 三、设置HTTP头
  139. 微信支付商户API V3要求通过HTTP Authorization头来传递签名 Authorization由认证类型和签名信息两部分组成
  140. 1.认证类型,目前为WECHATPAY2-SHA256-RSA2048
  141. 2.签名信息
  142. 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
  143. 商户API证书序列号serial_no,用于声明所使用的证书
  144. 请求随机串nonce_str
  145. 时间戳timestamp
  146. 签名值signature
  147. 注:以上五项签名信息,无顺序要求。
  148. '''
  149. timeStamp = str(int(time.time()))
  150. nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper()
  151. body = data if isinstance(data, str) else json.dumps(data) if data else ''
  152. sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body)
  153. signature = self.rsa_sign(private_key=private_key, sign_str=sign_str)
  154. authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % (
  155. mchid, nonce_str, signature, timeStamp, serial_no)
  156. return authorization
  157. def request(self, path, method=GET, data=None, skip_verify=False, sign_data=None, files=None, cipher_data=False, headers={}):
  158. if files:
  159. headers.update({'Content-Type': 'multipart/form-data'})
  160. else:
  161. headers.update({'Content-Type': 'application/json'})
  162. headers.update({'Accept': 'application/json'})
  163. authorization = self.build_authorization(path, method, self._mchid, self._cert_serial_no, self._private_key, data=sign_data if sign_data else data)
  164. headers.update({'Authorization': authorization})
  165. if method == SplitAccountTool.GET:
  166. response = requests.get(url=self._gate_way + path, headers=headers, proxies=self._proxy)
  167. elif method == SplitAccountTool.POST:
  168. response = requests.post(url=self._gate_way + path, json=None if files else data, data=data if files else None, headers=headers, files=files, proxies=self._proxy)
  169. else:
  170. raise CustomError('请选择正确的请求方式!')
  171. if response.status_code in range(200, 300) and not skip_verify:
  172. if not self._verify_signature(response.headers, response.text):
  173. raise CustomError('签名验证失败!')
  174. return response.status_code, response.text if 'application/json' in response.headers.get('Content-Type') else response.content
  175. def format_private_key(private_key_str):
  176. pem_start = '-----BEGIN PRIVATE KEY-----\n'
  177. pem_end = '\n-----END PRIVATE KEY-----'
  178. if not private_key_str.startswith(pem_start):
  179. private_key_str = pem_start + private_key_str
  180. if not private_key_str.endswith(pem_end):
  181. private_key_str = private_key_str + pem_end
  182. return private_key_str
  183. def load_private_key(private_key_str):
  184. try:
  185. return load_pem_private_key(data=format_private_key(private_key_str).encode('UTF-8'), password=None, backend=default_backend())
  186. except:
  187. raise CustomError('商户证书私钥加载失败!')
  188. def rsa_verify(timestamp, nonce, body, signature, certificate):
  189. '''验证签名'''
  190. sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body)
  191. public_key = certificate.public_key()
  192. message = sign_str.encode('UTF-8')
  193. signature = b64decode(signature)
  194. try:
  195. public_key.verify(signature, message, PKCS1v15(), SHA256())
  196. except InvalidSignature:
  197. return False
  198. return True
  199. def load_certificate(certificate_str):
  200. try:
  201. return load_pem_x509_certificate(data=certificate_str.encode('UTF-8'), backend=default_backend())
  202. except:
  203. return None
  204. def aes_decrypt(nonce, ciphertext, associated_data, apiv3_key):
  205. '''回调信息解密'''
  206. key_bytes = apiv3_key.encode('UTF-8')
  207. nonce_bytes = nonce.encode('UTF-8')
  208. associated_data_bytes = associated_data.encode('UTF-8')
  209. data = b64decode(ciphertext)
  210. aesgcm = AESGCM(key=key_bytes)
  211. try:
  212. result = aesgcm.decrypt(nonce=nonce_bytes, data=data, associated_data=associated_data_bytes).decode('UTF-8')
  213. except InvalidTag:
  214. result = None
  215. return result