import base64 import json import secrets import time from pathlib import Path from typing import Any from urllib import request as urllib_request from urllib.error import HTTPError, URLError from fastapi import HTTPException, status from app.config import wechat_pay_settings WECHAT_SIGNATURE_TYPE = "WECHATPAY2-SHA256-RSA2048" def ensure_wechat_pay_configured() -> None: if not wechat_pay_settings.is_configured: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="微信支付配置未完成,请先填写 WECHAT_PAY_* 配置。", ) def load_private_key(): from cryptography.hazmat.primitives import serialization key_data = Path(wechat_pay_settings.private_key_path).read_bytes() return serialization.load_pem_private_key(key_data, password=None) def load_public_key(): from cryptography.hazmat.primitives import serialization if not wechat_pay_settings.public_key_path: return None key_data = Path(wechat_pay_settings.public_key_path).read_bytes() return serialization.load_pem_public_key(key_data) def rsa_sign(message: str) -> str: from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding signature = load_private_key().sign( message.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256(), ) return base64.b64encode(signature).decode("ascii") def rsa_verify(message: str, signature: str) -> bool: from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding public_key = load_public_key() if public_key is None: return bool(wechat_pay_settings.skip_notify_verify) try: public_key.verify( base64.b64decode(signature), message.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256(), ) except InvalidSignature: return False return True def build_authorization(method: str, url_path: str, body: str) -> str: timestamp = str(int(time.time())) nonce = secrets.token_hex(16) message = f"{method}\n{url_path}\n{timestamp}\n{nonce}\n{body}\n" signature = rsa_sign(message) return ( f'{WECHAT_SIGNATURE_TYPE} mchid="{wechat_pay_settings.mch_id}",' f'nonce_str="{nonce}",signature="{signature}",' f'timestamp="{timestamp}",serial_no="{wechat_pay_settings.mch_serial_no}"' ) def post_wechat_json(url_path: str, payload: dict[str, Any]) -> dict[str, Any]: ensure_wechat_pay_configured() body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) url = f"{wechat_pay_settings.api_base_url.rstrip('/')}{url_path}" request = urllib_request.Request( url, data=body.encode("utf-8"), method="POST", headers={ "Accept": "application/json", "Authorization": build_authorization("POST", url_path, body), "Content-Type": "application/json", "User-Agent": "py-web-wechat-pay/0.1", }, ) try: with urllib_request.urlopen(request, timeout=10) as response: return json.loads(response.read().decode("utf-8")) except HTTPError as exc: error_body = exc.read().decode("utf-8") raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"微信支付请求失败:{error_body or exc.reason}", ) from exc except URLError as exc: raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"微信支付网络请求失败:{exc.reason}", ) from exc def build_jsapi_pay_params(prepay_id: str) -> dict[str, str]: timestamp = str(int(time.time())) nonce = secrets.token_hex(16) package = f"prepay_id={prepay_id}" message = f"{wechat_pay_settings.appid}\n{timestamp}\n{nonce}\n{package}\n" return { "appId": wechat_pay_settings.appid, "timeStamp": timestamp, "nonceStr": nonce, "package": package, "signType": "RSA", "paySign": rsa_sign(message), } def decrypt_notify_resource(resource: dict[str, str]) -> dict[str, Any]: from cryptography.hazmat.primitives.ciphers.aead import AESGCM ciphertext = base64.b64decode(resource["ciphertext"]) nonce = resource["nonce"].encode("utf-8") associated_data = resource.get("associated_data", "").encode("utf-8") aesgcm = AESGCM(wechat_pay_settings.api_v3_key.encode("utf-8")) plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data) return json.loads(plaintext.decode("utf-8")) def verify_notify_signature(headers: dict[str, str], body: str) -> bool: timestamp = headers.get("wechatpay-timestamp", "") nonce = headers.get("wechatpay-nonce", "") signature = headers.get("wechatpay-signature", "") if not timestamp or not nonce or not signature: return False message = f"{timestamp}\n{nonce}\n{body}\n" return rsa_verify(message, signature)