PAYATHON 2026

eGHL Payment 回调 URL 含 & 时 Hash value mismatched

支付小周

结论

callback URL 的参数分隔符应使用 &。只有把 URL 写入 HTML 表单属性时,才需要将其输出为 &。计算 Hash 时,应使用浏览器实际提交的原始 URL,也就是包含 & 的版本,不能使用包含 & 的 HTML 字符串。

示例 URL 中 & 前的空格也必须删除。不要对整个 callback URL 调用 urlencode()。

原因

同一个 callback URL 在不同环节有三种表示方式:

  1. 实际 callback URL:
http://localhost/joomla/index.php?option=com_k2store&view=checkout&task=confirmPayment&orderpayment_type=payment_eghl
  1. HTML 源码中的属性值:
value="http://localhost/joomla/index.php?option=com_k2store&view=checkout&task=confirmPayment&orderpayment_type=payment_eghl"
  1. 浏览器提交给 eGHL 的值:
http://localhost/joomla/index.php?option=com_k2store&view=checkout&task=confirmPayment&orderpayment_type=payment_eghl

& 只是 & 在 HTML 中的转义形式。浏览器解析表单后,实际提交的仍是普通的 &。

计算 Hash 所用的 callback 字符串必须与最终提交给 eGHL 的字符串完全一致。哪怕只相差一个空格、一次编码或一个字符,eGHL 重新计算出的 Hash 都会不同。

正确构造 callback URL

可以用 http_build_query() 构造查询参数,避免手工拼接时引入空格或重复编码:

<?php

$query = [
    'option'            => 'com_k2store',
    'view'              => 'checkout',
    'task'              => 'confirmPayment',
    'orderpayment_type' => 'payment_eghl',
];

$callback = 'http://localhost/joomla/index.php?'
    . http_build_query($query, '', '&', PHP_QUERY_RFC3986);

此时 $callback 的值是:

http://localhost/joomla/index.php?option=com_k2store&view=checkout&task=confirmPayment&orderpayment_type=payment_eghl

下面的写法在 & 前多了一个空格,不能使用:

?query=abce123 &another=value123

应改为:

?query=abce123&another=value123

Hash 和表单输出要使用不同的表示

计算 Hash 时,直接使用原始 $callback:

<?php

// 按 eGHL 文档规定的字段顺序拼接。
// 这里只演示 callback 的处理方式,不能据此推断完整签名字段。
$hashSource = $serviceId
    . $paymentId
    . $orderNumber
    . $amount
    . $currencyCode
    . $callback
    . $password;

$hashValue = hash('sha256', $hashSource);

将值输出到 HTML 时,再调用 htmlspecialchars():

<input
    type="hidden"
    name="CallbackURL"
    value="<?= htmlspecialchars(
        $callback,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    ) ?>"
>

<input
    type="hidden"
    name="HashValue"
    value="<?= htmlspecialchars(
        $hashValue,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    ) ?>"
>

不要先把 $callback 转成 &amp;,再用转换后的值计算 Hash:

<?php

// 错误:这是 HTML 表示,不是实际提交的 URL。
$htmlCallback = htmlspecialchars($callback, ENT_QUOTES, 'UTF-8');
$hashSource = $serviceId . $htmlCallback . $password;

为什么 urlencode() 没有解决问题

对整个 URL 调用 urlencode(),会把协议、斜杠、问号和参数分隔符全部编码:

<?php

echo urlencode($callback);

结果类似:

http%3A%2F%2Flocalhost%2Fjoomla%2Findex.php%3Foption%3Dcom_k2store%26view%3Dcheckout

这种字符串通常已不是支付接口所需的 callback URL。

也不能直接把参数之间的 & 改成 %26:

?option=com_k2store%26view=checkout

这样一来,%26view=checkout 会成为 option 参数值的一部分,而不是一个新的 view 参数。

需要编码的是各个参数的名称和值,不是整个 URL。http_build_query(..., PHP_QUERY_RFC3986) 已经完成了这项处理。

排查 Hash 不一致

如果按上述方式处理后仍然报错,可以逐项核对:

  • 计算 Hash 时使用的 callback 字符串;
  • 浏览器实际提交的 CallbackURL;
  • 字符串首尾是否有空格或换行;
  • URL 是否在计算 Hash 后又被修改;
  • 是否重复调用了 urlencode() 或 htmlspecialchars();
  • 金额格式是否始终一致,例如 81.50 与 81.5;
  • 字段拼接顺序、大小写和空值处理方式是否完全符合 eGHL 的要求。

提交前可以记录原始值及其十六进制表示,以便检查不可见字符:

<?php

error_log('CallbackURL: [' . $callback . ']');
error_log('CallbackURL hex: ' . bin2hex($callback));
error_log('Hash source: [' . $hashSource . ']');
error_log('Hash value: ' . $hashValue);

然后打开浏览器开发者工具,在 Network 面板中查看实际发送的表单数据。判断依据不是页面源码看起来是否相同,而是签名字符串中的 callback 与请求提交的 CallbackURL 是否逐字节一致。

生产环境中的 callback 必须是 eGHL 服务器能够访问的公网 HTTPS 地址。localhost 只能用于本机或支付平台提供的本地模拟环境,远程支付服务器无法回调用户电脑上的 localhost。

备注:内容仅供参考。