import re import time import hashlib import base64 import json import os 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 def _get_v3_config(club_id=None): from jituan.services.wechat_pay import get_wechat_v3_config return get_wechat_v3_config(club_id) def load_private_key(club_id=None): """加载私钥(支持 PKCS#1 和 PKCS#8)。""" cfg = _get_v3_config(club_id) key_path = cfg['PRIVATE_KEY_PATH'] with open(key_path, 'rb') as f: key_data = f.read() return serialization.load_pem_private_key( key_data, password=None, backend=default_backend(), ) def build_authorization(method, url, body, club_id=None): """生成 V3 接口的 Authorization 头。""" cfg = _get_v3_config(club_id) private_key = load_private_key(club_id) mchid = cfg['MCHID'] serial_no = cfg['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' signature = private_key.sign( message.encode('utf-8'), padding.PKCS1v15(), hashes.SHA256(), ) signature_b64 = base64.b64encode(signature).decode('utf-8') return ( f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",' f'timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"' ) def verify_wechat_sign(headers, body, club_id=None): """验证微信回调签名(需提前下载平台证书)。""" 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: return False except (ValueError, TypeError): return False cfg = _get_v3_config(club_id) message = '\n'.join([timestamp, nonce, body]) + '\n' cert_path = f"{cfg['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 resolve_callback_club_id(headers, body): """ 多商户回调:依次用各俱乐部平台证书验签,返回匹配的 club_id。 验签失败返回 None。 """ from jituan.constants import CLUB_ID_DEFAULT from jituan.models import Club if verify_wechat_sign(headers, body, club_id=CLUB_ID_DEFAULT): return CLUB_ID_DEFAULT for club in Club.query.filter(status=1).exclude(club_id=CLUB_ID_DEFAULT): if verify_wechat_sign(headers, body, club_id=club.club_id): return club.club_id return None def decrypt_callback_ciphertext(associated_data, nonce, ciphertext, club_id=None): """解密回调数据(使用 AES-GCM)。""" cfg = _get_v3_config(club_id) api_v3_key = cfg['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')