Files
Django/utils/wechat_v3.py
2026-06-14 00:24:53 +08:00

110 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
import time
import hashlib
import base64
import json
from urllib.parse import urlparse
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from Crypto.Cipher import AES
from django.conf import settings
import os
def load_private_key():
"""加载私钥(支持 PKCS#1 和 PKCS#8"""
key_path = settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH']
with open(key_path, 'rb') as f:
key_data = f.read()
# cryptography 自动识别格式
private_key = serialization.load_pem_private_key(
key_data,
password=None,
backend=default_backend()
)
return private_key
def build_authorization(method, url, body):
"""生成V3接口的Authorization头"""
private_key = load_private_key()
mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID']
serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO']
parsed = urlparse(url)
path = parsed.path
if parsed.query:
path += '?' + parsed.query
timestamp = str(int(time.time()))
nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32]
# 构造签名串
message = '\n'.join([method, path, timestamp, nonce, body]) + '\n'
# 使用 cryptography 签名
signature = private_key.sign(
message.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
signature_b64 = base64.b64encode(signature).decode('utf-8')
auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
return auth
def verify_wechat_sign(headers, body):
"""验证微信回调签名(需提前下载平台证书)"""
# 这部分使用 rsa 库验签,也可以改用 cryptography但 rsa 已足够
import rsa
serial = headers.get('Wechatpay-Serial')
signature = headers.get('Wechatpay-Signature')
timestamp = headers.get('Wechatpay-Timestamp')
nonce = headers.get('Wechatpay-Nonce')
if not all([serial, signature, timestamp, nonce]):
return False
if not re.match(r'^[A-Za-z0-9-]+$', serial):
return False
try:
ts = int(headers.get('Wechatpay-Timestamp', '0'))
if abs(time.time() - ts) > 300: # 5分钟有效期
return False
except (ValueError, TypeError):
return False
message = '\n'.join([timestamp, nonce, body]) + '\n'
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
if not os.path.exists(cert_path):
return False
with open(cert_path, 'rb') as f:
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read())
signature_bytes = base64.b64decode(signature)
try:
rsa.verify(message.encode('utf-8'), signature_bytes, pub_key)
return True
except rsa.VerificationError:
return False
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext):
"""解密回调数据(使用 AES-GCM"""
api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8')
nonce_bytes = nonce.encode('utf-8')
ciphertext_bytes = base64.b64decode(ciphertext)
cipher = AES.new(api_v3_key, AES.MODE_GCM, nonce=nonce_bytes)
if associated_data:
cipher.update(associated_data.encode('utf-8'))
plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:])
return plaintext.decode('utf-8')