PHP SDK
PHP SDK 是一个自包含的单文件类 AnonyV2Client.php(获取方式见 SDK 总览),require 后即可使用,无需 Composer 依赖。它封装了 v2 协议的全部安全逻辑:
- 加密:RSA-OAEP(MGF1 = SHA-1),明文按 214 字节分段,密文按 256 字节分段;
- 签名:SHA256withRSA,canonical 串为
timestamp + "\n" + nonce + "\n" + <base64密文>; - 防重放:自动生成秒级
ANONY-TIMESTAMP与 16 字节 CSPRNGANONY-NONCE,并对下行校验 ±5 分钟时间窗。
协议细节见加密与签名协议,这里只讲怎么用。
环境要求
| 依赖 | 用途 |
|---|---|
openssl 扩展 | 加密 / 解密 / 签名 / 验签(算法层唯一依赖) |
curl 扩展 | 仅内置的 getToken() / post() 发 HTTP 时用到;改用 Guzzle 等自选 HTTP 客户端走手动模式则不需要 |
密钥格式为 base64(PEM)——即对标准 PEM 文本整段再做一次 Base64,SDK 内部会自行解码加载。密钥的获取与保管见凭据说明。
初始化
构造函数(与源码一致):
public function __construct(
$appId,
$serverPublicKeyB64,
$merchantPrivateKeyB64,
$baseUrl = 'https://api.anonypay.io/api/merchant/'
)| 参数 | 说明 |
|---|---|
$appId | 商户号,即 ANONY-APP-ID |
$serverPublicKeyB64 | 平台公钥 public_t,base64(PEM),用于加密上行、验证下行签名 |
$merchantPrivateKeyB64 | 商户私钥 private_u,base64(PEM),用于签名上行、解密下行 |
$baseUrl | 接口基址,默认 https://api.anonypay.io/api/merchant/ |
require 'AnonyV2Client.php';
$client = new AnonyV2Client(
getenv('ANONY_APP_ID'),
getenv('ANONY_SERVER_PUB_B64'), // 平台公钥 public_t
getenv('ANONY_MERCHANT_PRIV_B64') // 商户私钥 private_u
);私钥仅存于你的服务端,切勿写入前端、日志或第三方。
获取 token:getToken()
public function getToken($secret): stringgetToken 的请求体不加密、不签名,仅用 Authorization: auth + md5(APP-ID + "&" + secret) 鉴权(分隔符是 &,这是最容易写错的一步,SDK 已在 authHeader() 中封装)。但响应仍是加密 + 签名信封,不是明文 token——getToken() 会自动验签、解密并取出 token 字段返回;失败时抛 RuntimeException。
$token = $client->getToken(getenv('ANONY_SECRET'));token 在有效期内可复用,建议缓存;业务接口返回 tokenExpired 时重新获取即可。详见 getToken 接口。
一步调用业务接口:post()
public function post($path, array $data, $token): arraypost() 内部自动完成整套流程:OAEP 加密业务 JSON → 生成时间戳 / nonce → 对 canonical 串签名 → 带 ANONY-APP-ID / TOKEN / TIMESTAMP / NONCE / SIGN 五个头 POST →(成功时)对响应验签 + 解密。你只管传业务参数、拿业务数据:
$res = $client->post('createAddress', [
'userOrder' => 'A1001',
'addressType' => 'trx',
], $token);
if (($res['code'] ?? null) == 10000) {
$address = $res['data']['address']; // data 已自动验签 + 解密为数组
$apiOrder = $res['data']['apiOrder'];
} else {
// 失败:message 为原因,data 为空
error_log('createAddress failed: ' . ($res['message'] ?? ''));
}- 返回值是
['code', 'data', 'message']:code == 10000时data已解密为数组;否则原样返回外层 JSON。 - 验签或解密失败会抛
RuntimeException,请 try/catch 后按异常处理,不要当作业务失败重试。 $path相对基址书写,如createAddress、createWithdrawOrder、merchantInfo、getDeposits、checkWithdrawOrder、v2/selfcheck;参数与响应字段见接口参考。
TRON 链 USDC 充值已关闭
向 trx 类型收款地址转入 TRON-USDC 不会入账、也不会触发回调。请勿引导你的用户向平台地址充值 TRON-USDC。支持的币种以接口参考中的枚举(13 种提现币种)为准。
上线前请先用 v2/selfcheck 跑通全链路(只读、可反复调用,请求内容会被原样 echo 回来):
$res = $client->post('v2/selfcheck', ['hello' => 'anony'], $token);
// $res['data'] 形如 { "pong": true, "apiVersion": 2, "serverTime": ..., "echo": {"hello":"anony"} }自检通过即证明上行(加密 + 签名被服务端接受)与下行(响应能被你验签 + 解密)双向就绪,详见上线自检。
手动模式:buildRequest + verifyResponse
如果你想用 Guzzle、Laravel HTTP Client 等自己的 HTTP 层,用这两个方法拆开「构造请求」与「校验响应」两步。
buildRequest
public function buildRequest(array $data, $token): array返回结构:
[
'body' => '<业务 JSON 的 base64 密文>', // 作为 HTTP body 原样发送
'headers' => [
'ANONY-APP-ID' => '...',
'ANONY-TOKEN' => '...',
'ANONY-TIMESTAMP' => '...',
'ANONY-NONCE' => '...',
'ANONY-SIGN' => '...',
],
]verifyResponse
public function verifyResponse(
$encryptedBody, // base64 密文(响应的 data 字段,或回调的整个 body)
$timestamp, // 头 ANONY-TIMESTAMP
$nonce, // 头 ANONY-NONCE
$sign, // 头 ANONY-SIGN
callable $isFreshNonce = null // nonce 去重回调;同步响应可省略,回调方向必须传
): array依次执行:时间窗校验(±5 分钟)→ nonce 去重($isFreshNonce 非空时)→ 平台公钥验签 → 商户私钥解密,返回业务数据数组;任一步失败抛 RuntimeException。
Guzzle 示例
use GuzzleHttp\Client;
$http = new Client(['base_uri' => 'https://api.anonypay.io/api/merchant/', 'timeout' => 15]);
$req = $client->buildRequest(['userOrder' => 'A1001'], $token);
$r = $http->post('checkWithdrawOrder', [
'body' => $req['body'],
'headers' => $req['headers'],
]);
$body = json_decode((string) $r->getBody(), true); // 务必用标准 JSON 库解析外层
if (($body['code'] ?? null) == 10000) {
$data = $client->verifyResponse(
$body['data'],
$r->getHeaderLine('ANONY-TIMESTAMP'),
$r->getHeaderLine('ANONY-NONCE'),
$r->getHeaderLine('ANONY-SIGN')
);
}外层 JSON 中 / 可能被转义为 \/,标准 JSON 库会自动还原,不要手写字符串切割。
接收回调:verifyResponse 复用
平台回调与下行响应使用同一套 canonical 验签 + OAEP 解密逻辑,直接复用 verifyResponse。两点与同步响应不同:
- 回调体不是
{code,data}JSON 包裹,而是直接的 base64 密文——把整个 HTTP body 原样传入即可; - 必须传
$isFreshNonce。该参数省略时verifyResponse会跳过 nonce 去重——同步响应可以省略,但回调省略后可被重放。请用原子存储(如 RedisSET key 1 NX EX 900,过期 ≥ 15 分钟)实现:首次见到该 nonce 返回true,重复返回false。
<?php
// callback.php —— 平台回调入口
require 'AnonyV2Client.php';
$client = new AnonyV2Client(
getenv('ANONY_APP_ID'),
getenv('ANONY_SERVER_PUB_B64'),
getenv('ANONY_MERCHANT_PRIV_B64')
);
$encryptedBody = file_get_contents('php://input'); // 回调体 = base64 密文
$timestamp = $_SERVER['HTTP_ANONY_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_ANONY_NONCE'] ?? '';
$sign = $_SERVER['HTTP_ANONY_SIGN'] ?? '';
try {
$data = $client->verifyResponse($encryptedBody, $timestamp, $nonce, $sign, 'isFreshNonce');
} catch (\RuntimeException $e) {
http_response_code(400); // 时间窗 / nonce 重复 / 验签失败:拒绝,不要处理业务
exit;
}
if ($data['businessType'] === 'deposit') {
// 充值到账:按 userOrder 记账(建议自身也做幂等)
// $data: txid, address, addressType, fromAddress, amount, currency, fee, apiOrder, userOrder
} elseif ($data['businessType'] === 'withdraw') {
// 提现状态:按 orderStatus 更新订单
// $data: toAddress, amount, currency, fee, apiOrder, userOrder, withdrawType, message, txid, orderStatus
}
echo 'SUCCESS'; // 处理成功后按约定返回纯文本应答nonce 去重函数 isFreshNonce 的两种实现:
function isFreshNonce($nonce)
{
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 注意:'nx' 必须作为位置参数(数组值),'ex' 为键值对
return $redis->set('anony:cb:nonce:' . $nonce, '1', ['nx', 'ex' => 900]) === true;
}function isFreshNonce($nonce)
{
$redis = new Predis\Client();
// SET key 1 EX 900 NX:设置成功返回 OK,nonce 已存在返回 null
return (string) $redis->set('anony:cb:nonce:' . $nonce, '1', 'EX', 900, 'NX') === 'OK';
}应答约定(实现细节)
存款回调需返回纯文本 SUCCESS(大写)才计为成功;提现回调 SUCCESS = 确认推进、FAIL = 拒绝并退款、其他应答会触发平台重试(退避 2s → 60min,最多 5 次)。此外提现还有 businessType = withdrawalPendingConfirm 的待确认回调(无 txid/orderStatus)。完整回调类型与应答语义见回调说明。
底层工具方法(静态,可单独调用)
如需自行组装流程或移植到其他框架,可直接使用以下静态方法:
| 方法 | 说明 |
|---|---|
encrypt($plaintext, $publicKeyB64) | RSA-OAEP(SHA-1) 加密,214 字节分段,返回 base64 密文 |
decrypt($cipherB64, $privateKeyB64) | 解密,base64 解码后按 256 字节分段 |
sign($canonical, $privateKeyB64) | SHA256withRSA 签名,返回 base64 |
verify($canonical, $publicKeyB64, $signB64) | 验签,返回 bool |
canonical($ts, $nonce, $payload) | 拼 canonical 串 ts . "\n" . nonce . "\n" . payload |
newNonce() | 16 字节 CSPRNG,返回 32 位定长 hex |
authHeader($appId, $secret) | 'auth ' . md5($appId . '&' . $secret),getToken 的 Authorization 头值 |
相关常量:OAEP_MAX_PLAIN = 214、RSA_BLOCK = 256、TS_WINDOW = 300(±5 分钟)。
常见异常与排查
| SDK 抛出的异常消息 | 原因 |
|---|---|
timestamp out of window | 下行时间戳超出 ±5 分钟窗,检查服务器 NTP 时间同步 |
duplicate nonce (replay) | $isFreshNonce 判定 nonce 重复,回调被重放(或你的去重键未按 nonce 隔离) |
signature verify failed | 验签失败:canonical 串不一致或平台公钥配置错误 |
getToken failed: ... | md5 鉴权失败等,message 为服务端原因 |
invalid public key / invalid private key | 密钥不是 base64(PEM) 格式,或公私钥用反了 |
decrypt failed: ... | 用错私钥,或密文被改动 |
服务端返回的业务错误码(signatureFailed、tokenExpired、Oops 白名单拦截等)见错误码与排障指南。