653 lines
31 KiB
Python
653 lines
31 KiB
Python
#!/bin/python
|
||
#coding: utf-8
|
||
# +-------------------------------------------------------------------
|
||
# | system: django-vue-lyadmin
|
||
# +-------------------------------------------------------------------
|
||
# | Author: lybbn
|
||
# +-------------------------------------------------------------------
|
||
# | QQ: 1042594286
|
||
# +-------------------------------------------------------------------
|
||
|
||
# ------------------------------
|
||
# 微信支付v3(需要APIV3 的apikey)(直连方式-普通商户)
|
||
# ------------------------------
|
||
# 支付官网文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml
|
||
# ------------------------------
|
||
|
||
from config import WXPAY_CLIENT_CERT_PATH,WXPAY_CLIENT_KEY_PATH,WXPAY_APPID,WXPAY_MCHID,WXPAY_APIKEY,WXPAY_SERIAL_NO,WXPAY_CERT_DIR,WXPAY_CERT_DIR_RESPONSE,WXPAY_APPID_APP,WX_GZH_APPID,WX_GZH_APPSECRET
|
||
import requests
|
||
import json
|
||
import time
|
||
import os
|
||
import uuid
|
||
import random
|
||
import string
|
||
from datetime import datetime
|
||
from base64 import b64decode, b64encode
|
||
from Cryptodome.PublicKey import RSA
|
||
from Cryptodome.Signature import pkcs1_15
|
||
from Cryptodome.Hash import SHA256
|
||
from cryptography.hazmat.primitives.hashes import SHA256 as SHA256_1
|
||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||
from cryptography.x509 import load_pem_x509_certificate
|
||
from cryptography.hazmat.backends import default_backend
|
||
from cryptography.exceptions import InvalidSignature, InvalidTag
|
||
import hashlib
|
||
from django.core.cache import cache
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
|
||
from enum import Enum
|
||
|
||
|
||
class RequestType(Enum):
|
||
GET = 0
|
||
POST = 1
|
||
|
||
|
||
class WeChatPayType(Enum):
|
||
JSAPI = 0
|
||
APP = 1
|
||
H5 = 2
|
||
NATIVE = 3
|
||
MINIPROG = 4
|
||
|
||
# 获取加密
|
||
def get_sign(sign_str):
|
||
rsa_key = RSA.importKey(open(WXPAY_CLIENT_KEY_PATH).read())
|
||
signer = pkcs1_15.new(rsa_key)
|
||
digest = SHA256.new(sign_str.encode('utf8'))
|
||
sign = b64encode(signer.sign(digest)).decode('utf-8')
|
||
return sign
|
||
|
||
#uniapp调起微信支付只支持MD5加密
|
||
def get_sign_md5(sign_str):
|
||
# 定义MD5
|
||
hmd5 = hashlib.md5()
|
||
# 生成MD5加密字符串
|
||
hmd5.update(sign_str)
|
||
# 获取MD5字符串
|
||
sig = hmd5.hexdigest()
|
||
# 将小写字母切换成大写
|
||
return sig.upper()
|
||
|
||
def get_sign_sha1(sign_str):
|
||
sha1 = hashlib.sha1()
|
||
sha1.update(sign_str.encode("utf-8"))
|
||
sig = sha1.hexdigest()
|
||
return sig
|
||
|
||
class WxAppPay:
|
||
"""
|
||
微信第三方app支付v3(直连方式-普通商户)
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.appid = WXPAY_APPID
|
||
self.appid_app = WXPAY_APPID_APP
|
||
self.mchid = WXPAY_MCHID
|
||
self.apikey = WXPAY_APIKEY
|
||
self.url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/app'#微信app支付
|
||
self.url2 = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi' # 微信小程序支付
|
||
self.url3 = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds' # 微信申请退款、查询单笔退款
|
||
self.url4 = 'https://api.mch.weixin.qq.com/v3/transfer/batches' # 商家转账到零钱(提现--原来得【企业付款到零钱】的升级款)
|
||
self.url5 = 'https://api.mch.weixin.qq.com/v3/pay/transactions/native' # Native支付(需要关联appid(微信小程序\微信公众号(认证服务号))才能使用)
|
||
self.gate_way = 'https://api.mch.weixin.qq.com'
|
||
self.notify_url = "https://weixin.qq.com/"#需要回调的默认url,实际上要填写自己的回调地址:要求https,且不能携带参数如:https://www.weixin.qq.com/wxpay/pay.php
|
||
self.serial_no = WXPAY_SERIAL_NO # 商户号证书序列号
|
||
self.certificates = []#动态请求微信支付平台证书列表(验证回调应答签名要使用,目前只能通过api访问)
|
||
self.cert_dir = WXPAY_CERT_DIR_RESPONSE + '/' if WXPAY_CERT_DIR_RESPONSE else None## 微信支付平台证书缓存目录,减少证书下载调用次数。 初始调试时可不设置,调试通过后再设置,示例值:'./cert'
|
||
self.load_local_certificates()
|
||
|
||
|
||
|
||
# 统一下单(微信app支付)
|
||
def payorder(self, order_no, total, description,attach,notify_url=None):
|
||
data = {
|
||
"mchid": self.mchid,
|
||
"out_trade_no": order_no,
|
||
"appid": self.appid_app,
|
||
"description": description,
|
||
"attach":attach,#标注,微信支付后会原样返回
|
||
"notify_url": notify_url,#必填项
|
||
"amount": {
|
||
"total": int(total),#订单总金额,单位为分
|
||
"currency": "CNY"
|
||
},
|
||
}
|
||
data = json.dumps(data) # 只能序列化一次
|
||
#构造微信支付签名验证
|
||
random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||
time_stamps = str(int(time.time()))
|
||
"""
|
||
HTTP请求方法\n
|
||
URL\n
|
||
请求时间戳\n
|
||
请求随机串\n
|
||
请求报文主体\n
|
||
"""
|
||
sign_str = f"POST\n{'/v3/pay/transactions/app'}\n{time_stamps}\n{random_str}\n{data}\n"
|
||
sign = get_sign(sign_str)
|
||
authorization = 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{self.mchid}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{self.serial_no}"'
|
||
headers = {'Content-Type': 'application/json', 'Accept': 'application/json','Authorization':authorization}
|
||
response = requests.post(self.url, data=data, headers=headers)
|
||
logger.info("微信app支付下单返回消息,订单号:%s,金额:%s,微信返回信息:%s" % (order_no,total/100,response.text))
|
||
"""
|
||
正常返回
|
||
{
|
||
"prepay_id": "wx261153585405162d4d02642eabe7000000"
|
||
}
|
||
预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时(直接返回给app端,支付时需要)
|
||
"""
|
||
res = json.loads(response.content)
|
||
random_str_app = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||
time_stamps_app = str(int(time.time()))
|
||
sign_str_app = f"{WXPAY_APPID_APP}\n{time_stamps_app}\n{random_str_app}\n{res['prepay_id']}\n"
|
||
sign_str_app_sign = get_sign(sign_str_app)
|
||
res['paySign'] = sign_str_app_sign
|
||
res['signType'] = 'RSA'
|
||
res['nonceStr'] = random_str_app
|
||
res['partnerid'] = WXPAY_MCHID
|
||
res['timestamp'] = time_stamps_app
|
||
res['oderno'] = order_no
|
||
return res
|
||
|
||
# 统一下单(微信小程序支付、微信公众号网页支付(JSAPI))
|
||
def payorder_jsapi(self, order_no, total, description,attach, openid,notify_url=None):
|
||
data = {
|
||
"mchid": self.mchid,
|
||
"out_trade_no": order_no,
|
||
"appid": self.appid,
|
||
"description": description,
|
||
"attach": attach, # 标注,微信支付后会原样返回
|
||
"payer":{
|
||
"openid":openid
|
||
},#(支付者openid)用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid
|
||
"notify_url": notify_url, # 必填项
|
||
"amount": {
|
||
"total": int(total), # 订单总金额,单位为分(整数)
|
||
"currency": "CNY"
|
||
},
|
||
}
|
||
data = json.dumps(data) # 只能序列化一次
|
||
# 构造微信支付签名验证
|
||
random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||
time_stamps = str(int(time.time()))
|
||
"""
|
||
HTTP请求方法\n
|
||
URL\n
|
||
请求时间戳\n
|
||
请求随机串\n
|
||
请求报文主体\n
|
||
"""
|
||
sign_str = f"POST\n{'/v3/pay/transactions/jsapi'}\n{time_stamps}\n{random_str}\n{data}\n"
|
||
sign = get_sign(sign_str)
|
||
authorization = 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{self.mchid}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{self.serial_no}"'
|
||
headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': authorization}
|
||
response = requests.post(self.url2, data=data, headers=headers)
|
||
logger.info("微信jsapi支付下单返回消息,订单号:%s,金额:%s,微信返回信息:%s" % (order_no, total / 100, response.text))
|
||
"""
|
||
正常返回
|
||
{
|
||
"prepay_id": "wx261153585405162d4d02642eabe7000000"
|
||
}
|
||
预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时(直接返回给app端,支付时需要)
|
||
"""
|
||
res = json.loads(response.content)
|
||
if 'code' in res and 'message' in res:#如果获取失败返回失败信息
|
||
raise ValueError(res['message'])
|
||
random_str_app = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||
time_stamps_app = str(int(time.time()))
|
||
package = "prepay_id="+res['prepay_id']
|
||
sign_str_app = f"{WXPAY_APPID}\n{time_stamps_app}\n{random_str_app}\n{package}\n"
|
||
sign_str_app_sign_v3_rsa = get_sign(sign_str_app)
|
||
res['sign'] = sign_str_app_sign_v3_rsa
|
||
res['signType'] = 'RSA'
|
||
res['package'] = package
|
||
res['nonceStr'] = random_str_app
|
||
res['partnerid'] = WXPAY_MCHID
|
||
res['timestamp'] = time_stamps_app
|
||
res['oderno'] = order_no
|
||
return res
|
||
|
||
# 统一下单(用户扫商家生成的订单支付二维码支付)
|
||
def payorder_native(self, order_no, total, description, attach, notify_url=None):
|
||
data = {
|
||
"mchid": self.mchid,
|
||
"out_trade_no": order_no,
|
||
"appid": self.appid,
|
||
"description": description,
|
||
"attach": attach, # 标注,微信支付后会原样返回
|
||
"notify_url": notify_url, # 必填项
|
||
"amount": {
|
||
"total": int(total), # 订单总金额,单位为分(整数)
|
||
"currency": "CNY"
|
||
},
|
||
}
|
||
data = json.dumps(data) # 只能序列化一次
|
||
# 构造微信支付签名验证
|
||
random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||
time_stamps = str(int(time.time()))
|
||
"""
|
||
HTTP请求方法\n
|
||
URL\n
|
||
请求时间戳\n
|
||
请求随机串\n
|
||
请求报文主体\n
|
||
"""
|
||
sign_str = f"POST\n{'/v3/pay/transactions/native'}\n{time_stamps}\n{random_str}\n{data}\n"
|
||
sign = get_sign(sign_str)
|
||
authorization = 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{self.mchid}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{self.serial_no}"'
|
||
headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': authorization}
|
||
response = requests.post(self.url5, data=data, headers=headers)
|
||
logger.info("微信native支付下单返回消息,订单号:%s,金额:%s,微信返回信息:%s" % (order_no, total / 100, response.text))
|
||
"""
|
||
正常返回
|
||
{
|
||
"code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz"
|
||
}
|
||
预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时(直接返回给app端,支付时需要)
|
||
"""
|
||
res = json.loads(response.content)
|
||
if 'code' in res and 'message' in res: # 如果获取失败返回失败信息
|
||
raise ValueError(res['message'])
|
||
res['oderno'] = order_no
|
||
return res
|
||
|
||
# 微信【商家转账到零钱】(提现)采用apiv3接口(原来的微信企业付款(提现)企业付款到零钱新用户已无法申请)
|
||
# 开通要求:商户号已入驻90日且截止今日回推30天商户号保持连续不间的交易。
|
||
# 开通方法:商户进入微信支付【商户平台—>产品中心—>商家转账到零钱】,点击开通进入开通流程
|
||
# 开通产品后,商户进入微信支付【商户平台—>产品中心—>商家转账到零钱—>产品设置】,配置发起方式,开启验密批量API。并配置API 调用的IP 地址。
|
||
# 该接口原官网是支持最多3000比的同时转账,这里只支持同时一个用户转账,如需支持多用户的形式的,需要自行扩展
|
||
def cashout(self,order_no,amount,openid):
|
||
batch_name = "转账"
|
||
batch_remark = "转账"
|
||
data = {
|
||
"appid": self.appid,
|
||
"out_batch_no": order_no,
|
||
"batch_name": batch_name,#批次名字:示例值:2019年1月深圳分部报销单
|
||
"batch_remark": batch_remark, # 转账说明,UTF8编码,最多允许32个字符,示例值:2019年1月深圳分部报销单
|
||
"total_amount": int(amount), # int类型,转账总金额, 转账金额单位为“分”。转账总金额必须与批次内所有明细转账金额之和保持一致,否则无法发起转账操作
|
||
"total_num":1, # int类型 转账笔数 ,一个转账批次单最多发起三千笔转账。转账总笔数必须与批次内所有明细之和保持一致,否则无法发起转账操作
|
||
"transfer_detail_list":[#转账明细,最多三千笔
|
||
{
|
||
"out_detail_no": order_no,
|
||
"transfer_amount": int(amount),
|
||
"transfer_remark": batch_remark,
|
||
"openid": openid,
|
||
},
|
||
],
|
||
}
|
||
data = json.dumps(data) # 只能序列化一次
|
||
# 构造微信签名验证
|
||
random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||
time_stamps = str(int(time.time()))
|
||
"""
|
||
HTTP请求方法\n
|
||
URL\n
|
||
请求时间戳\n
|
||
请求随机串\n
|
||
请求报文主体\n
|
||
"""
|
||
sign_str = f"POST\n{'/v3/transfer/batches'}\n{time_stamps}\n{random_str}\n{data}\n"
|
||
sign = get_sign(sign_str)
|
||
authorization = 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{self.mchid}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{self.serial_no}"'
|
||
headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': authorization}
|
||
response = requests.post(self.url4, data=data, headers=headers)
|
||
logger.info("微信提现【商家转账到零钱】返回消息,订单号:%s,金额:%s,微信返回信息:状态码:%s:%s" % (order_no, amount / 100, response.status_code,response.text))
|
||
"""
|
||
正常返回:状态码200
|
||
{
|
||
"out_batch_no": "plfk2020042013",#自己的系统内部单号
|
||
"batch_id": "1030000071100999991182020050700019480001",#微信的交易标识单号
|
||
"create_time": "2015-05-20T13:29:35.120+08:00"
|
||
}
|
||
错误返回:非200状态码
|
||
{"code":"NO_AUTH","message":"商户号无权限"}
|
||
"""
|
||
return response
|
||
|
||
# 微信app退款申请
|
||
def refundsorder(self, out_refund_no,transaction_id,reason,refund,total,notify_url=None):
|
||
"""
|
||
当交易发生之后一年内,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
|
||
退款有一定延时,建议在提交退款申请后1分钟发起查询退款状态,一般来说零钱支付的退款5分钟内到账,银行卡支付的退款1-3个工作日到账。
|
||
参考官方API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_9.shtml
|
||
"""
|
||
data = {
|
||
"out_refund_no": out_refund_no,#新退款单号
|
||
"transaction_id": transaction_id,#原支付交易对应的微信订单号
|
||
"reason": reason,#退款原因
|
||
"notify_url": notify_url,#必填项,退款回调通知地址
|
||
"amount": {
|
||
"refund": int(refund),#退款金额,单位为分,只能为整数,不能超过原订单支付金额
|
||
"total": int(total),#原订单金额,单位为分
|
||
"currency": "CNY"
|
||
},
|
||
}
|
||
data = json.dumps(data) # 只能序列化一次
|
||
#构造微信支付签名验证
|
||
random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||
time_stamps = str(int(time.time()))
|
||
"""
|
||
HTTP请求方法\n
|
||
URL\n
|
||
请求时间戳\n
|
||
请求随机串\n
|
||
请求报文主体\n
|
||
"""
|
||
sign_str = f"POST\n{'v3/refund/domestic/refunds'}\n{time_stamps}\n{random_str}\n{data}\n"
|
||
sign = get_sign(sign_str)
|
||
authorization = 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{self.mchid}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{self.serial_no}"'
|
||
headers = {'Content-Type': 'application/json', 'Accept': 'application/json','Authorization':authorization}
|
||
response = requests.post(self.url, data=data, headers=headers)
|
||
logger.info("微信支付订单退款申请返回消息,退款单号:%s,金额:%s,微信返回信息:%s" % (out_refund_no,total/100,response.text))
|
||
"""
|
||
正常返回
|
||
{
|
||
"refund_id": "50000000382019052709732678859",
|
||
"out_refund_no": "1217752501201407033233368018",
|
||
"transaction_id": "1217752501201407033233368018",
|
||
"out_trade_no": "1217752501201407033233368018",
|
||
"channel": "ORIGINAL",
|
||
"user_received_account": "招商银行信用卡0403",
|
||
"success_time": "2020-12-01T16:18:12+08:00",
|
||
"create_time": "2020-12-01T16:18:12+08:00",
|
||
"status": "SUCCESS",
|
||
"funds_account": "UNSETTLED",
|
||
"amount": {
|
||
"total": 100,
|
||
"refund": 100,
|
||
"from": [
|
||
{
|
||
"account": "AVAILABLE",
|
||
"amount": 444
|
||
}
|
||
],
|
||
"payer_total": 90,
|
||
"payer_refund": 90,
|
||
"settlement_refund": 100,
|
||
"settlement_total": 100,
|
||
"discount_refund": 10,
|
||
"currency": "CNY"
|
||
},
|
||
"promotion_detail": [
|
||
{
|
||
"promotion_id": "109519",
|
||
"scope": "SINGLE",
|
||
"type": "DISCOUNT",
|
||
"amount": 5,
|
||
"refund_amount": 100,
|
||
"goods_detail": {
|
||
"merchant_goods_id": "1217752501201407033233368018",
|
||
"wechatpay_goods_id": "1001",
|
||
"goods_name": "iPhone6s 16G",
|
||
"unit_price": 528800,
|
||
"refund_amount": 528800,
|
||
"refund_quantity": 1
|
||
}
|
||
}
|
||
]
|
||
}
|
||
"""
|
||
res = json.loads(response.content)
|
||
return res
|
||
|
||
#查询单笔退款
|
||
def query_refundsorder(self,out_refund_no):
|
||
"""
|
||
:param out_refund_no:商户系统内部的退款单号,示例值:'1217752501201407033233368018'
|
||
微信官方API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_10.shtml
|
||
"""
|
||
newurl = self.url3+"/"+out_refund_no
|
||
path = newurl.split(self.gate_way)[1]
|
||
return self.request(path)
|
||
|
||
# 获取公众号全局access_token
|
||
# access_token是公众号的全局唯一接口调用凭据,access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效
|
||
def get_gzh_access_token(self):
|
||
key = "lybbn_gzh_access_token"
|
||
access_token = cache.get(key)
|
||
if access_token:
|
||
return access_token
|
||
api_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}"
|
||
get_url = api_url.format(WX_GZH_APPID, WX_GZH_APPSECRET)
|
||
r = requests.get(get_url)
|
||
json_data = json.loads(r.content)
|
||
if 'errcode' in json_data and json_data['errcode'] != 0: # 如果获取失败返回失败信息
|
||
logger.error("获取公众号access_token错误,微信返回错误信息:%s" % (json_data))
|
||
return None
|
||
access_token = json_data['access_token']
|
||
cache.set(key, access_token, 7100)
|
||
return access_token
|
||
|
||
# 获取公众号jsapi_ticket
|
||
# jsapi_ticket是公众号用于调用微信 JS 接口的临时票据,正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的 api 调用次数非常有限,频繁刷新jsapi_ticket会导致 api 调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket
|
||
def get_gzh_jsapi_ticket(self):
|
||
key = "lybbn_gzh_jsapi_ticket"
|
||
jsapi_ticket = cache.get(key)
|
||
if jsapi_ticket:
|
||
return jsapi_ticket
|
||
access_token = self.get_gzh_access_token()
|
||
if not access_token:
|
||
return None
|
||
api_url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi"
|
||
get_url = api_url.format(access_token)
|
||
r = requests.get(get_url)
|
||
json_data = json.loads(r.content)
|
||
if 'errcode' in json_data and json_data['errcode'] != 0: # 如果获取失败返回失败信息
|
||
logger.error("获取公众号jsapi_ticket错误,微信返回错误信息:%s" % (json_data))
|
||
return None
|
||
jsapi_ticket = json_data['ticket']
|
||
cache.set(key, jsapi_ticket, 7100)
|
||
return jsapi_ticket
|
||
|
||
# 微信公众号生成临时的签名(公众号网页h5分享时前端需要调用)
|
||
def get_gzh_h5_js_sign(self, url):
|
||
key = "lybbn_gzh_jsapi_ticket"
|
||
jsapi_ticket = cache.get(key)
|
||
if not jsapi_ticket:
|
||
jsapi_ticket = self.get_gzh_jsapi_ticket()
|
||
if not jsapi_ticket:
|
||
return None
|
||
appId = WX_GZH_APPID
|
||
timestamp = int(time.time())
|
||
nonceStr = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15))
|
||
data = {
|
||
'nonceStr': nonceStr,
|
||
'jsapi_ticket': jsapi_ticket,
|
||
'timestamp': timestamp,
|
||
'url': url,
|
||
}
|
||
signatureStr = '&'.join(['%s=%s' % (key.lower(), data[key]) for key in sorted(data)])
|
||
data['signature'] = get_sign_sha1(signatureStr)
|
||
data['appId'] = appId
|
||
return data
|
||
|
||
def load_local_certificates(self):
|
||
if self.cert_dir and os.path.exists(self.cert_dir):
|
||
for file in os.listdir(self.cert_dir):
|
||
if file.lower().endswith('.pem'):
|
||
f = open(self.cert_dir + file, encoding="utf-8")
|
||
certificate = self.load_certificate(f.read())
|
||
f.close()
|
||
now = datetime.utcnow()
|
||
if certificate:
|
||
if now >= certificate.not_valid_before and now <= certificate.not_valid_after:
|
||
self.certificates.append(certificate)
|
||
f.close()
|
||
#本地加载证书
|
||
def load_certificate(self,certificate_str):
|
||
try:
|
||
return load_pem_x509_certificate(data=certificate_str.encode('UTF-8'), backend=default_backend())
|
||
except:
|
||
return None
|
||
|
||
def build_authorization(self,path,method,mchid,serial_no,datas=None,nonce_str=None):
|
||
timeStamp = str(int(time.time()))
|
||
nonce_str = nonce_str or ''.join(str(uuid.uuid4()).split('-')).upper()
|
||
body = json.dumps(datas) if datas else ''
|
||
sign_str = '%s\n%s\n%s\n%s\n%s\n' % (method, path, timeStamp, nonce_str, body)
|
||
signature = get_sign(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 request(self, path, method=RequestType.GET, datas=None, skip_verify=False):
|
||
headers = {}
|
||
headers.update({'Content-Type': 'application/json'})
|
||
headers.update({'Accept': 'application/json'})
|
||
headers.update({'User-Agent': 'django-vue-lyadmin wechatpay v3 python sdk'})
|
||
authorization = self.build_authorization(
|
||
path,
|
||
'GET' if method == RequestType.GET else 'POST',
|
||
self.mchid,
|
||
self.serial_no,
|
||
datas=datas,
|
||
nonce_str=None)
|
||
headers.update({'Authorization': authorization})
|
||
if method == RequestType.GET:
|
||
response = requests.get(url=self.gate_way + path, headers=headers)
|
||
else:
|
||
response = requests.post(url=self.gate_way + path, json=datas, 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 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
|
||
cert_str = self.aes_decrypt(
|
||
nonce=nonce,
|
||
ciphertext=ciphertext,
|
||
associated_data=associated_data,
|
||
apiv3_key=self.apikey)
|
||
certificate = self.load_certificate(cert_str)
|
||
now = datetime.utcnow()
|
||
if certificate:
|
||
if now >= certificate.not_valid_before and now <= certificate.not_valid_after:
|
||
self.certificates.append(certificate)
|
||
if self.cert_dir:
|
||
if not os.path.exists(self.cert_dir):
|
||
os.makedirs(self.cert_dir)
|
||
if not os.path.exists(self.cert_dir + serial_no + '.pem'):
|
||
f = open(self.cert_dir + serial_no + '.pem', 'w')
|
||
f.write(cert_str)
|
||
f.close()
|
||
|
||
#验证签名是否属于微信回调
|
||
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')
|
||
cert_found = False
|
||
for cert in self.certificates:
|
||
if int('0x' + serial_no, 16) == cert.serial_number:
|
||
cert_found = True
|
||
certificate = cert
|
||
logger.info('wechatpay debug info verify_signature_1: %s' % cert)
|
||
break
|
||
if not cert_found:
|
||
self.update_certificates()
|
||
for cert in self.certificates:
|
||
if int('0x' + serial_no, 16) == cert.serial_number:
|
||
cert_found = True
|
||
certificate = cert
|
||
logger.info('wechatpay debug info verify_signature_2: %s' % cert)
|
||
break
|
||
if not cert_found:
|
||
return False
|
||
if not self.rsa_verify(timestamp, nonce, body, signature, certificate):
|
||
logger.info('wechatpay debug info verify_signature_3: %s' % cert)
|
||
return False
|
||
return True
|
||
|
||
#解密微信回调(支付成功)请求
|
||
def decrypt_callback(self, request):
|
||
headers = {}
|
||
headers.update({'Wechatpay-Signature': request.META.get('HTTP_WECHATPAY_SIGNATURE')})
|
||
headers.update({'Wechatpay-Timestamp': request.META.get('HTTP_WECHATPAY_TIMESTAMP')})
|
||
headers.update({'Wechatpay-Nonce': request.META.get('HTTP_WECHATPAY_NONCE')})
|
||
headers.update({'Wechatpay-Serial': request.META.get('HTTP_WECHATPAY_SERIAL')})
|
||
body = request.body
|
||
|
||
if isinstance(body, bytes):
|
||
body = body.decode('UTF-8')
|
||
logger.debug('Callback Header: %s' % headers)
|
||
logger.debug('Callback Body: %s' % body)
|
||
if not self.verify_signature(headers, body):
|
||
return None
|
||
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':
|
||
raise Exception('sdk does not support this algorithm')
|
||
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 = self.aes_decrypt(
|
||
nonce=nonce,
|
||
ciphertext=ciphertext,
|
||
associated_data=associated_data,
|
||
apiv3_key=self.apikey)
|
||
logger.debug('Callback resource: %s' % result)
|
||
return result
|
||
|
||
#aes解密
|
||
def aes_decrypt(self,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=key_bytes)
|
||
try:
|
||
result = aesgcm.decrypt(nonce=nonce_bytes, data=data, associated_data=associated_data_bytes).decode('UTF-8')
|
||
except:
|
||
result = None
|
||
return result
|
||
|
||
#rsa验证
|
||
def rsa_verify(self,timestamp, nonce, body, signature, certificate):
|
||
sign_str = f'{timestamp}\n{nonce}\n{body}\n'
|
||
message = sign_str.encode('UTF-8')
|
||
public_key = certificate.public_key()
|
||
signature = b64decode(signature)
|
||
try:
|
||
public_key.verify(signature, message, PKCS1v15(), SHA256_1())
|
||
except InvalidSignature:
|
||
return False
|
||
return True
|
||
|
||
if __name__=="__main__":
|
||
wx=WxAppPay()
|
||
print(wx.payorder("80480600",100,"订单支付").text) |