4. 统一下单请求参数组装与签名

统一下单请求参数组装与签名 学习卡片

统一下单请求的签名链路

统一下单接口要求请求参数先按字段名 ASCII 升序排列,再拼接为 key=value&key=value 形式,末尾追加商户密钥后做 MD5,最后将结果转为大写。整个链路中,任何一步顺序错误都会导致网关返回 SIGN_ERROR。

① 参数收集 appid / mch_id out_trade_no / total_fee body / notify_url / trade_type 排除 sign 字段本身 ② ASCII 升序排序 appid < body < mch_id < notify_url < out_trade_no < total_fee < trade_type 按 key 字典序,非 value ③ 拼接 key=value appid=wx123&body=测试 &mch_id=10001&notify_url=... &out_trade_no=ORD001 &total_fee=1&trade_type=JSAPI 空值不参与拼接 ④ 追加 &key=商户密钥 stringA + "&key=" + apiKey 密钥不参与排序,仅末尾追加 ⑤ MD5 哈希 MD5(stringSignTemp) 输出 32 位十六进制小写 ⑥ 转大写 → sign sign = MD5(...).toUpperCase() 写入请求参数 sign 字段 最终请求体 XML 或 JSON 序列化 含 sign 字段,POST 发送 常见签名错误 • 排序用了 value 而非 key • 空值参与了拼接 • 密钥位置放错 / 未转大写 统一下单签名链路:收集 → 排序 → 拼接 → 追加密钥 → MD5 → 大写

参数组装与签名实现

下面以微信支付 V2 统一下单为例,实现一个可复用的签名工具类。核心逻辑是:过滤空值 → 按 key 升序 → 拼接 → 追加密钥 → MD5 → 大写。

import hashlib
import time
import random
import string

class UnifiedOrderSigner:
    """统一下单请求参数组装与签名"""

    def __init__(self, appid: str, mch_id: str, api_key: str):
        self.appid = appid
        self.mch_id = mch_id
        self.api_key = api_key

    def _generate_nonce_str(self, length: int = 32) -> str:
        """生成随机字符串,防止重放"""
        chars = string.ascii_letters + string.digits
        return ''.join(random.choice(chars) for _ in range(length))

    def build_params(self, out_trade_no: str, total_fee: int,
                     body: str, notify_url: str,
                     trade_type: str = "JSAPI",
                     openid: str = None) -> dict:
        """组装统一下单请求参数(不含 sign)"""
        params = {
            "appid": self.appid,
            "mch_id": self.mch_id,
            "nonce_str": self._generate_nonce_str(),
            "body": body,
            "out_trade_no": out_trade_no,
            "total_fee": total_fee,
            "spbill_create_ip": "127.0.0.1",
            "notify_url": notify_url,
            "trade_type": trade_type,
        }
        if openid:
            params["openid"] = openid
        return params

    def sign(self, params: dict) -> str:
        """对参数进行签名,返回大写 MD5 值"""
        # 1. 过滤空值,排除 sign 字段
        filtered = {
            k: v for k, v in params.items()
            if v is not None and v != "" and k != "sign"
        }
        # 2. 按 key 的 ASCII 升序排列
        sorted_keys = sorted(filtered.keys())
        # 3. 拼接为 key=value&key=value
        string_a = "&".join(f"{k}={filtered[k]}" for k in sorted_keys)
        # 4. 追加商户密钥
        string_sign_temp = f"{string_a}&key={self.api_key}"
        # 5. MD5 哈希并转大写
        md5 = hashlib.md5(string_sign_temp.encode("utf-8")).hexdigest()
        return md5.upper()

    def build_signed_params(self, **kwargs) -> dict:
        """组装参数并附加签名,返回完整请求参数"""
        params = self.build_params(**kwargs)
        params["sign"] = self.sign(params)
        return params

使用示例与验证

signer = UnifiedOrderSigner(
    appid="wx1234567890",
    mch_id="10000100",
    api_key="your_api_key_here"
)

params = signer.build_signed_params(
    out_trade_no="ORD20240101001",
    total_fee=1,
    body="测试商品",
    notify_url="https://example.com/pay/notify",
    trade_type="JSAPI",
    openid="oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
)

print(params["sign"])  # 输出 32 位大写 MD5
验证方法:将生成的 sign 与网关返回的签名对比。若不一致,优先检查:① 排序是否按 key 而非 value;② 空值是否被过滤;③ 密钥是否在末尾且格式为 &key=xxx;④ 是否转大写。
注意:total_fee 单位为分,必须为整数;nonce_str 每次请求都应重新生成,不可复用;sign 字段本身不参与签名计算。