Skip to content

如何签名与验签

更新时间: 2026-09-16 21:49

国际版 API 使用一套无 Token 的服务端签名机制:普通 API 请求由商户签给接口服务;普通 API 响应由接口服务签给商户;Webhook 通知请求由平台签给商户。三类场景的密钥、请求方向和 参与签名字段不同,必须分别实现。

商户管理员或明确授权的技术人员在商户 Portal 的“国际支付接入 → API 接入”中创建 API 凭据。API Secret 由平台安全生成,创建成功后只在当前页面会话中展示一次;平台不接受自定义 Secret。刷新或关闭浏览器后,商户、PMS 和技术支持都不能再次查看或找回该 Secret。商户应立即保存到 服务端 Vault、KMS 或受控环境变量。

常规轮换采用“创建替代 Key → 新旧 Key 并行 → 用 last_used_at 确认切换 → 停用旧 Key 观察 → 确认无流量后吊销旧 Key”。如果操作人无法取得当前 Secret,或怀疑 Secret 已泄露, 都必须创建替代 Key 并吊销旧 Key。

API 请求如何签名

普通 API 请求是商户后端调用接口服务。商户使用 API Secret 对本次 HTTP 请求生成 HMAC-SHA256 签名,并把 API Key、签名时间、随机串和签名值放入独立请求头。

1. 准备签名材料

材料说明
API Key商户后台获取的接口调用凭据,用于识别商户、密钥和权限;调用来源由平台运营侧安全组控制。
API Secret与 API Key 对应的服务端密钥,只用于本地计算签名,不能发送给接口服务。
HTTP 请求方法使用大写方法,例如 POSTGET
请求 URL去掉域名后的 Path 和原始 Query,例如 /intl/v1/payment/order/query?merchantOrderNo=DEMO_PAY_202606010001
请求时间戳Unix 秒级时间戳文本,通常允许与接口服务端时间相差 ±300 秒。
请求随机串Nonce 的值,也可称为 nonce;同一 API Key 下每次请求必须唯一,最长 128 字符。
请求报文主体最终发送到网络上的 UTF-8 原始 Body;GET 没有 Body 时使用空字符串。

2. 获取 HTTP 请求方法

取实际调用接口使用的 HTTP 方法,并转成大写:

text
POST

3. 获取请求 URL

请求 URL 只包含 Path 和 Query,不包含协议、域名、端口和 fragment。Query 必须使用客户端最终实际 发送的原始顺序和编码,不要重新排序、解码或二次编码。

text
/intl/v1/payment/order/create

带 Query 的查询接口示例:

text
/intl/v1/payment/order/query?merchantOrderNo=DEMO_PAY_202606010001

余额查询的请求 URL 按实际筛选条件生成;不传 countrycurrency 时,签名使用:

text
/intl/v1/balance/query

同时传国家和币种筛选时,签名使用实际 Query:

text
/intl/v1/balance/query?country=PH&currency=PHP

4. 生成请求时间戳

时间戳使用 Unix 秒,并作为 Header 文本发送。商户服务器必须通过 NTP 等方式保持时间准确;时间偏差过大时接口服务会在业务处理前拒绝请求。

bash
date +%s

示例值:

text
1780272000

5. 生成请求随机串

Nonce 用于降低重放风险。同一 API Key 下不要复用随机串;重试也必须重新生成随机串。

bash
openssl rand -hex 16

示例值:

text
nonce_fixture_001

6. 获取请求报文主体

POST 请求使用最终发送的原始 JSON 字符串参与签名。签名前后不得改变空格、换行、字段顺序或字符 编码;如果框架会重新序列化 JSON,必须使用真正发出的字节内容签名。

GET 请求没有请求体,第五行为空字符串,但仍然要保留最后一个换行字节。

7. 构造请求签名原文

请求签名原文固定五行,每一行后都必须追加换行字节 \n,包括最后一行。如果某个参数本身以 \n 结尾,也需要额外追加本规则要求的 \n

text
HTTP请求方法\n
请求URL\n
请求时间戳文本\n
请求随机串\n
请求报文主体\n

示例:

text
POST\n
/intl/v1/payment/order/create\n
1780272000\n
nonce_fixture_001\n
{"merchantOrderNo":"DEMO_PAY_202606010001","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","amount":{"value":"100.50"},"orderDescription":"Virtual order"}\n

8. 计算签名值

使用 API Secret 对签名原文计算 HMAC-SHA256,并转成小写十六进制字符串,再加上 v1= 前缀。

bash
printf 'POST\n/intl/v1/payment/order/create\n1780272000\nnonce_fixture_001\n{"merchantOrderNo":"DEMO_PAY_202606010001","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","amount":{"value":"100.50"},"orderDescription":"Virtual order"}\n' \
  | openssl dgst -sha256 -hmac "$INTL_API_SECRET" -hex

最终签名格式:

text
v1=<64位小写十六进制>

9. 组装请求签名 Header

http
Api-Key: ik_live_xxx
Timestamp: 1780272000
Nonce: nonce_fixture_001
Signature: v1=<64位小写十六进制>

注意:

  1. 请求 Header 中不包含 API Secret。
  2. TimestampNonceSignature 必须与参与签名的值完全一致。
  3. 普通 API 请求还必须发送标准 HTTP 格式 Header:Accept: application/json;POST 请求还必须发送 Content-Type: application/json。这两个 Header 不参与签名原文。
  4. Header 名称大小写不敏感;文档统一展示无品牌前缀的规范 Header 名,调用时不要自行添加产品、公司或品牌前缀。
  5. 请求和响应报文格式见基本规则
  6. 推荐优先使用 SDK,自研实现必须在上线前通过平台提供的请求签名测试向量。

API 响应如何验签

普通 API 响应是接口服务返回给商户。接口服务会用本次请求 API Key 对应的 API Secret 对响应原始 Body 生成签名,商户收到响应后应先验签,再按 JSON 解析和处理业务状态。

1. 获取响应头

普通 API 响应头包含以下字段:

Header返回时机说明
Content-Type所有带 JSON Body 的普通 API 响应固定为 application/json,表示响应 Body 是 UTF-8 JSON;不参与响应验签原文。
Request-Id所有普通 API 响应请求追踪号,用于报障、日志关联和对账。
Timestamp可识别 API Key 的普通 API 响应响应签名时间,Unix 秒级时间戳文本。
Nonce可识别 API Key 的普通 API 响应响应随机串,本次响应唯一。
Signature可识别 API Key 的普通 API 响应v1= 加小写 HMAC-SHA256 十六进制响应签名。

如果接口服务无法识别 API Key,例如 Api-Key 缺失或格式严重错误,认证失败响应可能无法生成 响应签名。此时商户仍应保存 Request-Id、HTTP 状态码和错误码用于排障。

2. 检查时间戳

检查 Timestamp 与商户服务器当前时间的偏差。默认建议允许最大 300 秒偏差;超过偏差时应拒绝把该 响应作为可信结果处理,并记录 Request-Id 排查网络代理或时钟问题。响应签名算法固定为 HMAC-SHA256,签名前缀固定为 v1=

3. 获取响应原始 Body

验签必须使用原始 Response Body。不要使用已经格式化、转义、重新排序字段或重新序列化后的 JSON 字符串。若响应 Body 为空,第三行使用空字符串,但仍然保留最后一个换行字节。

4. 构造响应验签原文

响应验签原文固定三行,每一行后都必须追加换行字节 \n,包括最后一行:

text
响应时间戳文本\n
响应随机串\n
响应报文主体\n

示例:

text
1780272000\n
nonce_fixture_response_001\n
{"platOrderNo":"PHI112606010000000001001","status":"PROCESSING"}\n

5. 计算并比对签名

商户使用本次请求 API Key 对应的 API Secret,对响应验签原文计算 HMAC-SHA256,小写十六进制后加 v1= 前缀,并与响应头 Signature 做常量时间比较。

验签不通过时:

  1. 不要按响应 Body 更新业务最终状态。
  2. 记录环境、时间、商户订单号、Request-Id 响应 Header、HTTP 状态码和响应 Body error.code
  3. 收到已验签的 HTTP 5xx 创建响应,或响应无法验签时,都不能直接确认订单是否已经创建; 必须先按原 merchantOrderNo 查单。连续查询一段时间仍不存在时,才能用同一单号和完全一致的原请求重发。
  4. 排查代理、网关或框架是否丢失响应头、修改 Body、自动解压或重编码。

Webhook 响应如何验签

Webhook 是平台主动调用商户通知地址的请求。这里的“验签”是商户验证平台发来的 Webhook 请求;商户对平台的 HTTP 应答不需要签名。

1. 获取 Webhook 请求头

Webhook 请求头使用独立投递 Header。签名元信息命名与普通 API 请求和响应保持一致:

Header说明
Content-Type固定为 application/json,表示 Webhook Body 是 UTF-8 JSON;不参与 Webhook 验签原文。
Request-Id本次投递请求追踪号,用于排障和日志关联。
Webhook-Secret-Id本次投递使用的 Webhook Secret 标识;用于在验签前选择本地保存的 Webhook Secret。商户在 Portal 生成或轮换 Webhook Secret 时获取并保存。
Timestamp本次投递签名时间,Unix 秒级时间戳文本。
Nonce本次投递随机串,重试时可能变化。
Signaturev1= 加小写 HMAC-SHA256 十六进制通知签名。

2. 选择 Webhook Secret

商户应根据 Webhook-Secret-Id 精确选择本地保存的 Webhook Secret。商户必须保存 Portal 展示的映射关系, 例如 whsecid_old_001 -> whsec_oldwhsecid_new_002 -> whsec_new。找不到 Secret ID、Secret 已停用、或商户环境不匹配时, 必须拒绝处理该通知。

3. 检查时间戳和事件去重键

验签前先检查 Timestamp 是否超过允许偏差,默认建议 300 秒。重试投递会重新生成 Request-IdTimestampNonceSignature,但 Body eventId 保持同一个事件 ID。商户必须按 Body eventId 做幂等。Header Timestamp 表示本次投递签名时间,并以文本发送;Body order.updateTime 表示本次通知订单状态的生效时间,以 JSON number 返回,同一事件重试时保持稳定。

4. 获取 Webhook 原始 Body

Webhook 验签必须使用收到的 UTF-8 原始 Body。不要先解析 JSON、格式化 JSON、替换换行或改变字段 顺序。很多框架读取 Body 后不能再次读取,接入时要在过滤器或中间件里提前缓存原始字节。

5. 构造 Webhook 验签原文

Webhook 验签原文固定三行,每一行后都必须追加换行字节 \n,包括最后一行:

text
投递时间戳文本\n
投递随机串\n
Webhook请求报文主体\n

示例:

text
1780272000\n
nonce_fixture_webhook_payment_001\n
{"eventId":"evt_fixture_001","eventType":"PAYMENT_SUCCEEDED","merchantNo":"MCH_FIXTURE_001","order":{"platOrderNo":"PHI112606010000000001001","merchantOrderNo":"DEMO_PAY_202606010001","status":"SUCCEEDED","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","country":"PH","amount":{"value":"100.50","currency":"PHP"},"referenceNo":"REF-20260601-0001","customData":{"cartId":"CART-10001","customerRef":"CUST-90001"},"createTime":1780272000,"updateTime":1780275600}}\n

6. 计算并比对签名

使用 Webhook-Secret-Id 对应的 Webhook Secret,对验签原文计算 HMAC-SHA256,小写十六进制后加 v1= 前缀,并与请求头 Signature 做常量时间比较。

验签通过后再解析 JSON,并按 Body eventId 做幂等处理。验签失败、Webhook Secret ID 未知或 Body 被篡改时, 返回 4xx 或 5xx;不要更新订单状态。

7. 应答要求

商户只有在事件已可靠落库或进入可靠异步队列后,才返回任意 HTTP 2xx 响应,无需返回业务数据。 平台会将任意 2xx 响应视为接收成功;非 2xx 响应、超时或网络异常会触发重试。 业务处理耗时较长时,应先完成验签、幂等和落库,再异步处理业务。