parent
07dc141b29
commit
c7e77ad46b
@ -1,141 +1,314 @@ |
|||||||
#!/usr/bin/env python |
|
||||||
# -*- coding: utf-8 -*- |
# -*- coding: utf-8 -*- |
||||||
# project: 4月 |
import json |
||||||
# author: NinEveN |
from enum import Enum |
||||||
# date: 2021/4/15 |
|
||||||
|
|
||||||
import socket |
from .core import Core, RequestType |
||||||
import time |
|
||||||
import uuid |
|
||||||
import hashlib |
|
||||||
import xml.etree.ElementTree as ET |
|
||||||
import requests |
|
||||||
|
|
||||||
|
|
||||||
def get_nonce_str(): |
class WeChatPay(): |
||||||
|
def __init__(self, |
||||||
|
wechatpay_type, |
||||||
|
mchid, |
||||||
|
parivate_key, |
||||||
|
cert_serial_no, |
||||||
|
appid, |
||||||
|
apiv3_key, |
||||||
|
notify_url=None): |
||||||
""" |
""" |
||||||
获取随机字符串 |
:param wechatpay_type: 微信支付类型,示例值:WeChatPayType.MINIPROG |
||||||
:return: |
:param mchid: 直连商户号,示例值:'1230000109' |
||||||
|
:param mch_private_key: 商户证书私钥,示例值:'MIIEvwIBADANBgkqhkiG9w0BAQE...' |
||||||
|
:param mch_key_serial_no: 商户证书序列号,示例值:'444F4864EA9B34415...' |
||||||
|
:param appid: 应用ID,示例值:'wxd678efh567hg6787' |
||||||
|
:param mch_apiv3_key: 商户APIv3密钥,示例值:'a12d3924fd499edac8a5efc...' |
||||||
|
:param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' |
||||||
""" |
""" |
||||||
return str(uuid.uuid4()).replace('-', '') |
self._type = wechatpay_type |
||||||
|
self._mchid = mchid |
||||||
|
self._appid = appid |
||||||
|
self._notify_url = notify_url |
||||||
|
self._core = Core(mchid=self._mchid, |
||||||
|
cert_serial_no=cert_serial_no, |
||||||
|
private_key=parivate_key, |
||||||
|
apiv3_key=apiv3_key) |
||||||
|
|
||||||
|
def pay(self, |
||||||
|
description, |
||||||
|
out_trade_no, |
||||||
|
amount, |
||||||
|
payer=None, |
||||||
|
time_expire=None, |
||||||
|
attach=None, |
||||||
|
goods_tag=None, |
||||||
|
detail=None, |
||||||
|
scene_info=None, |
||||||
|
settle_info=None, |
||||||
|
notify_url=None): |
||||||
|
"""统一下单 |
||||||
|
:return code, message: |
||||||
|
:param description: 商品描述,示例值:'Image形象店-深圳腾大-QQ公仔' |
||||||
|
:param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' |
||||||
|
:param amount: 订单金额,示例值:{'total':100, 'currency':'CNY'} |
||||||
|
:param payer: 支付者,示例值:{'openid':'oHkLxtx0vUqe-18p_AXTZ1innxkCY'} |
||||||
|
:param time_expire: 交易结束时间,示例值:'2018-06-08T10:34:56+08:00' |
||||||
|
:param attach: 附加数据,示例值:'自定义数据' |
||||||
|
:param goods_tag: 订单优惠标记,示例值:'WXG' |
||||||
|
:param detail: 优惠功能,示例值:{'cost_price':608800, 'invoice_id':'微信123', 'goods_detail':[{'merchant_goods_id':'商品编码', 'wechatpay_goods_id':'1001', 'goods_name':'iPhoneX 256G', 'quantity':1, 'unit_price':828800}]} |
||||||
|
:param scene_info: 场景信息,示例值:{'payer_client_ip':'14.23.150.211', 'device_id':'013467007045764', 'store_info':{'id':'0001', 'name':'腾讯大厦分店', 'area_code':'440305', 'address':'广东省深圳市南山区科技中一道10000号'}} |
||||||
|
:param settle_info: 结算信息,示例值:{'profit_sharing':False} |
||||||
|
:param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' |
||||||
|
""" |
||||||
|
params = {} |
||||||
|
params['appid'] = self._appid |
||||||
|
params['mchid'] = self._mchid |
||||||
|
params['notify_url'] = notify_url or self._notify_url |
||||||
|
if description: |
||||||
|
params.update({'description': description}) |
||||||
|
else: |
||||||
|
raise Exception('description is not assigned.') |
||||||
|
if out_trade_no: |
||||||
|
params.update({'out_trade_no': out_trade_no}) |
||||||
|
else: |
||||||
|
raise Exception('out_trade_no is not assigned.') |
||||||
|
if amount: |
||||||
|
params.update({'amount': amount}) |
||||||
|
else: |
||||||
|
raise Exception('amount is not assigned.') |
||||||
|
if payer: |
||||||
|
params.update({'payer': payer}) |
||||||
|
if scene_info: |
||||||
|
params.update({'scene_info': scene_info}) |
||||||
|
if time_expire: |
||||||
|
params.update({'time_expire': time_expire}) |
||||||
|
if attach: |
||||||
|
params.update({'attach': attach}) |
||||||
|
if goods_tag: |
||||||
|
params.update({'goods_tag': goods_tag}) |
||||||
|
if detail: |
||||||
|
params.update({'detail': detail}) |
||||||
|
if settle_info: |
||||||
|
params.update({'settle_info': settle_info}) |
||||||
|
if self._type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: |
||||||
|
if not payer: |
||||||
|
raise Exception('payer is not assigned') |
||||||
|
path = '/v3/pay/transactions/jsapi' |
||||||
|
elif self._type == WeChatPayType.APP: |
||||||
|
path = '/v3/pay/transactions/app' |
||||||
|
elif self._type == WeChatPayType.H5: |
||||||
|
if not scene_info: |
||||||
|
raise Exception('scene_info is not assigned.') |
||||||
|
path = '/v3/pay/transactions/h5' |
||||||
|
elif self._type == WeChatPayType.NATIVE: |
||||||
|
path = '/v3/pay/transactions/native' |
||||||
|
return self._core.request(path, method=RequestType.POST, data=params) |
||||||
|
|
||||||
def dict_to_order_xml(dict_data): |
def close(self, out_trade_no): |
||||||
|
"""关闭订单 |
||||||
|
:param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' |
||||||
""" |
""" |
||||||
dict to order xml |
if not out_trade_no: |
||||||
ASCII码从小到大排序 |
raise Exception('out_trade_no is not assigned.') |
||||||
:param dict_data: |
path = '/v3/pay/transactions/out-trade-no/%s/close' % out_trade_no |
||||||
:return: |
params = {'mchid': self._mchid} |
||||||
|
return self._core.request(path, method=RequestType.POST, data=params) |
||||||
|
|
||||||
|
def query(self, transaction_id=None, out_trade_no=None): |
||||||
|
"""查询订单 |
||||||
|
:param transaction_id: 微信支付订单号,示例值:1217752501201407033233368018 |
||||||
|
:param out_trade_no: 商户订单号,示例值:1217752501201407033233368018 |
||||||
""" |
""" |
||||||
xml = ["<xml>"] |
if not (transaction_id or out_trade_no): |
||||||
for k in sorted(dict_data): |
raise Exception('params is not assigned') |
||||||
xml.append("<{0}>{1}</{0}>".format(k, dict_data.get(k))) |
if transaction_id: |
||||||
xml.append("</xml>") |
path = '/v3/pay/transactions/id/%s' % transaction_id |
||||||
return "".join(xml) |
else: |
||||||
|
path = '/v3/pay/transactions/out-trade-no/%s' % out_trade_no |
||||||
|
path = '%s?mchid=%s' % (path, self._mchid) |
||||||
|
return self._core.request(path) |
||||||
|
|
||||||
|
def refund(self, |
||||||
|
out_refund_no, |
||||||
|
amount, |
||||||
|
transaction_id=None, |
||||||
|
out_trade_no=None, |
||||||
|
reason=None, |
||||||
|
funds_account=None, |
||||||
|
goods_detail=None, |
||||||
|
notify_url=None): |
||||||
|
"""申请退款 |
||||||
|
:param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' |
||||||
|
:param amount: 金额信息,示例值:{'refund':888, 'total':888, 'currency':'CNY'} |
||||||
|
:param transaction_id: 微信支付订单号,示例值:'1217752501201407033233368018' |
||||||
|
:param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018' |
||||||
|
:param reason: 退款原因,示例值:'商品已售完' |
||||||
|
:param funds_account: 退款资金来源,示例值:'AVAILABLE' |
||||||
|
:param goods_detail: 退款商品,示例值:{'merchant_goods_id':'1217752501201407033233368018', 'wechatpay_goods_id':'1001', 'goods_name':'iPhone6s 16G', 'unit_price':528800, 'refund_amount':528800, 'refund_quantity':1} |
||||||
|
:param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php' |
||||||
|
""" |
||||||
|
params = {} |
||||||
|
params['notify_url'] = notify_url or self._notify_url |
||||||
|
if out_refund_no: |
||||||
|
params.update({'out_refund_no': out_refund_no}) |
||||||
|
else: |
||||||
|
raise Exception('out_refund_no is not assigned.') |
||||||
|
if amount: |
||||||
|
params.update({'amount': amount}) |
||||||
|
else: |
||||||
|
raise Exception('amount is not assigned.') |
||||||
|
if transaction_id: |
||||||
|
params.update({'transaction_id': transaction_id}) |
||||||
|
if out_trade_no: |
||||||
|
params.update({'out_trade_no': out_trade_no}) |
||||||
|
if reason: |
||||||
|
params.update({'reason': reason}) |
||||||
|
if funds_account: |
||||||
|
params.update({'funds_account': funds_account}) |
||||||
|
if goods_detail: |
||||||
|
params.update({'goods_detail': goods_detail}) |
||||||
|
path = '/v3/refund/domestic/refunds' |
||||||
|
return self._core.request(path, method=RequestType.POST, data=params) |
||||||
|
|
||||||
def dict_to_xml(dict_data): |
def query_refund(self, out_refund_no): |
||||||
xml = ["<xml>"] |
"""查询单笔退款 |
||||||
for k, v in dict_data.items(): |
:param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018' |
||||||
xml.append("<{0}>{1}</{0}>".format(k, v)) |
""" |
||||||
xml.append("</xml>") |
path = '/v3/refund/domestic/refunds/%s' % out_refund_no |
||||||
return "".join(xml) |
return self._core.request(path) |
||||||
|
|
||||||
|
def trade_bill(self, bill_date, bill_type='ALL', tar_type='GZIP'): |
||||||
|
"""申请交易账单 |
||||||
|
:param bill_date: 账单日期,示例值:'2019-06-11' |
||||||
|
:param bill_type: 账单类型, 默认值:'ALL' |
||||||
|
:param tar_type: 压缩类型,默认值:'GZIP' |
||||||
|
""" |
||||||
|
path = '/v3/bill/tradebill?bill_date=%s&bill_type=%s&tar_type=%s' % ( |
||||||
|
bill_date, bill_type, tar_type) |
||||||
|
return self._core.request(path) |
||||||
|
|
||||||
def xml_to_dict(xml_data): |
def fundflow_bill(self, bill_date, account_type='BASIC', tar_type='GZIP'): |
||||||
|
"""申请资金账单 |
||||||
|
:param bill_date: 账单日期,示例值:'2019-06-11' |
||||||
|
:param account_type: 资金账户类型, 默认值:'BASIC',基本账户, 可选:'OPERATION',运营账户;'FEES',手续费账户 |
||||||
|
:param tar_type: 压缩类型,默认值:'GZIP' |
||||||
""" |
""" |
||||||
xml to dict |
path = '/v3/bill/fundflowbill?bill_date=%s&account_type=%s&tar_type=%s' % ( |
||||||
:param xml_data: |
bill_date, account_type, tar_type) |
||||||
:return: |
return self._core.request(path) |
||||||
|
|
||||||
|
def download_bill(self, url): |
||||||
|
"""下载账单 |
||||||
|
:param url: 账单下载地址,示例值:'https://api.mch.weixin.qq.com/v3/billdownload/file?token=xxx' |
||||||
""" |
""" |
||||||
xml_dict = {} |
path = url[len(self._core._gate_way):] if url.startswith( |
||||||
root = ET.fromstring(xml_data) |
self._core._gate_way) else url |
||||||
for child in root: |
return self._core.request(path) |
||||||
xml_dict[child.tag] = child.text |
|
||||||
return xml_dict |
|
||||||
|
|
||||||
|
# def certificates(self): |
||||||
|
# """下载微信支付平台证书 |
||||||
|
# """ |
||||||
|
# return self._core.certificates |
||||||
|
|
||||||
class WxPay(object): |
def combine_pay(self, |
||||||
""" |
combine_out_trade_no, |
||||||
https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=3 |
sub_orders, |
||||||
|
scene_info=None, |
||||||
|
combine_payer_info=None, |
||||||
|
time_start=None, |
||||||
|
time_expire=None, |
||||||
|
combine_appid=None, |
||||||
|
combine_mchid=None, |
||||||
|
notify_url=None): |
||||||
|
"""合单支付下单 |
||||||
|
:param combine_out_trade_no: 合单商户订单号, 示例值:'P20150806125346' |
||||||
|
:param sub_orders: 子单信息,示例值:[{'mchid':'1900000109', 'attach':'深圳分店', 'amount':{'total_amount':100,'currency':'CNY'}, 'out_trade_no':'20150806125346', 'description':'腾讯充值中心-QQ会员充值', 'settle_info':{'profit_sharing':False, 'subsidy_amount':10}}] |
||||||
|
:param scene_info: 场景信息, 示例值:{'device_id':'POS1:123', 'payer_client_ip':'14.17.22.32'} |
||||||
|
:param combine_payer_info: 支付者, 示例值:{'openid':'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o'} |
||||||
|
:param time_start: 交易起始时间,示例值:'2019-12-31T15:59:59+08:00' |
||||||
|
:param time_expire: 交易结束时间, 示例值:'2019-12-31T15:59:59+08:00' |
||||||
|
:param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787' |
||||||
|
:param combine_mchid: 合单发起方商户号,示例值:'1900000109' |
||||||
|
:param notify_url: 通知地址, 示例值:'https://yourapp.com/notify' |
||||||
""" |
""" |
||||||
|
params = {} |
||||||
|
params['combine_appid'] = combine_appid or self._appid |
||||||
|
params['combine_mchid'] = combine_mchid or self._mchid |
||||||
|
params['notify_url'] = notify_url or self._notify_url |
||||||
|
if combine_out_trade_no: |
||||||
|
params.update({'combine_out_trade_no': combine_out_trade_no}) |
||||||
|
else: |
||||||
|
raise Exception('combine_out_trade_no is not assigned.') |
||||||
|
if sub_orders: |
||||||
|
params.update({'sub_orders': sub_orders}) |
||||||
|
else: |
||||||
|
raise Exception('sub_orders is not assigned.') |
||||||
|
if scene_info: |
||||||
|
params.update({'scene_info': scene_info}) |
||||||
|
if combine_payer_info: |
||||||
|
params.update({'combine_payer_info': combine_payer_info}) |
||||||
|
if time_start: |
||||||
|
params.update({'time_start': time_start}) |
||||||
|
if time_expire: |
||||||
|
params.update({'time_expire': time_expire}) |
||||||
|
if self._type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]: |
||||||
|
if not combine_payer_info: |
||||||
|
raise Exception('combine_payer_info is not assigned') |
||||||
|
path = '/v3/combine-transactions/jsapi' |
||||||
|
elif self._type == WeChatPayType.APP: |
||||||
|
path = '/v3/combine-transactions/app' |
||||||
|
elif self._type == WeChatPayType.H5: |
||||||
|
if not scene_info: |
||||||
|
raise Exception('scene_info is not assigned.') |
||||||
|
path = '/v3/combine-transactions/h5' |
||||||
|
elif self._type == WeChatPayType.NATIVE: |
||||||
|
path = '/v3/combine-transactions/native' |
||||||
|
return self._core.request(path, method=RequestType.POST, data=params) |
||||||
|
|
||||||
def __init__(self, app_id, mch_id, notify_url, merchant_key): |
def combine_query(self, combine_out_trade_no): |
||||||
self.url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/native' |
"""合单查询订单 |
||||||
self.app_id = app_id # 微信分配的小程序ID |
:param combine_out_trade_no: 合单商户订单号,示例值:P20150806125346 |
||||||
self.mch_id = mch_id # 商户号 |
|
||||||
self.notify_url = notify_url # 通知地址 |
|
||||||
self.spbill_create_ip = socket.gethostbyname(socket.gethostname()) # 获取本机ip |
|
||||||
self.merchant_key = merchant_key # 商户KEY,修改为自己的 |
|
||||||
|
|
||||||
def create_sign(self, pay_data): |
|
||||||
""" |
""" |
||||||
生成签名 |
params = {} |
||||||
https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3 |
if not combine_out_trade_no: |
||||||
:param pay_data: |
raise Exception('param combine_out_trade_no is not assigned') |
||||||
:return: |
else: |
||||||
|
params.update({'combine_out_trade_no': combine_out_trade_no}) |
||||||
|
path = '/v3/combine-transactions/out-trade-no/%s' % combine_out_trade_no |
||||||
|
return self._core.request(path) |
||||||
|
|
||||||
|
def combine_close(self, combine_out_trade_no, sub_orders, combine_appid=None): |
||||||
|
"""合单关闭订单 |
||||||
|
:param combine_out_trade_no: 合单商户订单号,示例值:'P20150806125346' |
||||||
|
:param sub_orders: 子单信息, 示例值:[{'mchid': '1900000109', 'out_trade_no': '20150806125346'}] |
||||||
|
:param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787' |
||||||
""" |
""" |
||||||
|
params = {} |
||||||
|
params['combine_appid'] = combine_appid or self._appid |
||||||
|
|
||||||
# 拼接stringA |
if not combine_out_trade_no: |
||||||
string_a = '&'.join(["{0}={1}".format(k, pay_data.get(k)) for k in sorted(pay_data)]) |
raise Exception('combine_out_trade_no is not assigned.') |
||||||
# 拼接key |
if not sub_orders: |
||||||
string_sign_temp = '{0}&key={1}'.format(string_a, self.merchant_key).encode('utf-8') |
raise Exception('sub_orders is not assigned.') |
||||||
# md5签名 |
else: |
||||||
sign = hashlib.md5(string_sign_temp).hexdigest() |
params.update({'sub_orders': sub_orders}) |
||||||
return sign.upper() |
path = '/v3/combine-transactions/out-trade-no/%s/close' % combine_out_trade_no |
||||||
|
return self._core.request(path, method=RequestType.POST, data=params) |
||||||
|
|
||||||
def get_pay_info(self, pay_data): |
def sign(self, data): |
||||||
""" |
"""计算签名值paySign,供JSAPI、APP、NATIVE调起支付时使用 |
||||||
支付统一下单 |
:param data: 需要签名的参数清单,示例值:['wx888','1414561699','5K8264ILTKCH16CQ2502S....','prepay_id=wx201410272009395522657....'] |
||||||
:return: |
|
||||||
""" |
|
||||||
# 调用签名函数 |
|
||||||
|
|
||||||
post_data = { |
|
||||||
'appid': self.app_id, # 小程序ID |
|
||||||
'mch_id': self.mch_id, # 商户号 |
|
||||||
'description': pay_data.get('description'), # 商品描述 |
|
||||||
'out_trade_no': pay_data.get('out_trade_no'), # 商户订单号 |
|
||||||
'time_expire': pay_data.get('time_expire'), # 交易结束时间 示例值:2018-06-08T10:34:56+08:00 |
|
||||||
'attach': pay_data.get('attach'), # 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用 |
|
||||||
'notify_url': self.notify_url, # 通知地址 |
|
||||||
'amount': { |
|
||||||
'total': pay_data.get('total'), # 订单总金额,单位为分。示例值:100 |
|
||||||
'currency': 'CNY' |
|
||||||
} |
|
||||||
} |
|
||||||
sign = self.create_sign(post_data) |
|
||||||
post_data['sign'] = sign |
|
||||||
|
|
||||||
xml = dict_to_xml(post_data) |
|
||||||
|
|
||||||
# 统一下单接口请求 |
|
||||||
r = requests.post(self.url, data=xml.encode("utf-8")) |
|
||||||
r.encoding = "utf-8" |
|
||||||
res = xml_to_dict(r.text) |
|
||||||
err_code_des = res.get('err_code_des') |
|
||||||
# 出错信息 |
|
||||||
if err_code_des: |
|
||||||
return {'code': 40001, 'msg': err_code_des} |
|
||||||
prepay_id = res.get('prepay_id') |
|
||||||
|
|
||||||
return self.re_sign(post_data, prepay_id) |
|
||||||
|
|
||||||
def re_sign(self, post_data, prepay_id): |
|
||||||
""" |
""" |
||||||
再次对返回的数据签名 |
sign_str = '\n'.join(data) + '\n' |
||||||
https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7&index=3 |
return self._core.sign(sign_str) |
||||||
:param post_data: |
|
||||||
:param prepay_id: |
def decrypt_callback(self, headers, body): |
||||||
:return: |
"""解密回调接口收到的信息 |
||||||
""" |
""" |
||||||
pay_sign_data = { |
return self._core.decrypt_callback(headers, body) |
||||||
'appId': self.app_id, # 注意大小写与统一下单不一致 |
|
||||||
'timeStamp': post_data.get('out_trade_no'), |
|
||||||
'nonceStr': post_data.get('nonce_str'), |
class WeChatPayType(Enum): |
||||||
'package': 'prepay_id={0}'.format(prepay_id), |
JSAPI = 0 |
||||||
'signType': 'MD5', |
APP = 1 |
||||||
} |
H5 = 2 |
||||||
pay_sign = self.create_sign(pay_sign_data) |
NATIVE = 3 |
||||||
pay_sign_data['paySign'] = pay_sign |
MINIPROG = 4 |
||||||
return pay_sign_data |
|
||||||
|
@ -0,0 +1,233 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
import json |
||||||
|
from enum import Enum |
||||||
|
|
||||||
|
import requests |
||||||
|
|
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
import time |
||||||
|
import uuid |
||||||
|
from base64 import b64decode, b64encode |
||||||
|
import json |
||||||
|
|
||||||
|
from cryptography.exceptions import InvalidSignature |
||||||
|
from cryptography.hazmat.backends import default_backend |
||||||
|
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 |
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM |
||||||
|
from cryptography.hazmat.primitives.hashes import SHA256 |
||||||
|
from cryptography.hazmat.primitives.serialization import (load_pem_private_key, |
||||||
|
load_pem_public_key) |
||||||
|
from OpenSSL import crypto |
||||||
|
|
||||||
|
|
||||||
|
def build_authorization(path, |
||||||
|
method, |
||||||
|
mchid, |
||||||
|
serial_no, |
||||||
|
mch_private_key, |
||||||
|
data=None, |
||||||
|
nonce_str=None): |
||||||
|
timeStamp = str(int(time.time())) |
||||||
|
nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper() |
||||||
|
body = json.dumps(data) if data else '' |
||||||
|
sign_str = method + '\n' + path + '\n' + \ |
||||||
|
timeStamp + '\n' + nonce_str + '\n' + body + '\n' |
||||||
|
signature = sign(private_key=mch_private_key, sign_str=sign_str) |
||||||
|
authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % ( |
||||||
|
mchid, nonce_str, signature, timeStamp, serial_no) |
||||||
|
return authorization |
||||||
|
|
||||||
|
|
||||||
|
def sign(private_key, sign_str): |
||||||
|
private_key = load_pem_private_key(data=format_private_key( |
||||||
|
private_key).encode('UTF-8'), password=None, backend=default_backend()) |
||||||
|
message = sign_str.encode('UTF-8') |
||||||
|
signature = private_key.sign(message, PKCS1v15(), SHA256()) |
||||||
|
sign = b64encode(signature).decode('UTF-8').replace('\n', '') |
||||||
|
return sign |
||||||
|
|
||||||
|
|
||||||
|
def decrypt(nonce, ciphertext, associated_data, apiv3_key): |
||||||
|
key_bytes = apiv3_key.encode('UTF-8') |
||||||
|
nonce_bytes = nonce.encode('UTF-8') |
||||||
|
associated_data_bytes = associated_data.encode('UTF-8') |
||||||
|
data = b64decode(ciphertext) |
||||||
|
aesgcm = AESGCM(key_bytes) |
||||||
|
return aesgcm.decrypt(nonce_bytes, data, associated_data_bytes).decode('UTF-8') |
||||||
|
|
||||||
|
|
||||||
|
def format_private_key(private_key): |
||||||
|
pem_start = '-----BEGIN PRIVATE KEY-----\n' |
||||||
|
pem_end = '\n-----END PRIVATE KEY-----' |
||||||
|
if not private_key.startswith(pem_start): |
||||||
|
private_key = pem_start + private_key |
||||||
|
if not private_key.endswith(pem_end): |
||||||
|
private_key = private_key + pem_end |
||||||
|
return private_key |
||||||
|
|
||||||
|
|
||||||
|
def format_certificate(certificate): |
||||||
|
pem_start = '-----BEGIN CERTIFICATE-----\n' |
||||||
|
pem_end = '\n-----END CERTIFICATE-----' |
||||||
|
if not certificate.startswith(pem_start): |
||||||
|
certificate = pem_start + certificate |
||||||
|
if not certificate.endswith(pem_end): |
||||||
|
certificate = certificate + pem_end |
||||||
|
return certificate |
||||||
|
|
||||||
|
|
||||||
|
def verify(timestamp, nonce, body, signature, certificate): |
||||||
|
sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body) |
||||||
|
public_key_str = dump_public_key(certificate) |
||||||
|
public_key = load_pem_public_key(data=public_key_str.encode('UTF-8'), backend=default_backend()) |
||||||
|
message = sign_str.encode('UTF-8') |
||||||
|
signature = b64decode(signature) |
||||||
|
try: |
||||||
|
public_key.verify(signature, sign_str.encode('UTF-8'), PKCS1v15(), SHA256()) |
||||||
|
except InvalidSignature: |
||||||
|
return False |
||||||
|
return True |
||||||
|
|
||||||
|
|
||||||
|
def certificate_serial_number(certificate): |
||||||
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, format_certificate(certificate)) |
||||||
|
try: |
||||||
|
res = cert.get_signature_algorithm().decode('UTF-8') |
||||||
|
if res != 'sha256WithRSAEncryption': |
||||||
|
return None |
||||||
|
return hex(cert.get_serial_number()).upper()[2:] |
||||||
|
except: |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
def dump_public_key(certificate): |
||||||
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, format_certificate(certificate)) |
||||||
|
public_key = crypto.dump_publickey(crypto.FILETYPE_PEM, cert.get_pubkey()).decode("utf-8") |
||||||
|
return public_key |
||||||
|
|
||||||
|
|
||||||
|
class RequestType(Enum): |
||||||
|
GET = 0 |
||||||
|
POST = 1 |
||||||
|
|
||||||
|
|
||||||
|
class Core(): |
||||||
|
def __init__(self, mchid, cert_serial_no, private_key, apiv3_key): |
||||||
|
self._mchid = mchid |
||||||
|
self._cert_serial_no = cert_serial_no |
||||||
|
self._private_key = private_key |
||||||
|
self._apiv3_key = apiv3_key |
||||||
|
self._gate_way = 'https://api.mch.weixin.qq.com' |
||||||
|
self._certificates = [] |
||||||
|
self._update_certificates() |
||||||
|
|
||||||
|
def _update_certificates(self): |
||||||
|
path = '/v3/certificates' |
||||||
|
code, message = self.request( |
||||||
|
path, |
||||||
|
skip_verify=False if self._certificates else True) |
||||||
|
if code == 200: |
||||||
|
self._certificates.clear() |
||||||
|
data = json.loads(message).get('data') |
||||||
|
for v in data: |
||||||
|
serial_no = v.get('serial_no') |
||||||
|
effective_time = v.get('effective_time') |
||||||
|
expire_time = v.get('expire_time') |
||||||
|
encrypt_certificate = v.get('encrypt_certificate') |
||||||
|
algorithm = nonce = associated_data = ciphertext = None |
||||||
|
if encrypt_certificate: |
||||||
|
algorithm = encrypt_certificate.get('algorithm') |
||||||
|
nonce = encrypt_certificate.get('nonce') |
||||||
|
associated_data = encrypt_certificate.get( |
||||||
|
'associated_data') |
||||||
|
ciphertext = encrypt_certificate.get('ciphertext') |
||||||
|
if not ( |
||||||
|
serial_no and effective_time and expire_time and algorithm and nonce and associated_data and ciphertext): |
||||||
|
continue |
||||||
|
certificate = decrypt( |
||||||
|
nonce=nonce, |
||||||
|
ciphertext=ciphertext, |
||||||
|
associated_data=associated_data, |
||||||
|
apiv3_key=self._apiv3_key) |
||||||
|
self._certificates.append(certificate) |
||||||
|
|
||||||
|
def verify_signature(self, headers, body): |
||||||
|
signature = headers.get('Wechatpay-Signature') |
||||||
|
timestamp = headers.get('Wechatpay-Timestamp') |
||||||
|
nonce = headers.get('Wechatpay-Nonce') |
||||||
|
serial_no = headers.get('Wechatpay-Serial') |
||||||
|
verified = False |
||||||
|
for cert in self._certificates: |
||||||
|
if serial_no == certificate_serial_number(cert): |
||||||
|
verified = True |
||||||
|
certificate = cert |
||||||
|
break |
||||||
|
if not verified: |
||||||
|
self._update_certificates() |
||||||
|
for cert in self._certificates: |
||||||
|
if serial_no == certificate_serial_number(cert): |
||||||
|
verified = True |
||||||
|
certificate = cert |
||||||
|
break |
||||||
|
if not verified: |
||||||
|
return False |
||||||
|
if not verify(timestamp, nonce, body, signature, certificate): |
||||||
|
return False |
||||||
|
return True |
||||||
|
|
||||||
|
def request(self, path, method=RequestType.GET, data=None, skip_verify=False): |
||||||
|
headers = {} |
||||||
|
headers.update({'Content-Type': 'application/json'}) |
||||||
|
headers.update({'Accept': 'application/json'}) |
||||||
|
headers.update( |
||||||
|
{'User-Agent': 'wechatpay v3 python sdk(https://github.com/minibear2021/wechatpayv3)'}) |
||||||
|
authorization = build_authorization( |
||||||
|
path, |
||||||
|
'GET' if method == RequestType.GET else 'POST', |
||||||
|
self._mchid, |
||||||
|
self._cert_serial_no, |
||||||
|
self._private_key, |
||||||
|
data=data) |
||||||
|
headers.update({'Authorization': authorization}) |
||||||
|
if method == RequestType.GET: |
||||||
|
response = requests.get(url=self._gate_way + path, headers=headers) |
||||||
|
else: |
||||||
|
response = requests.post( |
||||||
|
self._gate_way + path, json=data, headers=headers) |
||||||
|
|
||||||
|
if response.status_code in range(200, 300) and not skip_verify: |
||||||
|
if not self.verify_signature(response.headers, response.text): |
||||||
|
raise Exception('failed to verify signature') |
||||||
|
return response.status_code, response.text |
||||||
|
|
||||||
|
def sign(self, sign_str): |
||||||
|
return sign(self._private_key, sign_str) |
||||||
|
|
||||||
|
def decrypt_callback(self, headers, body): |
||||||
|
if self.verify_signature(headers, body): |
||||||
|
data = json.loads(body) |
||||||
|
resource_type = data.get('resource_type') |
||||||
|
if resource_type != 'encrypt-resource': |
||||||
|
return None |
||||||
|
resource = data.get('resource') |
||||||
|
if not resource: |
||||||
|
return None |
||||||
|
algorithm = resource.get('algorithm') |
||||||
|
if algorithm != 'AEAD_AES_256_GCM': |
||||||
|
return None |
||||||
|
nonce = resource.get('nonce') |
||||||
|
ciphertext = resource.get('ciphertext') |
||||||
|
associated_data = resource.get('associated_data') |
||||||
|
if not (nonce and ciphertext): |
||||||
|
return None |
||||||
|
if not associated_data: |
||||||
|
associated_data = '' |
||||||
|
result = decrypt( |
||||||
|
nonce=nonce, |
||||||
|
ciphertext=ciphertext, |
||||||
|
associated_data=associated_data, |
||||||
|
apiv3_key=self._apiv3_key) |
||||||
|
return result |
||||||
|
else: |
||||||
|
return None |
@ -0,0 +1,101 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
import time |
||||||
|
import uuid |
||||||
|
from base64 import b64decode, b64encode |
||||||
|
import json |
||||||
|
|
||||||
|
from cryptography.exceptions import InvalidSignature |
||||||
|
from cryptography.hazmat.backends import default_backend |
||||||
|
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 |
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM |
||||||
|
from cryptography.hazmat.primitives.hashes import SHA256 |
||||||
|
from cryptography.hazmat.primitives.serialization import (load_pem_private_key, |
||||||
|
load_pem_public_key) |
||||||
|
from OpenSSL import crypto |
||||||
|
|
||||||
|
|
||||||
|
def build_authorization(path, |
||||||
|
method, |
||||||
|
mchid, |
||||||
|
serial_no, |
||||||
|
mch_private_key, |
||||||
|
data=None, |
||||||
|
nonce_str=None): |
||||||
|
timeStamp = str(int(time.time())) |
||||||
|
nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper() |
||||||
|
body = json.dumps(data) if data else '' |
||||||
|
sign_str = method + '\n' + path + '\n' + \ |
||||||
|
timeStamp + '\n' + nonce_str + '\n' + body + '\n' |
||||||
|
signature = sign(private_key=mch_private_key, sign_str=sign_str) |
||||||
|
authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"' % ( |
||||||
|
mchid, nonce_str, signature, timeStamp, serial_no) |
||||||
|
return authorization |
||||||
|
|
||||||
|
|
||||||
|
def sign(private_key, sign_str): |
||||||
|
private_key = load_pem_private_key(data=format_private_key( |
||||||
|
private_key).encode('UTF-8'), password=None, backend=default_backend()) |
||||||
|
message = sign_str.encode('UTF-8') |
||||||
|
signature = private_key.sign(message, PKCS1v15(), SHA256()) |
||||||
|
sign = b64encode(signature).decode('UTF-8').replace('\n', '') |
||||||
|
return sign |
||||||
|
|
||||||
|
|
||||||
|
def decrypt(nonce, ciphertext, associated_data, apiv3_key): |
||||||
|
key_bytes = apiv3_key.encode('UTF-8') |
||||||
|
nonce_bytes = nonce.encode('UTF-8') |
||||||
|
associated_data_bytes = associated_data.encode('UTF-8') |
||||||
|
data = b64decode(ciphertext) |
||||||
|
aesgcm = AESGCM(key_bytes) |
||||||
|
return aesgcm.decrypt(nonce_bytes, data, associated_data_bytes).decode('UTF-8') |
||||||
|
|
||||||
|
|
||||||
|
def format_private_key(private_key): |
||||||
|
pem_start = '-----BEGIN PRIVATE KEY-----\n' |
||||||
|
pem_end = '\n-----END PRIVATE KEY-----' |
||||||
|
if not private_key.startswith(pem_start): |
||||||
|
private_key = pem_start + private_key |
||||||
|
if not private_key.endswith(pem_end): |
||||||
|
private_key = private_key + pem_end |
||||||
|
return private_key |
||||||
|
|
||||||
|
|
||||||
|
def format_certificate(certificate): |
||||||
|
pem_start = '-----BEGIN CERTIFICATE-----\n' |
||||||
|
pem_end = '\n-----END CERTIFICATE-----' |
||||||
|
if not certificate.startswith(pem_start): |
||||||
|
certificate = pem_start + certificate |
||||||
|
if not certificate.endswith(pem_end): |
||||||
|
certificate = certificate + pem_end |
||||||
|
return certificate |
||||||
|
|
||||||
|
|
||||||
|
def verify(timestamp, nonce, body, signature, certificate): |
||||||
|
sign_str = '%s\n%s\n%s\n' % (timestamp, nonce, body) |
||||||
|
public_key_str = dump_public_key(certificate) |
||||||
|
public_key = load_pem_public_key(data=public_key_str.encode('UTF-8'), backend=default_backend()) |
||||||
|
message = sign_str.encode('UTF-8') |
||||||
|
signature = b64decode(signature) |
||||||
|
try: |
||||||
|
public_key.verify(signature, sign_str.encode('UTF-8'), PKCS1v15(), SHA256()) |
||||||
|
except InvalidSignature: |
||||||
|
return False |
||||||
|
return True |
||||||
|
|
||||||
|
|
||||||
|
def certificate_serial_number(certificate): |
||||||
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, format_certificate(certificate)) |
||||||
|
try: |
||||||
|
res = cert.get_signature_algorithm().decode('UTF-8') |
||||||
|
if res != 'sha256WithRSAEncryption': |
||||||
|
return None |
||||||
|
return hex(cert.get_serial_number()).upper()[2:] |
||||||
|
except: |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
def dump_public_key(certificate): |
||||||
|
cert = crypto.load_certificate(crypto.FILETYPE_PEM, format_certificate(certificate)) |
||||||
|
public_key = crypto.dump_publickey(crypto.FILETYPE_PEM, cert.get_pubkey()).decode("utf-8") |
||||||
|
return public_key |
@ -1,232 +0,0 @@ |
|||||||
#!/usr/bin/env python |
|
||||||
# -*- coding:utf-8 -*- |
|
||||||
# project: 4月 |
|
||||||
# author: NinEveN |
|
||||||
# date: 2021/4/15 |
|
||||||
|
|
||||||
import time |
|
||||||
import random |
|
||||||
from Cryptodome.PublicKey import RSA |
|
||||||
from Cryptodome.Signature import pkcs1_15 |
|
||||||
from Cryptodome.Hash import SHA256 |
|
||||||
from base64 import b64encode |
|
||||||
|
|
||||||
mchid = '1608486112' |
|
||||||
app_id = 'wx390e5985fd3699e6' |
|
||||||
serial_no = '27DADA4D2921CDD66B8B20A68276F09B90754922' |
|
||||||
private_key = '''-----BEGIN PRIVATE KEY----- |
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDbXhNoHljrkS8T |
|
||||||
jXg3+tTkaoOol8FDt0jSGckhzX46gkS16CWYwTthBKurfFtynsJe4uDOphS1ge/r |
|
||||||
QEU3+rWNxqa8o6gHSpp2UTYAz/1oYOlXuSa4NA1uD47lmVZJzad2ybWDSsoeRjFj |
|
||||||
c+X6F0ZiE3FmdN1iHz8NmbP99foih4smv15X+wX5DrsuLuPVHNB4D0fqvY7P5PO3 |
|
||||||
wUQXWQNezCYzpPoXX2H/UkyFEFZhWk6/9z3aAkYmqfd6IWPHewOqnVoQRKmo5bXb |
|
||||||
yWbB+QIl/HcSfNtq869s5lLGR2Rl2UX8IFXCcXnRPhSAVIeWfXN26Pc9dz9N4VTU |
|
||||||
yiZ8Y6AHAgMBAAECggEABdS0U1orJufPBogGIAbMzd1+7mZKPtCKYPtKe1mI92kr |
|
||||||
BmLLTQol1+hV39MIYz2RERCaxSNo/YIcrHYi4OALH1+eYvk+qCL1hBuYgeEFbVbW |
|
||||||
HPzQ6KiJitljBPtUbdXHk8K8zmaYhMF84pXcEQ+5UTYPF5gXoloORQBG5oM5SN2g |
|
||||||
2GTgYw1cpDzzRRwnmpvYd1ZydYNj8m6k7I2L1pwzRS/6/whz1sScpfh+91w1IVM0 |
|
||||||
WT+pPSdiVtQ7ktmvcTrWj7eNIbcsptZ1QgSV3UHkU0xzLG9N1TqJdHOquunXRS7V |
|
||||||
iw/4NgXveXSTSQrmmZVS+Kdyc+z1iDqwOXmE7hjioQKBgQD7js+40NCAPLYXEfer |
|
||||||
UFvZ6kem9mJIzAUdeTdK4BjYJrcU+UsXRmcWJIPI9HWSr31f/fSfu2SKyBa6+dFF |
|
||||||
SeNHuHPPqQAXrsNuhFG1wcKoNybk7KrsQlXheK7br42565Tegz3UXOKMrnPPlukH |
|
||||||
ZZdlYmwBFjEJvIr9jxJvJoW6qQKBgQDfPb697vR8ruLaecPmsq920iC3AQRanYFK |
|
||||||
dW6U98JkCN9A/LXA1jGyDTEhtlja+5Ylp0M1EnlcZ079Jnvek5haSNnD0xb1nMy8 |
|
||||||
P/o0/eWWArTgfjfiJeq1tSdrinGhNz0+Vty74wnbS+5P18H23I5jBlGX5hyNkU4L |
|
||||||
axUfJM9jLwKBgCo/1xVkRNB04eRICT/FlFeqKHSbRvCRC37iv+2ca6/J+M/V+s2i |
|
||||||
7mdipJuYqzKCtNztayt0rrM8Xczzbjlj6n8+NH05FiHkIUCriomrTEUyVh72vNJH |
|
||||||
ZeMjgMK23mfOcEda5YSIQSh9mEfSQbsTTfUiLZ+VGZFYEEP7xo3Se31ZAoGBAJas |
|
||||||
LPYytq8EtrYwowktJwJydoQt2otybRYdRmKjCn/MASrypZWeu/Hpt3SCh1xdnAyT |
|
||||||
5OeILYMxcv2noMksIxMkwl3KNl/V0dVo9O4ZQ4DJGN3AMuWfI9g6iX2q9mCSUPKn |
|
||||||
W9owNbHegN1AyXhdinjJhf6Y4EKohN1uC9Z2WMcfAoGAW90Z2LkqG2fen+R62syP |
|
||||||
aaInnu9bitb9rVENCNGXQHdWmIYBMM5zrg8nX8xNJ+yeGQhgxE+YeSq4FOpe0JkA |
|
||||||
daWIhg++OHN2MBRutj7oL/AFAxyu467YA5+itEJLHNATbOr/s13S66nePNXox/hr |
|
||||||
bIX1aWjPxirQX9mzaL3oEQI= |
|
||||||
-----END PRIVATE KEY-----''' |
|
||||||
timestamp = str(int(time.time())) |
|
||||||
import uuid |
|
||||||
|
|
||||||
wx_public_key = ''' |
|
||||||
|
|
||||||
''' |
|
||||||
|
|
||||||
|
|
||||||
def get_nonce_str(): |
|
||||||
""" |
|
||||||
获取随机字符串 |
|
||||||
:return: |
|
||||||
""" |
|
||||||
return str(uuid.uuid4()).replace('-', '') |
|
||||||
|
|
||||||
|
|
||||||
nonce_str = get_nonce_str() |
|
||||||
|
|
||||||
|
|
||||||
def sign_str(method, url_path, request_body): |
|
||||||
""" |
|
||||||
生成欲签名字符串 |
|
||||||
""" |
|
||||||
sign_list = [ |
|
||||||
method, |
|
||||||
url_path, |
|
||||||
timestamp, |
|
||||||
nonce_str, |
|
||||||
request_body |
|
||||||
] |
|
||||||
return '\n'.join(sign_list) + '\n' |
|
||||||
|
|
||||||
def sign_response_str(timestamp,nonce_str,request_body): |
|
||||||
""" |
|
||||||
生成欲签名字符串 |
|
||||||
""" |
|
||||||
sign_list = [ |
|
||||||
timestamp, |
|
||||||
nonce_str, |
|
||||||
request_body |
|
||||||
] |
|
||||||
return '\n'.join(sign_list) + '\n' |
|
||||||
|
|
||||||
def sign(sign_str): |
|
||||||
""" |
|
||||||
生成签名 |
|
||||||
""" |
|
||||||
rsa_key = RSA.importKey(private_key) |
|
||||||
signer = pkcs1_15.new(rsa_key) |
|
||||||
digest = SHA256.new(sign_str.encode('utf8')) |
|
||||||
sign = b64encode(signer.sign(digest)).decode('utf8') |
|
||||||
return sign |
|
||||||
|
|
||||||
|
|
||||||
def authorization(method, url_path, request_body): |
|
||||||
""" |
|
||||||
生成Authorization |
|
||||||
""" |
|
||||||
str = "" |
|
||||||
if isinstance(request_body, dict): |
|
||||||
str = json.dumps(request_body) |
|
||||||
signstr = sign_str(method, url_path, str) |
|
||||||
s = sign(signstr) |
|
||||||
authorization = 'WECHATPAY2-SHA256-RSA2048 ' \ |
|
||||||
'mchid="{mchid}",' \ |
|
||||||
'nonce_str="{nonce_str}",' \ |
|
||||||
'signature="{sign}",' \ |
|
||||||
'timestamp="{timestamp}",' \ |
|
||||||
'serial_no="{serial_no}"'. \ |
|
||||||
format(mchid=mchid, |
|
||||||
nonce_str=nonce_str, |
|
||||||
sign=s, |
|
||||||
timestamp=timestamp, |
|
||||||
serial_no=serial_no |
|
||||||
) |
|
||||||
return authorization |
|
||||||
|
|
||||||
|
|
||||||
from datetime import timezone |
|
||||||
|
|
||||||
import json |
|
||||||
|
|
||||||
post_data = { |
|
||||||
'appid': app_id, # 小程序ID |
|
||||||
'mchid': mchid, # 商户号 |
|
||||||
'description': '向 FLY分发平台 充值', # 商品描述 |
|
||||||
'out_trade_no': '12021415113114521722839155', # 商户订单号 |
|
||||||
# 'time_expire': pay_data.get('time_expire'), # 交易结束时间 示例值:2018-06-08T10:34:56+08:00 |
|
||||||
'attach': json.dumps({'user_id': 11}), # 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用 |
|
||||||
'notify_url': 'https://app.hehelucky.cn/api/v1/fir/server/pay_success', # 通知地址 |
|
||||||
'amount': { |
|
||||||
'total': 10, # 订单总金额,单位为分。示例值:100 |
|
||||||
'currency': 'CNY' |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
def make_pay_pc(): |
|
||||||
auth = authorization('POST', '/v3/pay/transactions/native', post_data) |
|
||||||
print(auth) |
|
||||||
import requests |
|
||||||
headers = { |
|
||||||
'Authorization': auth |
|
||||||
} |
|
||||||
req = requests.post('https://api.mch.weixin.qq.com/v3/pay/transactions/native', json=post_data, headers=headers) |
|
||||||
|
|
||||||
print(req.text) |
|
||||||
print(req.status_code) |
|
||||||
|
|
||||||
|
|
||||||
def get_wx_cert(): |
|
||||||
auth = authorization('GET', '/v3/certificates', '') |
|
||||||
print(auth) |
|
||||||
import requests |
|
||||||
headers = { |
|
||||||
'Authorization': auth |
|
||||||
} |
|
||||||
req = requests.get('https://api.mch.weixin.qq.com/v3/certificates', headers=headers) |
|
||||||
|
|
||||||
print(req.text) |
|
||||||
print(req.status_code) |
|
||||||
|
|
||||||
|
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM |
|
||||||
import base64 |
|
||||||
|
|
||||||
cert_info = {"data": [{"effective_time": "2021-04-15T14:08:03+08:00", |
|
||||||
"encrypt_certificate": {"algorithm": "AEAD_AES_256_GCM", "associated_data": "certificate", |
|
||||||
"ciphertext": "sOuR8QHOOh0QkLBtrsFdrsY6FohdN7JcI6saS7pb/YNowyLlzRQIFmge7C3dILt5DVJu8kzB7dPbqcQmP/J8INSyUqZtFOVVifszWulVyQo23vvC6kT9tHHp/IoPog/Z15rmpyWwmjtrNeWIWILHmmdeSruZKgop4C9L50vQoTBRS3z6JyrACX4OceYwgNR3ICZAu/TNofI6dp92vI9RwIylQxsV1dnzi+m9pYl9IvbF/OGzJazyBPurynFcdcHfq6G7BfKnlmGswh4u5uBqXBoSfzjF14T0clN+sLiZePL0wfyxVRNtAPFkFvsbtiy4ABIbkzwI7T+1Ne/RnLZizpq/4igkILfbtSzBEWO+KRSq2E3ejfKULMZA0uHe0NBVpaAf8f9jR3Bw/16c28hnxidJ+sjf/GMlhm+F3WgzrB7GRuDL97BTdjBKHV5Hm3Q/f94HUrZCUtmoMSihde/I2IE8bI0IPgnuFlOYGZm46yuXtj2aZcr2l9HaT8xCeNnUCPKzY3arVcVZKygKjnTu8Tyvsk4qV54SZTsiQ+hdQohim154401VnP7A2UsKvYQn14xvONkk3POwehe6SJrVwnjlODrKn9qdb2bo0w0yPyV8GkVqi3nn7Eh+Hnq1g6bJb0HXDOpkuf0JF/W0dceMBcTvFTvE0SDAsJgqoIVV1+B4nTCAEbTzmJ5mQKJFL2zzoSXxUZ9ORtWDNzq0Eo0mMnYuivm2fR2IvrjCYAxSXH9X54SmGiENg9usLLfmkp5l0wJqQ29PLB1LEbxI2YonOm0babMd8qfk2d3DVNhm/Uk6uqA+Trk8OtmCTHD6Yt7J4Iz4bqmL4q/1ea2FTIARkg3a+QKj2Yt2tDZeaBHeAsS9et8U82GdP0MQF61OTFhq/NknhyfGUXA6r4lgDN+tmZJyWGkwRhNphXqU6C57JWl8jU2CydPfmTORSlKTScr0fbAYsZQees0TnqnhAYAUFRNsGjdtmQ4fHQDSvXr9ner+e4bsQTqAXzGUYDwtgEjDh26Bn8jpHTkzfC9s491XIHKD5AueNv62JlM4fIDrqJ9o6q2a/skL2EWlegCWWena0eUh+fQdMYzuYg4VAmttk6fwr5S9h15PhyKcPugDyKuXjd3TGVF6oN+pGlyksDUJqrYYvcx16yNdpM4n+B+gf8uCXHpnRd6Ws/g37B0JZUdlIbx6BnXCVGzb8JTHkUOY6ZTQ0ArRn169Ta2DVPUmTIrxID7SZsvYiWiuKywXKfgCdEo3pBPZNthrtBp5GMsJg5dhjsFxsT28FSKolJRfxXViuEs36ybaI9Ks2g6rnBQ8AlpaV6RzWzR8QtTsF481rFK5mC/eaYXFGOGnNmyp12B4ER5UfEm88BBf4xiqk9ZZu8yem7EsomLrovPbF4szYSVyHY2SYalYyjKDZFdH21YUE9lvgQLsBt8LWfOG8PABuUQpEc3a/zVv5F5h+U/oHcdWGirOCIUgI+hk579hpiIdMpOO5YhSrpouuCqX8D+nYpkSbndBGQycfxlTtL2dx0f5DwKqHIPSsKd79F0tqKqodQuRm00KCuqINDNM6TcEwvN8liXy5mgHEbBtWGYlmg6a0Iufy3PvgJkB4hR/gqkEswp7YIJqR2BYoPWLVSROq+d8ExMNHPjihrFRaHDvUecKso7F8Sfq6N6VhScYM6QPk9ow+shc2nsbU+wBl3kHsgaYras+ku+5iHWT8QQk6vWG+RFrfiGfOFzJTXBslt/45ZUc5EdaSUOz9JK+3q8twIl0EBmUM8uThT3uM31/ShdH2ROYz5mdYANERt/23fXOzY4brQJ1gGJ+2wlqKPG/IBemT+1m6z3fGdvPvoQFk02OYmLYIoR0cNa5Y3/MUkGysggflg==", |
|
||||||
"nonce": "1ef9f91582da"}, "expire_time": "2026-04-14T14:08:03+08:00", |
|
||||||
"serial_no": "5091F0B7E805ACD1EDB2A3B7DD04B3A67D177F37"}]} |
|
||||||
|
|
||||||
|
|
||||||
def decrypt_cert(nonce, ciphertext, associated_data): |
|
||||||
key = "60DbP621a9C3162dDd4AB9c2O15a005L" |
|
||||||
|
|
||||||
key_bytes = str.encode(key) |
|
||||||
nonce_bytes = str.encode(nonce) |
|
||||||
ad_bytes = str.encode(associated_data) |
|
||||||
data = base64.b64decode(ciphertext) |
|
||||||
|
|
||||||
aesgcm = AESGCM(key_bytes) |
|
||||||
return aesgcm.decrypt(nonce_bytes, data, ad_bytes) |
|
||||||
|
|
||||||
# encrypt_certificate = cert_info['data'][0]['encrypt_certificate'] |
|
||||||
# a=decrypt_cert(encrypt_certificate['nonce'],encrypt_certificate['ciphertext'],encrypt_certificate['associated_data']) |
|
||||||
# print(a) |
|
||||||
# # |
|
||||||
# make_pay_pc() |
|
||||||
|
|
||||||
request_body={'id': '65cce1b4-bbb7-541c-b063-5f284f650196', 'create_time': '2021-04-16T18:12:19+08:00', 'resource_type': 'encrypt-resource', 'event_type': 'TRANSACTION.SUCCESS', 'summary': '支付成功', 'resource': {'original_type': 'transaction', 'algorithm': 'AEAD_AES_256_GCM', 'ciphertext': 'rx9sLCTnnrS3GH/WqSMr+eEJqBFNo75Lb9ZW1EnoOhJ/tbJhXYl3biIR+tj1OK7qhj6ctRFeDc/zwwZ9Y5gPuWMADHd7KhsGmFdhJj4ap+UNq6UFO8g2tF3mGRQ11Cj2BCj0F+31EF8n9UCjm9SE1f0vOSwZFM6tx3/pkEgWyBbHqienr2eWZVlv8qlmUqioYoZNdJbfGNLfD/Uo2OqJg2EtXeEmdglrcC6b5hTuqGD/NcBrj83OvgsaimEbtzhphTGWRWKw2/qJtLVGKTHTMFrJ7g990i6w35vEKALWbAKsDIpCtIwPxYXlxtfxMZsLI/CYLN3Oa0W0DeR7GF+TBYgFsbvMAeryq9plbQm50qMDLfuhXcxIc7XutWGREMAmoS8e8NFtMhhi39QA4BVxSiQvND7qPXQBEESAVL+VDyCDN/SlEU2bEu+E6XD1o5LL/hvOlBnXFtm7mXbKj4j2M1II9De2oYD5R25EOXUi5oB24opaPXgHubSr6QpApQrWl3DGVMsaHVsaB4tWHH8XRaJXZpdDjxzz9DtmmTu7ZCBQifWqmC5vb+78f2+9gEavkHbL4XisKf/IFGUxzW3bKi/042xHVB2Z/JM49+Y=', 'associated_data': 'transaction', 'nonce': 'BFK77WOGPp21'}} |
|
||||||
|
|
||||||
wx_headers = { |
|
||||||
'HTTP_WECHATPAY_NONCE': 's6pY4nneUbgFh9nJNiSYv1h9ZLC7ruSj', |
|
||||||
'HTTP_WECHATPAY_SERIAL': '5091F0B7E805ACD1EDB2A3B7DD04B3A67D177F37', |
|
||||||
'HTTP_WECHATPAY_SIGNATURE': 'T8oiVc5oFsEfNDpOSdTeTiVPyO6rbx9MDEV24EtB4myPZhVYmSsJWBrtPXbYemaEPFJTtkrlzGsc3Rm6PsZoOnxSvKVIbvy60So6y36nRSmgawolTK8ruRorm0qaeRJtUAAsJImmQrMlXSqy5YaZqW3GbbLci4r2UYYoDfaNwSTJiwgit3HFYiIL6rYrtUJ3ekwMW5oyT4Aio37ulGnxymC6Nnyqbos4zpo9y6Nc8upf8rStUJ5ya3d+rZxG6fsPkUOVNz9NwaaKQk2YdelpHjd8K/cfaVNJ1ot82x8ArbEWd0k/Iz02LtLLelfF1V0w//0zmukWxWaCV2K9I+ir6w==', |
|
||||||
'HTTP_WECHATPAY_TIMESTAMP': '1618567939', |
|
||||||
} |
|
||||||
HTTP_WECHATPAY_TIMESTAMP = wx_headers.get('HTTP_WECHATPAY_TIMESTAMP','') |
|
||||||
HTTP_WECHATPAY_NONCE = wx_headers.get('HTTP_WECHATPAY_NONCE','') |
|
||||||
HTTP_WECHATPAY_SIGNATURE = wx_headers.get('HTTP_WECHATPAY_SIGNATURE','') |
|
||||||
|
|
||||||
WX_CERT_KEY=b'-----BEGIN CERTIFICATE-----\nMIID3DCCAsSgAwIBAgIUUJHwt+gFrNHtsqO33QSzpn0XfzcwDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjEwNDE1MDYwODAzWhcNMjYwNDE0MDYwODAzWjBuMRgwFgYDVQQDDA9U\nZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl\nbnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGDAJDTjERMA8GA1UEBwwIU2hlblpo\nZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDONPXG4eQktFWEVbzD\n7ev8eMK9EEL4BBMCcvQCn76PF3wgbvYSO2CLpp7qP1NNJhFDp5ItUOtvz68AD6u1\nPNWxkOMJqdWPXRpewdUo5nr8lZCR4i3XiY+OSO/cA8C8K8mDvVNsMT1wPMp75Vil\nBL2gK5lfjj/Kjoi/aJSU//gQDpuZ+4GHBFQcOK4QqmPY8rdqhX6z93cFEkFDGPUw\nLXE9jZvYGGf5xwKPFKLvjrrg8zX+znJOXOPQtpvKD/RBfHI9ebv/PxepiHw3prM9\nLoIf6FsVaqFqV1tTJZjAgPAgEhRqM53+8PxZTdxJZDCWv/vf4BvG3Bcfw0CSTlYO\npQpJAgMBAAGjgYEwfzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DBlBgNVHR8EXjBc\nMFqgWKBWhlRodHRwOi8vZXZjYS5pdHJ1cy5jb20uY24vcHVibGljL2l0cnVzY3Js\nP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3NTQ5ODQ2QzAxQzNFOEVCRDIwDQYJ\nKoZIhvcNAQELBQADggEBAA508oXmLC0x0VdsW0ThOeN+BXzuqLsKec945BZz0qPm\n/fc3Wn2Ro5yb5tsh94aqyJGmOWgZsWo/nQk2XcE5BPLn7rX0q7uMGCMbJbxfFiuS\nNuJNDSXamYUCGRXgEsZn8mh8EwZ7MKefq2LxtX9VvJF3o1KsvOWaltr5Ra3DBh1F\nghxzyOOimiqn+duT9ZbbA0nfGJjSsLq61rzc/qBMuBsdJnqeYMFs/+AIDuGYNbOj\nFTcK0+fEFfU1H99RGCu4j9jQlOqnB6uXOnr7VSQPfMEpKd6xTmaa01YHWu9oAijO\nupWVgJZI0cC3/L5s5OzlUp8Bc84NOFxSN0yXAZtH1Bc=\n-----END CERTIFICATE-----' |
|
||||||
|
|
||||||
rsa_key = RSA.importKey(WX_CERT_KEY) |
|
||||||
public_key=rsa_key.publickey().exportKey() |
|
||||||
|
|
||||||
Wechatpay_Signature = base64.b64decode(HTTP_WECHATPAY_SIGNATURE) |
|
||||||
|
|
||||||
res_sign_str = sign_response_str(HTTP_WECHATPAY_TIMESTAMP,HTTP_WECHATPAY_NONCE,json.dumps(request_body)) |
|
||||||
print(res_sign_str) |
|
||||||
print(Wechatpay_Signature) |
|
||||||
|
|
||||||
key = RSA.importKey(public_key) |
|
||||||
|
|
||||||
# 验证签名 |
|
||||||
verifer = pkcs1_15.new(key) # 使用公钥创建校验对象 |
|
||||||
|
|
||||||
hasher = SHA256.new(Wechatpay_Signature)# 对收到的消息文本提取摘要 |
|
||||||
#hasher.update(message.encode()) |
|
||||||
|
|
||||||
verifer.verify(hasher, res_sign_str.encode("utf-8")) # 校验摘要(本来的样子)和收到并解密的签名是否一致 |
|
||||||
|
|
||||||
|
|
||||||
# rsa_key = RSA.importKey(private_key) |
|
||||||
# signer = pkcs1_15.new(rsa_key) |
|
||||||
# digest = SHA256.new(sign_str.encode('utf8')) |
|
||||||
# sign = b64encode(signer.sign(digest)).decode('utf8') |
|
Loading…
Reference in new issue