PAYATHON 2026

PayFast Payment Integration 签名不匹配如何修复?

支付阿杰

结论

该错误表示:PayFast 根据收到的表单字段重新计算 MD5 后,结果与提交的 signature 不一致。

这段代码需要检查并修复以下问题:

  1. 使用与当前环境对应的 merchant_id、merchant_key 和 passphrase。
  2. 删除重复定义的商户凭据,避免签名时误用其他值。
  3. 严格按照表单字段的顺序生成参数串。
  4. 不要将 signature 本身加入待签名数据。
  5. 只有 PayFast 后台配置了 passphrase,签名时才能加入该值。
  6. 将金额统一格式化为两位小数。
  7. 输出隐藏字段时使用 htmlspecialchars(),避免字段值被改变或破坏 HTML。
  8. 正式测试 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、字段顺序、金额格式和多余空格。

备注:内容仅供参考。