Skip to content

自检与排查

对接不通时,请按本页顺序排查。绝大多数问题集中在三处:OAEP 哈希不是 SHA-1canonical 签名串没有逐字节对齐服务器时钟不准

排查前先跑 selfcheck

v2/selfcheck 是只读探活接口,不创建订单、不改任何状态,可反复调用。一次成功的 selfcheck 同时证明上行(你的加密+签名能被服务端接受)与下行(服务端响应能被你验签+解密)都正确。它应当是你排查的第一步,也是收敛问题范围最快的手段。

1. 排查总表

现象可能原因
signatureFailedcanonical 串不一致(换行符、字段顺序)、签名算法不是 SHA256、用错密钥、缺/错 ts/nonce、时间戳超窗、nonce 重复
解密失败 / 乱码OAEP 哈希不是 SHA-1、分段大小不是 214/256、用错密钥
tokenExpiredtoken 失效,重新 getToken
Oops来源 IP 不在白名单
请求被拒(体过大)请求体超过平台上限(默认 128KB)

通用排查顺序:

  1. 确认 OAEP 用 SHA-1(见第 3 节);
  2. 确认 canonical 串用 \n(LF)连接,且字段顺序为 ts, nonce, body
  3. 确认密钥角色:上行用平台公钥加密、商户私钥签名;下行用平台公钥验签、商户私钥解密;
  4. 确认服务器时间同步(NTP)。

完整错误码列表见错误码

2. signatureFailed 深挖

signatureFailed 是最常见、也最容易误判的错误——它不只代表"签名算错了"。

一个错误码,四种触发原因

服务端把以下四类问题统一报为 signatureFailed

#触发原因说明
1验签失败canonical 串不一致、签名算法不是 SHA256withRSA、用错密钥
2时间戳超窗ANONY-TIMESTAMP 偏离服务端时间超过 ±5 分钟(±300 秒)
3nonce 重复同一商户号的 nonce 在 900 秒内重复出现(服务端 Redis SET NX EX 900 去重)
4必带头缺失ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN 任一缺失或为空

所以签名代码"看起来没问题"时,不要只盯着签名——时钟和 nonce 同样会报这个错。

按下面的顺序排查,成本从低到高:

第一步:校时(NTP)

时间戳窗口是 ±5 分钟,服务器时钟漂移几分钟就会间歇性失败(重启 NTP 前后时好时坏是典型症状)。

  • 确认服务器开启 NTP 同步(timedatectl / chronyc tracking)。
  • selfcheck 直接对时:其响应解密后含 serverTime 字段,与你本机 time() 对比,差值超过几十秒就先修时钟。

第二步:重试必须换新 nonce

服务端对 (商户号, nonce) 去重,重复即拒绝。这意味着:

  • 每一次 HTTP 请求(包括超时重试、失败重试)都必须重新生成 nonce、重新取时间戳、重新签名。
  • 把整个"密文 + 头"缓存下来原样重发,必然命中 nonce 去重,报 signatureFailed
  • nonce 必须是 ≥16 字节 CSPRNG 随机串(hex / base64 均可,建议定长 hex)。用毫秒时间戳、自增序号当 nonce 都不合格且容易撞重。

第三步:逐字节核对 canonical 串

签名/验签的输入必须是(这是规范,不是示例):

ANONY-TIMESTAMP + "\n" + ANONY-NONCE + "\n" + <body>

逐项核对:

  • 三段用 \n(LF,0x0A)连接——不是 \r\n(CRLF),不是空格,末尾没有多余换行。Windows 环境和某些模板引擎容易悄悄混入 \r
  • 字段顺序固定为 ts, nonce, body,不可调换。
  • <body> 是密文,不是明文:上行请求中 <body> = OAEP 加密后的 Base64 密文(即实际发出的 HTTP body);下行响应/回调中 <body> = 响应体里的 Base64 密文(外层 JSON 的 data 字段值 / 回调的原始 body)。对业务明文 JSON 签名是最高频的错法之一。
  • 签名算法是 SHA256withRSA(PKCS#1 v1.5 签名填充),不是 MD5、不是 PSS。
  • 签名用商户私钥,验下行签名用平台公钥——两把钥匙别拿反。

把你拼出的 canonical 串落盘后用 xxd 看十六进制,确认分隔符是 0a 且没有 0d(见第 4 节脚本)。

3. OAEP 必须 SHA-1(五语言写法)

加密是 RSA-OAEP,MGF1 = SHA-1。很多语言默认 OAEP-SHA256,两边不一致会解不开(表现为"解密失败/乱码")。明文按 214 字节分段逐段加密、密文拼接后整体 Base64;解密时 Base64 解码后按 256 字节分段。

各语言显式写法(务必显式指定,不要依赖默认值):

语言写法
PHPopenssl_public_encrypt($d,$o,$k, OPENSSL_PKCS1_OAEP_PADDING)(默认即 SHA-1)
Pythonpadding.OAEP(mgf=MGF1(SHA1()), algorithm=SHA1(), label=None)
Node{ padding: RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' }
Gorsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil)
JavaCipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")
php
// PHP 的 OPENSSL_PKCS1_OAEP_PADDING 默认即 SHA-1,无需额外参数
openssl_public_encrypt($d, $o, $k, OPENSSL_PKCS1_OAEP_PADDING);
python
# cryptography 库:OAEP 与 MGF1 都必须显式指定 SHA1
padding.OAEP(mgf=MGF1(SHA1()), algorithm=SHA1(), label=None)
js
// oaepHash 必须显式写 'sha1',Node 默认是 sha1 但建议显式声明
{ padding: RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' }
go
// 第一个参数决定 OAEP 哈希,必须是 sha1.New()
rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil)
java
// transformation 字符串里写死 SHA-1 + MGF1
Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");

直接使用官方 SDKPHP / Python / Node / Go / Java)可完全避开这一坑。

4. 手工 debug:用 openssl 复现签名与解密

当 SDK 行为存疑时,可以脱离代码、用 openssl 命令行独立复现整条链路。以下脚本在任何有 openssl / jq 的 Linux 机器上可直接运行。

准备:还原 PEM 密钥凭据base64(PEM) 格式,先解一层 Base64):

bash
# private_u = 商户私钥(PKCS#8),public_t = 平台公钥(SPKI)
echo "$MERCHANT_PRIVATE_U_B64" | base64 -d > private_u.pem
echo "$SERVER_PUBLIC_T_B64"    | base64 -d > public_t.pem

# 能正常解析说明还原成功
openssl pkey -in private_u.pem -noout -text | head -2
openssl pkey -pubin -in public_t.pem -noout -text | head -2

复现上行签名(对 canonical 串做 SHA256withRSA):

bash
TS=$(date +%s)                                # 秒级 Unix 时间戳
NONCE=$(openssl rand -hex 16)                 # ≥16 字节 CSPRNG
BODY='<实际发出的 HTTP body,即 Base64 密文,原样一整行>'

# 用 printf 拼 canonical 串:三段 LF 连接、末尾无换行。
# 千万不要用 echo —— 它会追加尾随换行导致签名必错。
printf '%s\n%s\n%s' "$TS" "$NONCE" "$BODY" > canonical.txt

# 十六进制自检:分隔符必须是 0a(LF),不能出现 0d(CR)
xxd canonical.txt | head -3

# 商户私钥签名,Base64 后即 ANONY-SIGN 头的值
openssl dgst -sha256 -sign private_u.pem canonical.txt | base64 -w0

把这个签名与你 SDK 产出的 ANONY-SIGN 对比:同样的 ts/nonce/body 下两者必须完全一致。不一致说明你的 canonical 拼接或签名算法有问题。

验证下行签名(响应/回调同一套逻辑):

bash
# 保存完整响应:curl -sD headers.txt -o response.json ...
RESP_TS=$(grep -i '^ANONY-TIMESTAMP' headers.txt | tr -d '\r' | awk '{print $2}')
RESP_NONCE=$(grep -i '^ANONY-NONCE' headers.txt | tr -d '\r' | awk '{print $2}')
grep -i '^ANONY-SIGN' headers.txt | tr -d '\r' | awk '{print $2}' | base64 -d > resp_sign.bin

# 用标准 JSON 工具取 data(jq 会自动还原 \/ 转义,切勿手写字符串切割)
DATA=$(jq -r '.data' response.json)

printf '%s\n%s\n%s' "$RESP_TS" "$RESP_NONCE" "$DATA" > resp_canonical.txt

# 平台公钥验签,输出 "Verified OK" 即通过
openssl dgst -sha256 -verify public_t.pem -signature resp_sign.bin resp_canonical.txt

解密下行 data(商户私钥,OAEP SHA-1,按 256 字节分段):

bash
printf '%s' "$DATA" | base64 -d > cipher.bin
split -b 256 cipher.bin seg_
for f in seg_*; do
  openssl pkeyutl -decrypt -inkey private_u.pem -in "$f" \
    -pkeyopt rsa_padding_mode:oaep \
    -pkeyopt rsa_oaep_md:sha1 -pkeyopt rsa_mgf1_md:sha1
done > plain.json
cat plain.json   # 应为业务 JSON 明文

如果 openssl 能解开而你的代码解不开,问题就在你代码的 OAEP 参数(回到第 3 节);如果 openssl 也解不开,通常是密钥拿错(下行必须用商户私钥 private_u 解密,不是平台公钥)。

5. 充值未入账 / 未收到回调

先确认回调机制侧的常规项:callback_url 是否配置且公网可达、是否按约定返回成功应答、是否做了时间窗 + nonce 去重(去重逻辑写错会把正常回调当重放拒掉)。然后注意以下特殊情况:

TRON 链 USDC 充值已关闭

TRON-USDC 充值通道已关闭:向 TRON 收款地址转入 USDC,平台不入账、不回调。 请勿引导用户在 TRON 链上用 USDC 充值。当前可用的充值链(addressType)为 trx / solana / eth_bnb / tonbtc 为预留值,Bitcoin 仍在接入中。提现币种以接口文档列出的 13 种枚举为准。如需 USDC,请使用 eth.usdc / solana.usdc / bnb.usdc

实现细节:小额充值不触发回调

充值到账金额低于该币种的 deposit_min(最低充值额,可通过公共接口 getCurrencies 查询)时不会触发回调。排查"到账了但没回调"时先核对金额是否达到门槛。

实现细节:回调超时与重试

平台回调你的 callback_url 时超时时间为 3 秒,失败后按 2s → 30s → 2min → 5min → 15min → 60min 退避重试,最多重试 5 次后停止。请保证回调处理接口快速返回(先落库、异步处理业务),否则会被判为超时进入重试。存款回调需返回纯文本 SUCCESS(大写)才计为成功。

6. 仍未解决?联系平台

以上都排查过仍无法定位时,通过 Telegram 联系 @anonypay,并附上以下信息(缺一项都会拖慢定位):

  • APP-ID(商户号);
  • 问题发生的精确时间点(精确到秒,注明时区),便于平台侧检索日志;
  • 完整请求头ANONY-APP-ID / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN 原样贴出,ANONY-TOKEN 必须脱敏(如仅保留前 4 位 + ****);
  • 请求 body 密文的前若干字符、收到的响应 code / message

切勿发送密钥

任何情况下都不要把商户私钥 private_usecret、完整 token 发给任何人——包括自称平台人员的人。平台排查问题不需要你的私钥。

相关页面