4. 统一下单请求参数组装与签名
统一下单请求的签名链路
统一下单接口要求请求参数先按字段名 ASCII 升序排列,再拼接为 key=value&key=value 形式,末尾追加商户密钥后做 MD5,最后将结果转为大写。整个链路中,任何一步顺序错误都会导致网关返回 SIGN_ERROR。
参数组装与签名实现
下面以微信支付 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 字段本身不参与签名计算。