鉴权 & 签名
物流巴巴开放平台采用业界通用的 HMAC-SHA256 签名机制 + IP 白名单 双重鉴权,从协议层防止 AppKey 泄漏被盗用。本页详细说明签名算法和踩坑要点。
签名公式
Signature = HMAC-SHA256(
AppSecret,
METHOD + "\n" + PATH + "\n" + Timestamp + "\n" + Nonce + "\n" + MD5(BODY)
)将 5 个字段用换行符(\n 即字节 0x0A)连接成消息串,再用 AppSecret 作为密钥,对消息串做 HMAC-SHA256 计算,最终输出 64 位小写十六进制字符串作为签名。
请求头规范
所有 OpenAPI 请求必须在 HTTP Header 中携带以下 4 个字段:
| Header 字段 | 必填 | 说明 |
|---|---|---|
| X-Awice-AppKey | 是 | 控制台创建应用时获得的 AppKey |
| X-Awice-Timestamp | 是 | Unix 时间戳(秒),容忍 ±300 秒 |
| X-Awice-Nonce | 是 | 8-32 字节随机串,10 分钟内不可重复 |
| X-Awice-Signature | 是 | HMAC-SHA256 签名(64 位小写 hex) |
字段计算细节
- METHOD:大写的 HTTP 方法(
POST/GET),不含 HTTP 版本号。 - PATH:请求路径,不含 query string,不含 host。例如
/openapi/v1/freight/fcl/search。注意请求 URL 是https://www.5688.cn/api+ PATH,但签名时只用 PATH。 - Timestamp:Unix 时间戳(秒),服务端容忍客户端 ±300 秒时钟漂移。建议每次请求实时获取。
- Nonce:每次请求必须不同的随机串。服务端会缓存最近 10 分钟内的所有 Nonce,重复使用返回 2004。
- MD5(BODY):请求体的 32 位小写十六进制 MD5。
- POST/PUT:对原始 JSON 字符串做 MD5(注意空格、字段顺序要稳定)
- GET/DELETE:BODY 为空字符串,
md5("") = d41d8cd98f00b204e9800998ecf8427e
签名错误常见原因
如果你拿到 2002 签名校验失败,95% 的情况是下面这几个原因:
- 时钟不同步:云服务器 / 本地机器 NTP 没开,时间偏差 > 5 分钟,实际返回的是 2003 而非 2002。先用
date命令检查系统时间。 - 路径包含 query string:签名时只能用 PATH 部分,错误示例:
/openapi/v1/x?a=1正确:/openapi/v1/x。 - JSON 序列化方式不一致:客户端用
json.dumps()带默认空格 vs 不带,会导致 MD5 完全不同。建议固定使用json.dumps(..., separators=(',', ':'))紧凑格式,且服务端按你发的 raw body 计算 MD5(不会重新序列化)。 - AppSecret 包含多余空白:从控制台复制时复制到了换行或空格,建议在代码里
trim()一下。 - METHOD 大小写错误:必须是
POST(大写),不是post。
多语言签名工具函数
PHP
<?php
function buildSignature(string $method, string $path, string $body, string $appSecret): array {
$ts = (string)time();
$nonce = bin2hex(random_bytes(8));
$msg = strtoupper($method) . "\n" . $path . "\n" . $ts . "\n" . $nonce . "\n" . md5($body);
$sign = hash_hmac('sha256', $msg, $appSecret);
return [
'timestamp' => $ts,
'nonce' => $nonce,
'signature' => $sign,
];
}
// 用法:
$body = json_encode(['pol' => 'CNSHA', 'pod' => 'USLSA']);
$hdrs = buildSignature('POST', '/openapi/v1/freight/fcl/search', $body, $appSecret);Python
import hmac, hashlib, time, secrets
def build_signature(method: str, path: str, body: str, app_secret: str) -> dict:
ts = str(int(time.time()))
nonce = secrets.token_hex(8)
body_md5 = hashlib.md5(body.encode()).hexdigest()
msg = f"{method.upper()}\n{path}\n{ts}\n{nonce}\n{body_md5}".encode()
sig = hmac.new(app_secret.encode(), msg, hashlib.sha256).hexdigest()
return {'timestamp': ts, 'nonce': nonce, 'signature': sig}
# 用法:
import json
body = json.dumps({'pol': 'CNSHA', 'pod': 'USLSA'})
hdrs = build_signature('POST', '/openapi/v1/freight/fcl/search', body, app_secret)Node.js
const crypto = require('crypto');
function buildSignature(method, path, body, appSecret) {
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(8).toString('hex');
const bodyMd5 = crypto.createHash('md5').update(body).digest('hex');
const msg = `${method.toUpperCase()}\n${path}\n${ts}\n${nonce}\n${bodyMd5}`;
const sig = crypto.createHmac('sha256', appSecret).update(msg).digest('hex');
return { timestamp: ts, nonce, signature: sig };
}
// 用法:
const body = JSON.stringify({ pol: 'CNSHA', pod: 'USLSA' });
const hdrs = buildSignature('POST', '/openapi/v1/freight/fcl/search', body, appSecret);Go
package awice
import (
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
type SignHeaders struct {
Timestamp string
Nonce string
Signature string
}
func BuildSignature(method, path, body, appSecret string) (*SignHeaders, error) {
ts := fmt.Sprintf("%d", time.Now().Unix())
nonceBytes := make([]byte, 8)
if _, err := rand.Read(nonceBytes); err != nil {
return nil, err
}
nonce := hex.EncodeToString(nonceBytes)
bodyMd5 := md5.Sum([]byte(body))
bodyMd5Hex := hex.EncodeToString(bodyMd5[:])
msg := strings.ToUpper(method) + "\n" + path + "\n" + ts + "\n" + nonce + "\n" + bodyMd5Hex
h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(msg))
sig := hex.EncodeToString(h.Sum(nil))
return &SignHeaders{Timestamp: ts, Nonce: nonce, Signature: sig}, nil
}Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.Map;
public class AwiceSigner {
public static Map<String, String> buildSignature(
String method, String path, String body, String appSecret) throws Exception {
String ts = String.valueOf(System.currentTimeMillis() / 1000);
byte[] nonceBytes = new byte[8];
new SecureRandom().nextBytes(nonceBytes);
String nonce = HexFormat.of().formatHex(nonceBytes);
String bodyMd5 = HexFormat.of().formatHex(
MessageDigest.getInstance("MD5").digest(body.getBytes(StandardCharsets.UTF_8)));
String msg = method.toUpperCase() + "\n" + path + "\n" + ts + "\n" + nonce + "\n" + bodyMd5;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = HexFormat.of().formatHex(mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)));
return Map.of("timestamp", ts, "nonce", nonce, "signature", signature);
}
// 用法:
// String body = "{ \"pol\":\"CNSHA\",\"pod\":\"USLSA\" }";
// Map<String,String> hdrs = buildSignature("POST", "/openapi/v1/freight/fcl/search", body, appSecret);
}