API签名验签说明

适用范围: 所有经过 VerifyApiSign 中间件的对外 API 接口。

代码依据:

  • app/Http/Middleware/VerifyApiSign.php
  • app/Helpers/functions.php 中的 getSignByYabandPayV3()

1. 整体校验流程

服务端收到请求后,会按下面顺序校验:

  1. 校验 Header 里是否有 Authorization: Bearer {token}。
  2. 校验 Body 里是否包含 uid、sign、timestamp、nonce、data。
  3. 按 uid 找到对应 API 账户。
  4. 校验 Header 里的 token 是否和该账户绑定的 token 一致。
  5. 如果该账户配置了 IP 白名单,则校验来源 IP。
  6. 用该账户的 secret_key 重新计算签名。
  7. 比较服务端签名和请求里的 sign 是否完全一致。
  8. 校验当前接口路径是否在该账户允许调用的接口权限范围内。
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:

  1. 准备完整请求 Body。
  2. 删除 sign 字段。
  3. 如果有 role 字段,也删除,因为它不参与签名。
  4. 取出 data 对象里的所有字段,平铺到外层。
  5. 再把外层的 uid、timestamp、nonce 一起放入同一个待签名集合。
  6. 按字段名升序排序。
  7. 按 key=value 方式拼接。
  8. 用 & 把所有键值对连成一个字符串。
  9. 使用 secret_key 作为密钥,对这个字符串做 HMAC-SHA256。
  10. 得到 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&timestamp=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. 对接注意事项

  1. sign 必须使用小写 64 位十六进制字符串。
  2. data 中的字段会被“提到外层”后再排序签名,不是直接对整个 JSON 做签名。
  3. role 字段即使传了,也不会参与签名。
  4. 目前中间件只检查 timestamp 和 nonce 是否存在,还没有做超时校验和防重放校验。
  5. 当前签名函数会把值直接按字符串拼接,所以 data 里建议只传标量值,不建议直接传数组或对象。
  6. Header 的 Authorization 和 Body 的 uid 必须属于同一个 API 账户,否则会返回 -3100。
  7. 如果账户配置了 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 项都打印到自己的请求日志里,排查会很快:

  1. 原始请求 Body
  2. 排序后的待签名字段
  3. 最终待签名原串
  4. 计算出来的 sign

这样一旦联调失败,双方只需要比对“待签名原串”是否一致,通常几分钟就能定位问题。

results matching ""

    No results matching ""