背景
微信支付API v3的请求签名是比较麻烦的,需要读取pem证书进行签名。
微信支付接口文档
如何生成请求签名:https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-generation.html
实现代码
import datetime
import io
import random
import string
from urllib.parse import urlparse, quote, urlencode, urlunparse
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
import requests
import json
# 证书路径
pem_path = 'path/apiclient_key.pem'
# 商户号
mchid = '16xxxxd3'
# 证书序列号
serial_no = '3Dxxxxxxxxxxxxxxx83'
# 32位API v3 密钥
api_v3_key = 'z8xxxxxxxxxxxxxxxxxxj6'
# 微信
base_url = "https://api.mch.weixin.qq.com"
def generate_random_string(length):
"""
随机生成字符串
:param length:
:return:
"""
# 选择字母和数字的组合
characters = string.ascii_letters + string.digits
# 随机选择字符并组合成字符串
random_string = ''.join(random.choice(characters) for _ in range(length))
return random_string
def get_canonical_url(url):
"""
获取绝对URL
:param url:
:return:
"""
parsed_url = urlparse(url)
# 获取路径并进行编码
canonical_url = quote(parsed_url.path)
# 如果有查询参数,添加到路径后面
if parsed_url.query:
canonical_url += "?" + parsed_url.query
return canonical_url
def build_message(method, url, timestamp, nonce_str, body):
"""
生成【请求签名串】
:param method: 获取HTTP请求的方法(GET,POST,PUT)
:param url: 获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。
:param timestamp: 获取发起请求时的系统当前时间戳(秒)
:param nonce_str: 随机串
:param body: 获取请求中的请求报文主体,请求方法为GET时,报文主体为空,当请求方法为POST或PUT时,请使用真实发送的JSON报文。 图片上传API,请使用meta对应的JSON报文
:return:
"""
if method == 'GET' or body is None:
body = ''
url_encode = get_canonical_url(url)
return f'{method}\n{url_encode}\n{timestamp}\n{nonce_str}\n{body}\n'
def sign(message):
"""
生成签名
:param message:
:return:
"""
# 从 PEM 文件加载私钥
with open(pem_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None, # 如果私钥有密码,提供相应的密码
)
# 使用 SHA256 和 RSA 对消息进行签名
signature = private_key.sign(
message.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
# 使用 Base64 编码签名
signature_base64 = base64.b64encode(signature).decode('utf-8')
return signature_base64
def get_token(method, url, body):
"""
生成token
:param method: HTTP请求的方法(GET,POST,PUT)
:param url: 获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串
:param body: 获取请求中的请求报文主体,
:return:
"""
# 签名算法
schema = "WECHATPAY2-SHA256-RSA2048"
nonce_str = generate_random_string(10)
timestamp = int(datetime.datetime.now().timestamp())
# 签名
signature = sign(build_message(method, url, timestamp, nonce_str, body))
return f'{schema} mchid="{mchid}",nonce_str="{nonce_str}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature}"'
def decrypt(key, nonce, ciphertext, associated_data):
"""
解密证书
:param key:
:param nonce:
:param ciphertext:
:param associated_data:
:return:
"""
# 32位 ApiV3Key
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)
def test_token():
# 证书接口
url = f'{base_url}/v3/certificates'
# 获取token
access_token = get_token('GET', url, '')
print(access_token)
# 设置header
header = {
'Authorization': access_token
}
response = requests.get(url, headers=header)
json_data = response.json()
print(json_data)
# 解析证书
encrypt_certificate = json_data['data'][0]['encrypt_certificate']
print(decrypt(api_v3_key, encrypt_certificate['nonce'], encrypt_certificate['ciphertext'], encrypt_certificate['associated_data']))
# 验证随机字符串
print(generate_random_string(10))
# 测试获取证书,请求证书
test_token()