Onerway
快速开始

请求签名

了解 Onerway 如何通过请求签名对 API 请求进行身份鉴权、如何生成 sign 字段,以及如何验签 webhook 通知。

Onerway 依赖 sign 字段对 API 请求进行身份鉴权。商户服务端需使用对应环境的密钥 (SECRET),根据请求报文动态计算签名 (sign);严禁在前端、移动端或公开的代码仓库中暴露 SECRET。

从服务端调用 API 前,必须完成对应环境的 IP 白名单配置。沙盒和生产环境需分别配置。

签名规则

生成 sign 时,请按以下顺序处理请求参数:

  1. 排除 sign 字段。
  2. 排除值为 null、undefined 和空字符串的字段。
  3. 若字段值为对象 (Object) 或数组 (Array),将其序列化为紧凑的 JSON 字符串(无多余空格或换行);嵌套结构的序列化规则见对象和数组字段转换。
  4. 按字段名的 ASCII 码升序排列。
  5. 仅将排序后的字段值直接拼接,不要拼接字段名、等号 (=)、与号 (&) 或任何其他分隔符;拼接后的字符串称为 Canonical String。
  6. 在 Canonical String 末尾追加 SECRET。
  7. 对最终的字符串进行 SHA-256 哈希计算,并将结果转换为小写的十六进制字符串。

非字符串标量参与拼接时:布尔值转为 true / false;数字转为十进制字符串,不含多余尾零或科学计数法(例如 1.10 拼接为 1.1)。

对象和数组字段转换

对象或数组字段需要序列化为紧凑的 JSON 字符串:

{
  "billingInformation": {
    "country": "US",
    "email": "customer@test.com"
  }
}

转换后参与签名并发送:

{
  "billingInformation": "{\"country\":\"US\",\"email\":\"customer@test.com\"}"
}

嵌套字段需要先转换内层,再转换外层。例如 txnOrderMsg.products 是数组,最终会作为 txnOrderMsg 字符串里的 JSON 字符串:

{
  "txnOrderMsg": "{\"appId\":\"replace_with_app_id\",\"products\":\"[{\\\"currency\\\":\\\"USD\\\",\\\"name\\\":\\\"test product\\\",\\\"num\\\":\\\"1\\\",\\\"price\\\":\\\"1\\\"}]\",\"returnUrl\":\"https://developers.onerway.com/example-return\"}"
}

请求示例

原始请求可以保留对象结构,sign 字段为空:

{
  "merchantNo": "replace_with_merchant_no",
  "merchantTxnId": "replace_with_unique_transaction_id",
  "merchantTxnTime": "2026-04-24 15:37:39",
  "orderAmount": "1",
  "orderCurrency": "USD",
  "billingInformation": {
    "country": "US",
    "email": "customer@test.com",
    "province": "CA"
  },
  "txnOrderMsg": {
    "appId": "replace_with_app_id",
    "products": [
      {
        "currency": "USD",
        "name": "test product",
        "num": "1",
        "price": "1"
      }
    ],
    "returnUrl": "https://developers.onerway.com/example-return"
  },
  "productType": "CARD",
  "subProductType": "DIRECT",
  "txnType": "SALE",
  "sign": null
}

发送前应把对象字段转换为字符串,并填入计算后的 sign:

{
  "billingInformation": "{\"country\":\"US\",\"email\":\"customer@test.com\",\"province\":\"CA\"}",
  "merchantNo": "replace_with_merchant_no",
  "merchantTxnId": "replace_with_unique_transaction_id",
  "merchantTxnTime": "2026-04-24 15:37:39",
  "orderAmount": "1",
  "orderCurrency": "USD",
  "productType": "CARD",
  "sign": "replace_with_calculated_signature",
  "subProductType": "DIRECT",
  "txnOrderMsg": "{\"appId\":\"replace_with_app_id\",\"products\":\"[{\\\"currency\\\":\\\"USD\\\",\\\"name\\\":\\\"test product\\\",\\\"num\\\":\\\"1\\\",\\\"price\\\":\\\"1\\\"}]\",\"returnUrl\":\"https://developers.onerway.com/example-return\"}",
  "txnType": "SALE"
}

最终请求体可以按字段名排序,便于排查;签名是否正确仅取决于签名规则中的排序和拼接结果。

签名算例

用以下最小请求和示例密钥 example_secret 对拍你的实现:

{
  "merchantNo": "demo_merchant_no",
  "merchantTxnId": "demo_txn_id_001",
  "orderAmount": "1",
  "orderCurrency": "USD",
  "sign": null
}
Canonical String: demo_merchant_nodemo_txn_id_0011USD
待哈希字符串:     demo_merchant_nodemo_txn_id_0011USDexample_secret
sign:            8af506acd9322fcab9d676dd9b861512f194d9ab724eacdb358d90d087d9caac

代码示例

以下示例默认输入是商户服务端内存中的字典 (Map) 或对象 (Object);若某个字段已经是 JSON 字符串,请确保它已按同样的由内向外规则完成内层转换。normalizeValue 的结果应同时用于签名和最终请求体;不要在计算签名后用不同的序列化方式重新构建对象或数组字段,这会导致网关验签失败。

<?php

function is_assoc_array(array $value): bool {
    if ($value === []) {
        return false;
    }
    return array_keys($value) !== range(0, count($value) - 1);
}

function normalize_nested($value) {
    if (!is_array($value)) {
        return $value;
    }

    if (!is_assoc_array($value)) {
        return array_map('normalize_nested', $value);
    }

    $result = [];
    foreach ($value as $key => $child) {
        $result[$key] = is_array($child)
            ? json_encode(normalize_nested($child), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
            : $child;
    }
    return $result;
}

function normalize_value($value) {
    if ($value === null || $value === '') {
        return $value;
    }
    if (is_bool($value)) {
        return $value ? 'true' : 'false';
    }
    return is_array($value)
        ? json_encode(normalize_nested($value), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        : (string) $value;
}

function generate_signature(array $payload, string $secret): string {
    $normalized = [];
    foreach ($payload as $key => $value) {
        if ($key !== 'sign') {
            $normalized[$key] = normalize_value($value);
        }
    }

    ksort($normalized, SORT_STRING);

    $canonical = '';
    foreach ($normalized as $value) {
        if ($value !== null && $value !== '') {
            $canonical .= $value;
        }
    }

    return hash('sha256', $canonical . $secret);
}

Webhook 验签

分账、分账回退,以及 Ethoca / RDR 报备状态通知使用通知体中的 sign 验签;其他通知使用下述 X-Rh-Signature header。各通知的签名参与字段见对应 API Reference。

分账及分账回退通知

使用当前环境的 SECRET,按签名规则计算收到的通知的签名,并与通知体中的 sign 比较。排除 sign 自身;relatedTxnId、relatedMerchantTxnId 参与签名,值为 null 时按签名规则排除。receivers 使用收到的原始 JSON 字符串,不要解析后重新序列化。完整字段见分账结果通知。

/profit/query 查询响应也返回 data.sign,但商户无需对查询响应验签。

Ethoca / RDR 报备状态通知

使用当前环境的 SECRET,按签名规则计算收到的通知的签名,并与通知体中的 sign 比较。排除 sign 自身,其余字段按通用规则参与签名;值为 null 或空字符串时按该规则排除。

id 在通知中为 JSON 数字,可能超出 JavaScript 安全整数范围。接收报文时应使用能无损保留大整数的解析方式,保留原始十进制值用于验签与应答;不要先转换为可能丢失精度的 JavaScript number。

完整字段和应答要求见 Ethoca 报备状态通知与 RDR 报备状态通知。Ethoca 预警通知仍使用下述 header 验签。

其他通知

Onerway 在通知 header X-Rh-Signature 中携带签名:逗号分隔的一项或多项 v1=<签名>,v1 标识签名方案版本。存在多个同时启用的密钥时(例如密钥轮换期间),每个启用的密钥对应一项:

X-Rh-Signature: v1=4bde57c3350c402ca8c3728697cd5d7dbd78c8f0313761607e7f538be3349c39, v1=6b543c2e887e20a98dd4629242dd60050a4cfdda3372cdc17b4422c8fe98d5fa

验签时,使用当前配置的 SECRET 按签名规则计算收到通知的签名,结果与 X-Rh-Signature 中任意一项一致即验签通过。字段选取与拼接沿用同一规则,另有两点差异:

  • 额外排除该 webhook 在 API Reference 页面「验签范围」中标注为不参与验签的字段,即使通知返回了这些字段的值;未来新增的通知字段默认参与验签。
  • 对象和数组字段在通知中已按紧凑 JSON 字符串承载,直接使用收到的原始字符串值参与拼接,不要重新解析再序列化。

通知 body 仍返回 sign 字段,但它仅使用第一个启用的密钥计算,密钥轮换期间,它可能与你使用当前配置的 SECRET 计算出的签名不同。这些通知应使用 X-Rh-Signature 验签,不要依赖 body sign;使用 body sign 的通知按前述对应小节处理。

常见错误

  • 对象 (Object) 或数组 (Array) 字段没有按最终发送形态序列化为紧凑的 JSON 字符串。
  • txnOrderMsg.products 仍是数组对象,而不是 txnOrderMsg 字符串里的 JSON 字符串。
  • 发送了当前接口未定义的字段:网关按接口定义的字段集合计算签名,多发的字段只进入商户侧计算,导致签名不一致;字段结构与文档不符同理。
  • 请求环境不匹配,例如沙盒环境的 merchantNo / SECRET 请求了生产环境 API,或生产环境凭证请求了沙盒环境 API。

需要协助排查时

请求验签失败时,请在商户服务端打印并提供以下信息:

  • 签名计算前的最终拼接字符串 (Canonical String)。
  • 生成后的 sign。
  • 请求环境:沙盒环境或生产环境。
  • 请求接口和完整请求体。敏感信息可以脱敏,但不要修改参与签名字段的值。

Webhook 验签失败时,请提供收到的完整通知报文、对应 API Reference 指定的 body sign 或 X-Rh-Signature,以及你计算的 Canonical String 与签名结果。