跳到主要内容
版本:v2.0

V2 接入示例

V2 接入方(appid 为 V2 版本)在调用所有接口(含 getToken 与业务接口)时,均需在请求头中附加 HMAC-SHA256 签名。

信息

自 2026 年 9 月 21 日 起,新生成的 appid 均为 V2 版 APPID。

推荐:使用 SDK 自动签名​

Node.js 和 Python SDK 已内置 V2 签名,不需要自己计算签名,也不需要手动获取 Token。 普通服务端项目也可以使用,不要求部署为信多帮云应用。

SDK 负责参数排序与编码、时间戳、随机串和 HMAC-SHA256 签名,为获取 Token 和业务请求添加签名头,并缓存、刷新 Token。以下示例显式启用 V2 签名,也适用于升级到 V2 后 AppID 没有 V2 前缀的凭据。未使用 SDK 时,再参考下方签名规则和手写示例。

Node.js SDK​

安装正式版 SDK:

npm install https://paas.xinduobang.cn/sdk/releases/0.2.0/xdb-paas-sdk-javascript-0.2.0.tgz

在服务端安全配置环境变量 XDB_OPEN_APP_ID、XDB_OPEN_APP_SECRET,然后调用:

import { createOpenPlatformClient } from "@xinduobang/xdb-paas-sdk/server";

const open = createOpenPlatformClient({ signing: "v2" });
const result = await open.request("/api/productList", { pageNum: 1 });

Python SDK​

python -m pip install https://paas.xinduobang.cn/sdk/releases/0.2.0/xdb_paas_sdk-0.2.0-py3-none-any.whl

同样在服务端配置上述两个环境变量:

from xdb_paas import create_open_platform_client

open_platform = create_open_platform_client(signing="v2")
result = open_platform.request("/api/productList", {"pageNum": 1})

示例调用获取营种列表;其他接口按对应文档传入路径和业务参数即可。SDK 不代替接口授权,也不会取消接口限流。

AppSecret 只放在服务端

不要把 AppSecret 写入浏览器代码、公开仓库或日志。托管云应用由平台自动注入凭据;普通服务端项目由开发者安全配置。V2 接入即使已有 Token,仍需要服务端 AppSecret 计算签名。SDK 默认只根据 AppID 的 V2 前缀自动识别,其他 V2 凭据应按上例显式启用签名。

更多 SDK 用法见调用开放平台。

签名规则​

  1. 构造待签名 body:将全部请求参数按 key 升序排列,用 URL-encode 拼接(等同于 http_build_query)。
  2. 拼接待签名串:timestamp + "\n" + nonce + "\n" + body
  3. 计算签名:以 secret 为密钥,对待签名串计算 HMAC-SHA256,取 hex 小写。
body = url_encode_sorted(params)
待签名串 = timestamp + "\n" + nonce + "\n" + body
X-Signature = lowercase( hex( HMAC-SHA256( secret, 待签名串 ) ) )

签名请求头​

Header说明
X-TimestampUnix 秒级时间戳
X-Nonce≥ 16 位随机字符串,每次请求唯一
X-Signature签名值

接入流程​

  1. 用 V2 接入凭据的 appid 和 secret(AppID 可能以 V2 开头,也可能由后台升级为 V2) 调用 /api/getToken,请求体 + 签名头一起发送,获取 token。
  2. 调用业务接口时,请求体中携带 token,同时附加签名头。

PHP​

<?php

$baseUrl = 'https://open.xinduobang.cn';
$appid = 'V2YOUR_APPID';
$secret = 'YOUR_SECRET';

// ========== 签名工具函数 ==========

/** 参数按 key 升序 + http_build_query */
function v2_build_body(array $params): string {
ksort($params);
return http_build_query($params);
}

/** 生成签名头 */
function v2_sign_headers(string $body, string $secret): array {
$timestamp = (string)time();
$nonce = bin2hex(random_bytes(16));
$signStr = $timestamp . "\n" . $nonce . "\n" . $body;
return [
'X-Timestamp' => $timestamp,
'X-Nonce' => $nonce,
'X-Signature' => hash_hmac('sha256', $signStr, $secret),
];
}

/** 发起带签名的 POST 请求 */
function v2_post(string $url, array $params, string $secret): array {
$body = v2_build_body($params);
$headers = v2_sign_headers($body, $secret);

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'X-Timestamp: ' . $headers['X-Timestamp'],
'X-Nonce: ' . $headers['X-Nonce'],
'X-Signature: ' . $headers['X-Signature'],
]);
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp, true);
}

// ========== Step 1: 获取 Token(需要签名) ==========

$tokenResult = v2_post($baseUrl . '/api/getToken', [
'appid' => $appid,
'secret' => $secret,
], $secret);

if ($tokenResult['code'] !== 0) {
die("getToken 失败: " . $tokenResult['msg'] . "\n");
}

$token = $tokenResult['data']['token'];
echo "获取 token 成功: " . $token . "\n\n";

// ========== Step 2: 调用 catOrder(需要签名) ==========

$orderResult = v2_post($baseUrl . '/api/catOrder', [
'token' => $token,
'ordernum' => 'NO20230511203452547280',
'pageNum' => 1,
'pageSize' => 10,
], $secret);

if ($orderResult['code'] !== 0) {
die("catOrder 失败: " . $orderResult['msg'] . "\n");
}

echo "catOrder 成功,共 " . $orderResult['totalnum'] . " 条订单\n";
print_r($orderResult['data']);

Java​

使用 JDK 11+ 内置 HttpClient,无需额外依赖。

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class V2Demo {

private static final String BASE_URL = "https://open.xinduobang.cn";
private static final String APPID = "V2YOUR_APPID";
private static final String SECRET = "YOUR_SECRET";
private static final SecureRandom RANDOM = new SecureRandom();

// ========== 签名工具 ==========

/** 参数按 key 升序 + URL-encode */
static String buildBody(Map<String, String> params) {
return new TreeMap<>(params).entrySet().stream()
.map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
+ "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}

/** 计算 HMAC-SHA256 hex 小写 */
static String hmacSha256(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) hex.append(String.format("%02x", b));
return hex.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/** 生成随机 nonce */
static String generateNonce() {
byte[] bytes = new byte[16];
RANDOM.nextBytes(bytes);
StringBuilder hex = new StringBuilder();
for (byte b : bytes) hex.append(String.format("%02x", b));
return hex.toString();
}

/** 发起带签名的 POST 请求 */
static String post(String url, Map<String, String> params) throws Exception {
String body = buildBody(params);
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = generateNonce();
String signature = hmacSha256(timestamp + "\n" + nonce + "\n" + body, SECRET);

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", signature)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();

return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
}

// ========== 主流程 ==========

public static void main(String[] args) throws Exception {
// Step 1: 获取 Token(需要签名)
String tokenResp = post(BASE_URL + "/api/getToken", Map.of(
"appid", APPID,
"secret", SECRET
));
System.out.println("getToken 响应: " + tokenResp);

// 简单解析 token(实际项目建议用 Gson / Jackson)
String token = tokenResp.split("\"token\":\"")[1].split("\"")[0];
System.out.println("获取 token 成功: " + token);

// Step 2: 调用 catOrder(需要签名)
String orderResp = post(BASE_URL + "/api/catOrder", Map.of(
"token", token,
"ordernum", "NO20230511203452547280",
"pageNum", "1",
"pageSize", "10"
));
System.out.println("catOrder 响应: " + orderResp);
}
}

Node.js​

使用 SDK 时无需编写下方签名函数,安装与调用方法见本页前面的“推荐:使用 SDK 自动签名”。以下为不使用 SDK 时的手写示例。

使用 Node.js 内置 fetch(v18+)和 crypto,无需额外依赖。

const crypto = require('crypto');

const BASE_URL = 'https://open.xinduobang.cn';
const APPID = 'V2YOUR_APPID';
const SECRET = 'YOUR_SECRET';

// ========== 签名工具 ==========

/** 参数按 key 升序 + URL-encode */
function buildBody(params) {
const encode = value => encodeURIComponent(value)
.replace(/[!'()*~]/g, char => '%' + char.charCodeAt(0).toString(16).toUpperCase())
.replace(/%20/g, '+');
return Object.keys(params)
.sort()
.filter(k => params[k] !== null && params[k] !== undefined)
.map(k => encode(k) + '=' + encode(typeof params[k] === 'boolean' ? Number(params[k]) : params[k]))
.join('&');
}

/** 发起带签名的 POST 请求 */
async function post(url, params) {
const body = buildBody(params);
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString('hex');
const signStr = `${timestamp}\n${nonce}\n${body}`;
const signature = crypto.createHmac('sha256', SECRET).update(signStr).digest('hex');

const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
body,
});
return resp.json();
}

// ========== 主流程 ==========

async function main() {
// Step 1: 获取 Token(需要签名)
const tokenResult = await post(BASE_URL + '/api/getToken', {
appid: APPID,
secret: SECRET,
});

if (tokenResult.code !== 0) {
console.error('getToken 失败:', tokenResult.msg);
process.exit(1);
}

const token = tokenResult.data.token;
console.log('获取 token 成功:', token);

// Step 2: 调用 catOrder(需要签名)
const orderResult = await post(BASE_URL + '/api/catOrder', {
token,
ordernum: 'NO20230511203452547280',
pageNum: 1,
pageSize: 10,
});

if (orderResult.code !== 0) {
console.error('catOrder 失败:', orderResult.msg);
process.exit(1);
}

console.log(`catOrder 成功,共 ${orderResult.totalnum} 条订单`);
console.log(JSON.stringify(orderResult.data, null, 2));
}

main();

上面的简化函数适用于标量参数;对象或数组字段请按对应接口要求先转为 JSON 字符串,或使用 SDK 的 PHP 嵌套表单编码。


常见错误排查​

调用返回 4001 / V2签名校验失败 时,按下列顺序自查。关键前提:服务端不看你发送的 raw body 字节,而是把 $_REQUEST(GET + POST + Cookie 合并)全部参数按 key 升序 + PHP http_build_query 重新拼一次再做 HMAC。因此客户端签名基串里的 body 必须与这条规则的字节结果完全一致。

1. body 拼接:URL-encode 语言差异(最高频原因)​

签名基串的 body 必须等于 PHP http_build_query(ksort(params)) 的字节结果。各语言默认 encode 行为并不同步:

字符PHP http_build_query(RFC1738)JS encodeURIComponentJava URLEncoder.encodePython urllib.parse.urlencodecurl --data-urlencode
@%40@(不编码)%40%40%40
(空格)+%20++%20
~~(不编码)~(不编码)%7E%7E%7E
**(不编码)%2A*(不编码)%2A%2A
+%2B%2B%2B%2B%2B
特别注意 @

V2 的 token 前缀为 token@@...,含两个 @。JS encodeURIComponent 不会 encode @——若按 Node 段手写示例请确保使用本页面顶部的 buildBody(已针对 @ 等做安全处理);否则客户端签名基串是 token=token@@...、服务端重拼是 token=token%40%40...,永远对不上。

60 秒自查方法:把你签名时用的 body 字符串原样打印出来,用一行 PHP 命令做对照:

php -r 'echo http_build_query(["pageNum"=>"1","pageSize"=>"1","token"=>"<粘贴你的token>"]);'

两者必须逐字节一致(包括 %40、%2B 之类)。若不同,问题就出在 encode 规则。

2. 参数集合:$_REQUEST 污染​

服务端把 GET + POST + Cookie 全部合并成 $_REQUEST 一起排序签名。以下常见做法会把签名带偏:

  • 请求 URL 后面带了 ?t=123 之类的缓存戳 → 会进入签名基串,但客户端多半只算了 POST 参数;
  • HTTP 客户端自动附带 Cookie(Postman 的 Cookie Jar、curl -b、浏览器 fetch 带 credentials) → PHPSESSID 等会被并入签名;
  • 表单里多传了一个未文档化的调试字段(如 debug=1)。

正确做法:请求 URL 不带任何 query string、请求头不带 Cookie,签名与发送都只包含本接口文档列出的业务参数 + token。

3. 时间戳 / Nonce​

  • X-Timestamp 必须是 Unix 秒级时间戳(不是毫秒),与服务器时间偏差 ≤ 5 分钟;
  • 服务器 / 本机时钟漂移是间歇性签名失败的常见来源,建议启用 NTP;
  • X-Nonce 每次请求唯一、长度 ≥ 16,同一 nonce 短时间重复可能被防重放拒绝。

4. HMAC 基串与输出​

  • 基串顺序固定为 timestamp + "\n" + nonce + "\n" + body,换行是 LF(\n),不是 CRLF(\r\n);
  • 算法为 HMAC-SHA256,密钥是接入凭据里的 secret(前后不能带空格 / 换行,从配置读取时注意 trim);
  • 结果取 hex 小写。

5. 请求头是否送达​

  • X-Timestamp / X-Nonce / X-Signature 三个头一个都不能缺;
  • 通过公司网关 / 反向代理 / WAF 时确认这些自定义头未被剥离(部分网关默认只透传白名单头)。

服务端 failure reason 对照​

平台日志里的 signReason 会指明具体失败类型。自查仍无解时,把下面这三样一起发给平台协助定位:签名用的 body 原文、X-Timestamp 与 X-Nonce 值、你的 appid(不要发 secret):

signReason含义首要排查方向
missing_header三个签名头之一未到达§5 网关 / SDK 是否发送
bad_signatureHMAC 结果不一致§1 body 编码、§2 参数集合、§4 secret 是否带空格
expired_timestamp时间戳偏差 > 5 分钟§3 时钟;注意秒级不是毫秒
nonce_replaynonce 重复§3 每次生成新 nonce

兜底建议​

  • 优先使用官方 SDK(Node / Python 内置 V2 签名,见本页顶部「推荐:使用 SDK 自动签名」),无需关心上述所有编码细节;
  • 未使用 SDK 时直接抄本页 PHP 段——PHP 端的 http_build_query 与服务端一致,可规避 §1 差异;其他语言手写时务必把 §1 差异表当验收清单逐条对照;
  • 排查签名问题的最短路径:把客户端签名用的 body 打印出来与服务端重拼结果做字节 diff,比反复改代码猜原因高效得多。
提示

签名基串只要与服务端重拼结果任一字节不同就会失败;参数顺序、URL-encode、Cookie 混入、CRLF、多余空格都会导致同样的报错。写非 PHP 客户端时,把「与 http_build_query(ksort(params)) 逐字节一致」当验收标准。