第一次提交:小程序前端最新版本
This commit is contained in:
207
utils/wechat_v3.py
Normal file
207
utils/wechat_v3.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# utils/wechat_v3.py
|
||||
'''import time
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import rsa
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def load_private_key():
|
||||
"""加载商户私钥(PEM格式)"""
|
||||
with open(settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH'], 'rb') as f:
|
||||
key_data = f.read()
|
||||
return rsa.PrivateKey.load_pkcs1(key_data)
|
||||
|
||||
|
||||
def build_authorization(method, url, body):
|
||||
"""
|
||||
生成V3接口的Authorization头
|
||||
:param method: GET/POST
|
||||
:param url: 完整URL(含域名和路径)
|
||||
:param body: 请求体JSON字符串
|
||||
:return: Authorization字符串
|
||||
"""
|
||||
private_key = load_private_key()
|
||||
mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID']
|
||||
serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO']
|
||||
|
||||
# 提取URL路径(不含query)
|
||||
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'
|
||||
|
||||
# 使用私钥签名(SHA256)
|
||||
signature = rsa.sign(message.encode('utf-8'), private_key, 'SHA-256')
|
||||
signature_b64 = base64.b64encode(signature).decode('utf-8')
|
||||
|
||||
# 组装Authorization
|
||||
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):
|
||||
"""
|
||||
验证微信回调的签名
|
||||
需要提前从微信平台下载证书公钥,存放在 PLATFORM_CERT_DIR 下,文件名 = 序列号.pem
|
||||
:param headers: 请求头
|
||||
:param body: 原始请求体字符串
|
||||
:return: bool
|
||||
"""
|
||||
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
|
||||
|
||||
# 构造验签名串
|
||||
message = '\n'.join([timestamp, nonce, body]) + '\n'
|
||||
|
||||
# 读取对应序列号的平台证书公钥
|
||||
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
|
||||
try:
|
||||
with open(cert_path, 'rb') as f:
|
||||
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read())
|
||||
except FileNotFoundError:
|
||||
# 如果证书不存在,可尝试下载(生产环境建议定时下载)
|
||||
return False
|
||||
|
||||
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):
|
||||
"""
|
||||
使用APIv3密钥解密回调中的加密数据
|
||||
:param associated_data: 附加数据
|
||||
:param nonce: 随机串
|
||||
:param ciphertext: 密文(Base64)
|
||||
:return: 解密后的JSON字符串
|
||||
"""
|
||||
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)
|
||||
|
||||
# AES-GCM解密
|
||||
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')'''
|
||||
|
||||
# utils/wechat_v3.py
|
||||
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
|
||||
|
||||
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')
|
||||
Reference in New Issue
Block a user