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 写入浏览器代码、公开仓库或日志。托管云应用由平台自动注入凭据;普通服务端项目由开发者安全配置。V2 接入即使已有 Token,仍需要服务端 AppSecret 计算签名。SDK 默认只根据 AppID 的 V2 前缀自动识别,其他 V2 凭据应按上例显式启用签名。
更多 SDK 用法见调用开放平台。
签名规则
- 构造待签名 body:将全部请求参数按 key 升序排列,用 URL-encode 拼接(等同于
http_build_query)。 - 拼接待签名串:
timestamp + "\n" + nonce + "\n" + body - 计算签名:以
secret为密钥,对待签名串计算 HMAC-SHA256,取 hex 小写。
body = url_encode_sorted(params)
待签名串 = timestamp + "\n" + nonce + "\n" + body
X-Signature = lowercase( hex( HMAC-SHA256( secret, 待签名串 ) ) )
签名请求头
| Header | 说明 |
|---|---|
| X-Timestamp | Unix 秒级时间戳 |
| X-Nonce | ≥ 16 位随机字符串,每次请求唯一 |
| X-Signature | 签名值 |
接入流程
- 用 V2 接入凭据的
appid和secret(AppID 可能以V2开头,也可能由后台升级为 V2) 调用/api/getToken,请求体 + 签名头一起发送,获取token。 - 调用业务接口时,请求体中携带
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 encodeURIComponent | Java URLEncoder.encode | Python urllib.parse.urlencode | curl --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_signature | HMAC 结果不一致 | §1 body 编码、§2 参数集合、§4 secret 是否带空格 |
expired_timestamp | 时间戳偏差 > 5 分钟 | §3 时钟;注意秒级不是毫秒 |
nonce_replay | nonce 重复 | §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)) 逐字节一致」当验收标准。