
本文详细解析 payfast 支付网关中 md5 签名的正确生成方法,涵盖参数排序、大小写规范、url 编码细节及调试技巧,帮助 python 开发者避免 “signature does not match” 错误。
本文详细解析 payfast 支付网关中 md5 签名的正确生成方法,涵盖参数排序、大小写规范、url 编码细节及调试技巧,帮助 python 开发者避免 “signature does not match” 错误。
在集成 PayFast 支付 API 时,签名(signature)验证失败是开发者最常遇到的问题之一,典型报错为 400 signature: Generated signature does not match submitted signature。该错误并非源于加密算法本身,而几乎总是由参数拼接字符串(param string)与官方要求不一致导致——哪怕一个空格、大小写偏差或排序错位都会使 MD5 哈希值完全不同。
✅ 关键规范回顾(依据 PayFast 官方文档)
PayFast 要求签名生成严格遵循以下规则:
-
参数必须按固定顺序排列(非字典序!),顺序为:
merchant_id,merchant_key,return_url,cancel_url,notify_url,name_first,name_last,email_address,email_confirmation,cell_number,amount,item_name,item_description,shipping_fee,payment_method,custom_int1,custom_int2,custom_int3,custom_int4,custom_int5,custom_str1,custom_str2,custom_str3,custom_str4,custom_str5,passphrase -
所有键(key)和值(value)必须转为大写(注意:
passphrase是唯一小写键名,但其值仍需大写编码); -
URL 编码必须使用
urllib.parse.quote_plus(),且编码后全部转为大写字母(如%3A→%3A不变,但` →+,é→%C3%89`); -
空值参数必须完全排除(不能传
None、空字符串或""); -
PASSPHRASE字段名必须全大写,且仅在拼接末尾追加一次(即使原始数据中已含); - 最终字符串不含任何前导/尾随空格或换行符。
⚠️ 原代码中的核心问题分析
你提供的实现存在多个隐性偏差:
安全 Python 代码沙箱:在隔离的沙箱中执行任意 Python 代码,预装 requests、NumPy、pandas 库。返回 stdout、stderr。
-
参数顺序未强制固定:
dict.items()在 Python 3.7+ 虽保持插入序,但若构造字典时顺序错误,或使用sorted()等操作,极易出错; -
PASSPHRASE键名误写为PASSPHRASE(全大写)但文档明确要求字段名为小写passphrase—— 注意:这是唯一键名小写、值大写编码的特例; -
urllib.parse.quote_plus(...).upper()存在风险:%xx编码本身已是大写十六进制,重复.upper()无害,但若原始值含 Unicode 字符(如é→%C3%89),.upper()对%符号无效,逻辑上冗余且易误导; -
未对
passphrase值做 URL 编码后再大写:应与其它值一致处理。
✅ 推荐修正实现(生产就绪版)
import hashlib
import urllib.parse
def generate_payfast_signature(data: dict, passphrase: str) -> str:
"""
严格按照 PayFast 文档生成 MD5 签名。
参数顺序、大小写、编码方式均与官方要求 100% 一致。
"""
# 固定参数顺序(关键!)
required_keys = [
"merchant_id", "merchant_key", "return_url", "cancel_url", "notify_url",
"name_first", "name_last", "email_address", "email_confirmation",
"cell_number", "amount", "item_name", "item_description",
"shipping_fee", "payment_method", "custom_int1", "custom_int2",
"custom_int3", "custom_int4", "custom_int5", "custom_str1",
"custom_str2", "custom_str3", "custom_str4", "custom_str5"
]
# 构建有序参数列表(仅包含非空值)
encoded_pairs = []
for key in required_keys:
if key in data and data[key] not in (None, "", b""):
value = str(data[key]).strip()
# URL encode + 转大写(安全:quote_plus 已返回大写 %XX)
encoded_value = urllib.parse.quote_plus(value)
encoded_pairs.append(f"{key.upper()}={encoded_value}")
# 追加 passphrase(注意:key 小写,value 同样需编码+大写)
if passphrase:
encoded_passphrase = urllib.parse.quote_plus(str(passphrase))
encoded_pairs.append(f"passphrase={encoded_passphrase}")
# 拼接最终字符串(无空格,无换行)
param_string = "&".join(encoded_pairs)
# 调试建议:打印前检查是否与 PayFast 示例完全一致
# print("Param string for hashing:", repr(param_string))
# 生成 MD5 签名
signature = hashlib.md5(param_string.encode("utf-8")).hexdigest()
return signature
# 使用示例
payload = {
"merchant_id": "11111111",
"merchant_key": "UAAAAAAAAAAHA",
"return_url": "https://example.com/dev/payfast-redirect",
"cancel_url": "https://example.com/dev/payfast-redirect?status=failed",
"notify_url": "https://example.com/dev/payments/payfast-notify",
"email_address": "john.doe@gmail.com",
"amount": "300",
"item_name": "COOL&THING",
"item_description": "BLAH&BLAH&BLAH"
}
sig = generate_payfast_signature(payload, "PHRASEFROMSANDBOX")
print("✅ Final signature:", sig)? 调试黄金法则
-
逐字符比对:将生成的
param_string与 PayFast 沙箱返回的“expected string”(如有)或文档示例逐字符对比(推荐用 diffchecker.com); -
禁用
.upper():urllib.parse.quote_plus()输出默认为大写%XX,无需额外.upper();保留它可能掩盖编码逻辑错误; - 沙箱环境验证:PayFast 提供 Signature Validator Tool(需登录),可上传你的参数和密钥,实时校验;
-
日志记录明文串:上线前务必记录
param_string(脱敏后),它是排错唯一可信依据。
✅ 总结
PayFast 签名失败极少因哈希算法错误,绝大多数源于字符串构造阶段的微小偏差。坚持三点即可大幅降低故障率:
① 硬编码参数顺序(勿依赖字典遍历);
② passphrase 字段名小写、值编码、不参与主循环;
③ 全程使用 urllib.parse.quote_plus(),避免手动 .upper() 干预编码结果。
遵循本教程实现,你将得到与 PayFast 服务端完全一致的签名,顺利通过验证。
立即学习“Python免费学习笔记(深入)”;

















