eGHL Payment 回调 URL 含 & 时 Hash value mismatched
结论
callback URL 的参数分隔符应使用 &。只有把 URL 写入 HTML 表单属性时,才需要将其输出为 &。计算 Hash 时,应使用浏览器实际提交的原始 URL,也就是包含 & 的版本,不能使用包含 & 的 HTML 字符串。
示例 URL 中 & 前的空格也必须删除。不要对整个 callback URL 调用 urlencode()。
原因
同一个 callback URL 在不同环节有三种表示方式:
- 实际 callback URL:
http://localhost/joomla/index.php?option=com_k2store&view=checkout&task=confirmPayment&orderpayment_type=payment_eghl
- HTML 源码中的属性值:
value="http://localhost/joomla/index.php?option=com_k2store&view=checkout&task=confirmPayment&orderpayment_type=payment_eghl"
- 浏览器提交给 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 转成 &,再用转换后的值计算 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。
备注:内容仅供参考。
