PayFast Payment Integration 签名不匹配如何修复?
结论
该错误表示:PayFast 根据收到的表单字段重新计算 MD5 后,结果与提交的 signature 不一致。
这段代码需要检查并修复以下问题:
- 使用与当前环境对应的
merchant_id、merchant_key和 passphrase。 - 删除重复定义的商户凭据,避免签名时误用其他值。
- 严格按照表单字段的顺序生成参数串。
- 不要将
signature本身加入待签名数据。 - 只有 PayFast 后台配置了 passphrase,签名时才能加入该值。
- 将金额统一格式化为两位小数。
- 输出隐藏字段时使用
htmlspecialchars(),避免字段值被改变或破坏 HTML。 - 正式测试 ITN 通知时,不要使用 PayFast 无法访问的
localhost地址。
为什么会出现签名不匹配
PayFast 会对规范化后的参数串执行 MD5:
merchant_id=...&merchant_key=...&return_url=...&amount=100.00&...&passphrase=...
以下任何一项不一致,都会导致签名不匹配:
- 参数顺序不同;
- 参数值前后有空格;
- 编码方式不同;
- 双方对空字段的处理方式不同;
- 金额一边是
100,另一边是100.00; - 代码添加了 PayFast 后台未配置的 passphrase;
- 后台配置了 passphrase,但代码没有添加;
- sandbox 地址搭配了 production 商户凭据,或者相反;
- 生成签名后又修改了某个表单字段;
- 将
signature字段加入了签名计算。
原代码还重复定义了 $merchantID 和 $merchantKey:
$merchantID = ' 10196067';
$merchantKey = 'n3wqwtxa1irme';
// 后面又覆盖
$merchantID = '***';
$merchantKey = '***z';
第一处 merchant_id 的开头还有一个空格。虽然这个值之后会被覆盖,但重复定义很容易让调试过程使用错误的凭据,应该删掉。
推荐的实现
<?php
session_start();
$isSandbox = true;
$pfHost = $isSandbox
? 'sandbox.payfast.co.za'
: 'www.payfast.co.za';
/*
* 必须使用与当前环境匹配的凭据。
* Sandbox 和 Production 的凭据不要混用。
*/
$merchantId = 'YOUR_MERCHANT_ID';
$merchantKey = 'YOUR_MERCHANT_KEY';
/*
* 只有在对应 PayFast 账户中配置了 passphrase 时才填写。
* 如果没有配置,请设置为 null。
*/
$passphrase = 'YOUR_CONFIGURED_PASSPHRASE';
// $passphrase = null;
$amount = number_format(100, 2, '.', '');
$data = [
'merchant_id' => trim($merchantId),
'merchant_key' => trim($merchantKey),
'return_url' => 'https://example.com/payment_success.php',
'cancel_url' => 'https://example.com/payment_cancel.php',
'notify_url' => 'https://example.com/payment_notify.php',
'name_first' => 'John',
'name_last' => 'Doe',
'email_address' => 'gcira2023@outlook.com',
'amount' => $amount,
'item_name' => 'Cell Phone',
'item_description'=> 'Electronics',
];
function generatePayFastSignature(array $data, ?string $passphrase = null): string
{
$pairs = [];
foreach ($data as $key => $value) {
/*
* signature 不得参与签名计算。
* null 和空字符串也不应发送或参与计算。
*/
if ($key === 'signature' || $value === null || $value === '') {
continue;
}
$pairs[] = $key . '=' . urlencode(trim((string) $value));
}
if ($passphrase !== null && trim($passphrase) !== '') {
$pairs[] = 'passphrase=' . urlencode(trim($passphrase));
}
return md5(implode('&', $pairs));
}
$data['signature'] = generatePayFastSignature($data, $passphrase);
?>
<form action="https://<?= htmlspecialchars($pfHost, ENT_QUOTES, 'UTF-8') ?>/eng/process"
method="post">
<?php foreach ($data as $name => $value): ?>
<input
type="hidden"
name="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"
value="<?= htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') ?>"
>
<?php endforeach; ?>
<button type="submit">Pay Now</button>
</form>
确认 passphrase 是否一致
passphrase 不能随意填写,必须与对应 PayFast 账户中配置的值完全一致,包括:
- 字母大小写;
- 数字和特殊字符;
- 前后是否有空格;
- sandbox 与 production 账户各自的配置。
如果后台没有设置 passphrase,代码应使用:
$passphrase = null;
如果后台已经设置,则代码必须填写完全相同的值:
$passphrase = '后台配置的准确值';
不要将 passphrase 作为隐藏字段提交。它只用于生成签名:
$pairs[] = 'passphrase=' . urlencode(trim($passphrase));
检查实际用于签名的字符串
排查问题时,可以暂时将参数串单独生成并进行比较,但不要在生产页面中输出商户密钥或 passphrase:
function buildPayFastParameterString(
array $data,
?string $passphrase = null
): string {
$pairs = [];
foreach ($data as $key => $value) {
if ($key === 'signature' || $value === null || $value === '') {
continue;
}
$pairs[] = $key . '=' . urlencode(trim((string) $value));
}
if ($passphrase !== null && trim($passphrase) !== '') {
$pairs[] = 'passphrase=' . urlencode(trim($passphrase));
}
return implode('&', $pairs);
}
$parameterString = buildPayFastParameterString($data, $passphrase);
$signature = md5($parameterString);
检查 $parameterString 时,需要确认:
- 不包含
signature=...; - 字段名前后没有空格;
- 字段顺序与提交表单一致;
amount使用类似100.00的格式;- passphrase 是否按照后台配置加入;
- 生成签名后没有修改
$data。
不要在生产环境中记录完整参数串,其中可能包含 merchant_key、邮箱等敏感信息。
关于 localhost 回调地址
以下地址只能由当前电脑访问:
'return_url' => 'http://localhost/payment_success.php',
'cancel_url' => 'http://localhost/payment_cancel.php',
'notify_url' => 'http://localhost/payment_notify.php',
这通常不会直接导致签名不匹配,但 PayFast 服务器无法向本机的 notify_url 发送 ITN 通知。完整联调时,应改用公网可以访问的 HTTPS 地址:
'return_url' => 'https://example.com/payment_success.php',
'cancel_url' => 'https://example.com/payment_cancel.php',
'notify_url' => 'https://example.com/payment_notify.php',
本机开发时可以使用受控的 HTTPS 隧道,同时确保通知处理地址不需要登录,也不会被 CSRF 校验直接拦截。
其他注意事项
不要使用以下方式设置金额:
$amount = $_POST['amount'] = 100;
这种写法会无意义地修改 $_POST。如果金额来自订单,应根据服务端订单数据计算,并统一格式化:
$amount = number_format((float) $grandTotal, 2, '.', '');
也不要直接相信浏览器提交的金额:
$amount = $_POST['amount'];
用户可以修改隐藏字段。正确做法是根据订单 ID 从服务端重新读取商品价格,再计算应付金额。
sandbox 和 production 的配置需要整体切换,不能只更换提交域名:
$isSandbox = true;
各项配置应保持一致:
- sandbox URL + sandbox 凭据 + sandbox passphrase;
- production URL + production 凭据 + production passphrase。
完成这些调整后,如果仍然出现签名不匹配,应逐项比较生成签名时使用的数据与浏览器实际 POST 的数据,重点检查凭据环境、passphrase、字段顺序、金额格式和多余空格。
备注:内容仅供参考。
