API签名验签说明
适用范围: 所有经过 VerifyApiSign 中间件的对外 API 接口。
代码依据:
app/Http/Middleware/VerifyApiSign.phpapp/Helpers/functions.php中的getSignByYabandPayV3()
1. 整体校验流程
服务端收到请求后,会按下面顺序校验:
- 校验 Header 里是否有
Authorization: Bearer {token}。 - 校验 Body 里是否包含
uid、sign、timestamp、nonce、data。 - 按
uid找到对应 API 账户。 - 校验 Header 里的
token是否和该账户绑定的token一致。 - 如果该账户配置了 IP 白名单,则校验来源 IP。
- 用该账户的
secret_key重新计算签名。 - 比较服务端签名和请求里的
sign是否完全一致。 - 校验当前接口路径是否在该账户允许调用的接口权限范围内。
flowchart TD
A[客户端发起请求] --> B[检查 Authorization Bearer Token]
B --> C[检查 uid sign timestamp nonce data 是否存在]
C --> D[按 uid 查 API 账户]
D --> E[校验 token 是否匹配]
E --> F[校验来源 IP 白名单]
F --> G[用 secret_key 重新计算签名]
G --> H{sign 是否一致}
H -- 是 --> I[校验接口权限]
I --> J[放行业务请求]
H -- 否 --> K[返回 -1001]
2. 请求格式
2.1 Header
Authorization: Bearer {api_account_token}
Content-Type: application/json
2.2 Body
{
"uid": "8000000001",
"timestamp": 1731926400,
"nonce": "Qe3CkH2xP8",
"sign": "26ebd4b01363160622c8d515fb2afa76b81ea529b2f2a5bb8d6fbf53c1b3d645",
"data": {
"request_id": "req_202411180049",
"customer_id": "CUST20260001",
"timezone": "Asia/Shanghai",
"customer_type": "legal_entity",
"holder_name": "Yabandpay B.V.",
"alternative_name": "YB HQ",
"creditor_identifier": "YABAND001",
"access": 1,
"status": 1,
"remark": "NL account opening"
}
}
2.3 字段说明
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| uid | string | 是 | API 账户唯一标识,当前规则为 8 开头的 10 位数字 |
| timestamp | integer | 是 | Unix 时间戳(秒) |
| nonce | string | 是 | 随机字符串,建议每次请求唯一 |
| sign | string | 是 | 按本文规则计算出的 HMAC-SHA256 签名,小写 64 位十六进制字符串 |
| data | object | 是 | 业务参数对象,参与签名 |
| role | string | 否 | 即使传了,也不参与签名 |
3. 签名规则
3.1 先说结论
这个项目当前使用的不是“可逆加密”,而是“摘要签名”。
签名算法固定为:
HMAC-SHA256
密钥固定为:
secret_key
3.2 计算步骤
客户端必须严格按下面步骤生成 sign:
- 准备完整请求 Body。
- 删除
sign字段。 - 如果有
role字段,也删除,因为它不参与签名。 - 取出
data对象里的所有字段,平铺到外层。 - 再把外层的
uid、timestamp、nonce一起放入同一个待签名集合。 - 按字段名升序排序。
- 按
key=value方式拼接。 - 用
&把所有键值对连成一个字符串。 - 使用
secret_key作为密钥,对这个字符串做HMAC-SHA256。 - 得到 64 位小写十六进制字符串,作为最终
sign。
3.3 参与签名的真实字段
以上面的示例请求为例,真正参与签名的字段是:
| 排序后字段名 | 值 |
|---|---|
| access | 1 |
| alternative_name | YB HQ |
| creditor_identifier | YABAND001 |
| customer_id | CUST20260001 |
| customer_type | legal_entity |
| holder_name | Yabandpay B.V. |
| nonce | Qe3CkH2xP8 |
| remark | NL account opening |
| request_id | req_202411180049 |
| status | 1 |
| timestamp | 1731926400 |
| timezone | Asia/Shanghai |
| uid | 8000000001 |
3.4 待签名原串
下面这段字符串,就是服务端真实参与签名的原文:
access=1&alternative_name=YB HQ&creditor_identifier=YABAND001&customer_id=CUST20260001&customer_type=legal_entity&holder_name=Yabandpay B.V.&nonce=Qe3CkH2xP8&remark=NL account opening&request_id=req_202411180049&status=1×tamp=1731926400&timezone=Asia/Shanghai&uid=8000000001
3.5 示例密钥与签名结果
为了方便联调,下面给一组可以直接复算对上的演示数据:
secret_key = demo_secret_key_123456
对上面的待签名原串执行 HMAC-SHA256 后,得到的签名是:
26ebd4b01363160622c8d515fb2afa76b81ea529b2f2a5bb8d6fbf53c1b3d645
也就是说,下面这组 Body 里的 sign 是和实际代码完全对得上的:
{
"uid": "8000000001",
"timestamp": 1731926400,
"nonce": "Qe3CkH2xP8",
"sign": "26ebd4b01363160622c8d515fb2afa76b81ea529b2f2a5bb8d6fbf53c1b3d645",
"data": {
"request_id": "req_202411180049",
"customer_id": "CUST20260001",
"timezone": "Asia/Shanghai",
"customer_type": "legal_entity",
"holder_name": "Yabandpay B.V.",
"alternative_name": "YB HQ",
"creditor_identifier": "YABAND001",
"access": 1,
"status": 1,
"remark": "NL account opening"
}
}
4. 代码示例
4.1 PHP 示例
<?php
$secretKey = 'demo_secret_key_123456';
$body = [
'uid' => '8000000001',
'timestamp' => 1731926400,
'nonce' => 'Qe3CkH2xP8',
'data' => [
'request_id' => 'req_202411180049',
'customer_id' => 'CUST20260001',
'timezone' => 'Asia/Shanghai',
'customer_type' => 'legal_entity',
'holder_name' => 'Yabandpay B.V.',
'alternative_name' => 'YB HQ',
'creditor_identifier' => 'YABAND001',
'access' => 1,
'status' => 1,
'remark' => 'NL account opening',
],
];
function buildSign(array $param, string $secretKey): string
{
unset($param['sign']);
unset($param['role']);
$tmp = [];
$data = $param['data'] ?? [];
unset($param['data']);
foreach ($data as $k => $v) {
$tmp[$k] = $v;
}
foreach ($param as $k => $v) {
$tmp[$k] = $v;
}
ksort($tmp);
$pairs = [];
foreach ($tmp as $k => $v) {
$pairs[] = $k . '=' . $v;
}
return hash_hmac('sha256', implode('&', $pairs), $secretKey);
}
$body['sign'] = buildSign($body, $secretKey);
echo $body['sign'];
// 输出:
// 26ebd4b01363160622c8d515fb2afa76b81ea529b2f2a5bb8d6fbf53c1b3d645
4.2 cURL 示例
curl --location 'https://ibantest.yabandpay.com/api/iban/add' \
--header 'Authorization: Bearer demo_token_123456' \
--header 'Content-Type: application/json' \
--data '{
"uid": "8000000001",
"timestamp": 1731926400,
"nonce": "Qe3CkH2xP8",
"sign": "26ebd4b01363160622c8d515fb2afa76b81ea529b2f2a5bb8d6fbf53c1b3d645",
"data": {
"request_id": "req_202411180049",
"customer_id": "CUST20260001",
"timezone": "Asia/Shanghai",
"customer_type": "legal_entity",
"holder_name": "Yabandpay B.V.",
"alternative_name": "YB HQ",
"creditor_identifier": "YABAND001",
"access": 1,
"status": 1,
"remark": "NL account opening"
}
}'
5. 服务端返回错误码
| code | 说明 |
|---|---|
| 401 | 缺少 Authorization、Bearer 格式错误,或缺少 uid/sign/timestamp/nonce/data |
| -1001 | 签名错误 |
| -2001 | uid 对应账户不存在,或账户缓存中未找到 |
| -2010 | 当前账户没有该接口访问权限 |
| -3036 | 来源 IP 不在白名单 |
| -3100 | Header 里的 token 和 uid 对应账户不匹配 |
6. 对接注意事项
sign必须使用小写 64 位十六进制字符串。data中的字段会被“提到外层”后再排序签名,不是直接对整个 JSON 做签名。role字段即使传了,也不会参与签名。- 目前中间件只检查
timestamp和nonce是否存在,还没有做超时校验和防重放校验。 - 当前签名函数会把值直接按字符串拼接,所以
data里建议只传标量值,不建议直接传数组或对象。 - Header 的
Authorization和 Body 的uid必须属于同一个 API 账户,否则会返回-3100。 - 如果账户配置了 IP 白名单,必须从白名单 IP 发起请求。
7. 联调自测用例
下面给 10 条联调时最常见的输入和预期结果,方便对接方逐条排查:
| 编号 | 输入情况 | 预期结果 |
|---|---|---|
| 1 | Header、Body、签名都正确,且有接口权限 | 成功通过中间件 |
| 2 | 缺少 Authorization Header |
返回 401,提示缺少认证信息 |
| 3 | Authorization 不是 Bearer xxx 格式 |
返回 401,提示 Token 格式错误 |
| 4 | Header 里 Token 为空 | 返回 401,提示 Token 无效 |
| 5 | Body 缺少 uid |
返回 401 |
| 6 | Body 缺少 sign |
返回 401 |
| 7 | Body 中 data 为空数组或空值 |
返回 401 |
| 8 | uid 对应账户存在,但 Header Token 不是这个账户的 Token |
返回 -3100 |
| 9 | 账户配置了 IP 白名单,但请求来源 IP 不在白名单 | 返回 -3036 |
| 10 | 参与签名的任意字段被改动,但 sign 没有重新计算 |
返回 -1001 |
8. 对接建议
建议调用方把下面 4 项都打印到自己的请求日志里,排查会很快:
- 原始请求 Body
- 排序后的待签名字段
- 最终待签名原串
- 计算出来的
sign
这样一旦联调失败,双方只需要比对“待签名原串”是否一致,通常几分钟就能定位问题。