最近,公司要写一个app,支付方式为微信支付和支付宝支付,后台是用python写的,支付宝支付网上有很多的教程,但是微信支付,网上的教程却微乎其微,并且微信支付的文档写的确实垃圾的很,想要询问客服,却要排很长时间的队,但是功夫不负有心人,经过半个多月的折磨,终究是把它搞出来了。现在记录一下当时的心酸史。
首先要熟悉一下微信支付的整个流程让大家有一个宏观的认识。下面贴图
整个流程我做一个总结:
首先确定我们是用什么方式来进行微信支付的,二维码(native)?app?小程序?收款码?,然后去查找对应的文档https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml,前期的准本工作,我这里就不做叙述了,官方的文档写的很清楚,要生成商户号,生成证书、生成apiV3_key 等等。我这里就讲一下实现微信支付的流程。我用的是二维码(native)支付的,我就用这个做为例子来讲,app支付的话,需要前端引入微信的sdk,安卓有安卓的,ios有ios的,自己去找。
第一步:要调用微信的api接口,发起统一下单,请求成功后,微信支付会返回一个链接为 { "code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz" } 大概是这个样式的。
第二步:我们要将这个链接生成二维码,生成二维码的操作,是需要自己的代码实现的。网上一大堆的教程,我这里就不叙述了。
第三步:就是前端展示二维码,然后让用户来扫,扫完以后,就进入了手机微信页面,然后进行支付(这一步都是微信来做的,我们自己的程序不需要处理什么)
第四步:用户支付成功后,微信会回调我们第一步传给微信的一个回调地址,这个地址必须是https的,而且是公网可以访问,后面我会叙述。
第五部:如果没有收到回调,也不要着急,微信提供了订单查询的接口,可以查询订单的支付状态,但是需要我们主动去调用。
自此,整个微信支付的过程,就算完成了,后面对于回调成功后该如何处理,就是自己的事情了。下面到了小伙伴最爱的贴代码环节。
这里我要强调一下,微信支付文档中的很多细节方面的东西,确实需要完善,不然即使是单纯的接口调用,也会让你死去活来。
# -*- coding: UTF-8 -*-
import requests
import json
import time
import uuid
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from base64 import b64encode
import urllib3
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
import rsa
import qrcode
import config
import os
# APIV3_key = "D65555FE59D5408BA4722A211E843B6E"
class wepay():
def __init__(self):
self.appid = config.appid # appid
self.mchid = config.mchid # 商户号
self.APIV3_key = config.APIV3_key # apiv3 key
self.serial_no = config.serial_no # 商户平台证书序列号
self.sh_certificate_url = "/Users/xxx/cert/apiclient_key.pem" # 商户证书在服务器上的位置
self.sh_certificate_url_cert = "/Users/xxxx/cert/apiclient_cert.pem" # 商户证书在服务器上的位置
# http get 方法
def http_get(self, url, data, header=''):
try:
rs = requests.get(url, json=json.dumps(data), headers=header)
return rs.text, rs.headers
except:
pass
# http post
def http_post(self, url, data, header):
try:
response = requests.request("POST", url, headers=header, json=data)
return response.text
except:
pass
# 获取当前时间戳
def getTime(self):
return str(int(time.time()))
# 获取32位不带- 的uuid
def getUUID(self):
return "".join(str(uuid.uuid4()).split("-")).upper()
# 生成签名
def sign_str(self, method, url_path, timestamp, nonce_str, request_body):
if request_body:
# POST
sign_list = [
method,
url_path,
timestamp,
nonce_str,
request_body
]
return '\n'.join(sign_list) + '\n'
else:
# GET
sign_list = [
method,
url_path,
timestamp,
nonce_str
]
return '\n'.join(sign_list) + '\n\n'
# 调起支付签名
def pay_sign_str(self, appid, timestamp, nonce_str, prepay_id):
sign_list = [
appid,
timestamp,
nonce_str,
prepay_id
]
return '\n'.join(sign_list) + '\n'
# 签名加密
def sign(self, sign_str):
with open(self.sh_certificate_url, "r") as f:
private_key = f.read()
# 这里要注意的秘钥只能有三行
# -----BEGIN PRIVATE KEY-----
# ******************秘钥只能在一行,不能换行*****************
# -----END PRIVATE KEY-----
f.close()
pkey = RSA.importKey(private_key)
# h = SHA256.new(sign_str.encode('utf-8'))
# signature = PKCS1_v1_5.new(pkey).sign(h)
# sign = base64.b64encode(signature).decode()
signer = PKCS1_v1_5.new(pkey)
digest = SHA256.new(sign_str.encode('utf8'))
sign = b64encode(signer.sign(digest)).decode('utf-8')
return sign, private_key
# 生成获取平台证书请求头authorization 参数值
def authorization(self, method, url_path, nonce_str, timestamp, body=None, mchid=config.mchid):
# 加密子串
signstr = self.sign_str(method=method, url_path=url_path, timestamp=timestamp, nonce_str=nonce_str, request_body=body)
print(signstr)
# 加密后子串
sign1, key = self.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=sign1,
timestamp=timestamp,
serial_no=self.serial_no)
return authorization
# 记录一下这里的辛酸历史
# 解密证书需要四个参数,分别是随机数, APIv3key, 密文,和附加数据包,其中apiv3key是在微信商户平台的api安全里能找见,其余的三个参数都是在平台证书返回的报文中获取的,
# 2021年4月21号,我被整整卡了一天来解析ciphertext参数, 官方文档中,被没有提到ciphertext参数为平台公钥,我是排了5个小时的队,才咨询到了客服,客服才告诉我ciphertext参数为平台公钥,
# 然后我就开始解密,每次解密都会报错cryptography.exceptions.InvalidTag 网上给的答案是apiv3key有问题,经过我一天反复排查,发现是随机数出现了问题,随机数需要使用平台证书报文中返回的哪个随机数,微信文档中只字不提,简直是坑
# 特此记录,谨让以后开发此处的小伙伴不要踩坑。
# 报文解密
def decrypt(self, key, nonce, associated_data, ciphertext):
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)
# 生成验签名串和signature
def verify_signature(self, body, header):
timeStamp = header['Wechatpay-Timestamp']
nonce = header['Wechatpay-Nonce']
# print({timeStamp}, {nonce})
str_list=[
timeStamp,
nonce,
body,
]
# 验签名串
verify_signature_str = "\n".join(str_list)
wechatpay_signature = header['Wechatpay-Signature']
return verify_signature_str, wechatpay_signature
# native统一下单
def native_order(self, description= "", total=0):
# 订单号
# 这里有一个小坑,total一定要是int格式,否则即使生成了支付二维码,支付的时候,依然会报支付系统繁忙。
out_trade_no = self.getUUID()
order = {
'appid': self.appid, # appid
'mchid': self.mchid, # 商户号
'description': description, # 产品描述
'out_trade_no': out_trade_no, # 订单号,建议是32位的uuid
'notify_url': 'https://xxxx/wechat', # 通知地址,是微信支付成功后通知的地址,为自身服务的地址,也就是上文提到的微信回调地址
'amount': {
'total': total, # 支付金额(单位:分)
'currency': 'CNY' # 支持币种,CNY:人民币,境内商户号仅支持人民币。
}
}
data = json.dumps(order)
# 微信支付无论请求什么接口,都是需要附带请求头的,请求头的加密方式,也是写到了上面
authorization = self.authorization('POST', '/v3/pay/transactions/native', self.getUUID(), self.getTime(), data)
# 注意请求头这里,一定是这个格式,不能添加多的请求头参数,否则会报签名失败,这个地方调试了最少3天才获取到支付id
headers = {'Authorization': authorization, 'Content-Type': 'application/json'}
res1 = requests.post("https://api.mch.weixin.qq.com/v3/pay/transactions/native", data=data, headers=headers)
status_code = res1.status_code
if status_code == 200:
data_json = res1.json()
url = data_json['code_url']
return url, out_trade_no
else:
return None
# 生成支付二维码
def native_code(self, code_url, out_trade_no):
img = qrcode.make(code_url)
# 二维码存放在服务器上的路径
code_dir = os.path.join(config.report_public_root, 'image', 'code', 'native')
if not os.path.exists(code_dir):
os.makedirs(code_dir)
# 二维码存放在服务器上的名字,以订单号命名
code_name = "{}.{}".format(out_trade_no, "png")
patched_file_path = os.path.join(code_dir, code_name)
with open(patched_file_path, 'wb') as f:
img.save(f)
return patched_file_path.replace(config.report_public_root+'/', "")
# 支付状态查询
def select_order_status(self, out_trade_no):
authorization = self.authorization('GET', '/v3/pay/transactions/out-trade-no/{}?mchid={}'.format(out_trade_no, config.mchid), self.getUUID(), self.getTime())
# 注意请求头这里,一定是这个格式,不能添加多的请求头参数,否则会报签名失败,这个地方调试了最少3天才获取到支付id
headers = {'Authorization': authorization, 'Content-Type': 'application/json'}
res, header = self.http_get("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{}?mchid={}".format(out_trade_no, config.mchid), data=''.format(), header=headers)
return res
# 关闭订单
def close_order(self, out_trade_no):
data = {
"mchid": self.mchid
}
authorization = self.authorization('POST', '/v3/pay/transactions/out-trade-no/{}/close'.format(out_trade_no), self.getUUID(), self.getTime(), json.dumps(data))
# 注意请求头这里,一定是这个格式,不能添加多的请求头参数,否则会报签名失败,这个地方调试了最少3天才获取到支付id
headers = {'Authorization': authorization, 'Content-Type': 'application/json'}
res = requests.post("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{}/close".format(out_trade_no), data=json.dumps(data), headers=headers)
status_code = res.status_code
if status_code == 204:
return '订单关闭成功!'
else:
return None