鉴权 & 签名

物流巴巴开放平台采用业界通用的 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-TimestampUnix 时间戳(秒),容忍 ±300 秒
X-Awice-Nonce8-32 字节随机串,10 分钟内不可重复
X-Awice-SignatureHMAC-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% 的情况是下面这几个原因:

  1. 时钟不同步:云服务器 / 本地机器 NTP 没开,时间偏差 > 5 分钟,实际返回的是 2003 而非 2002。先用 date 命令检查系统时间。
  2. 路径包含 query string:签名时只能用 PATH 部分,错误示例:/openapi/v1/x?a=1 正确:/openapi/v1/x
  3. JSON 序列化方式不一致:客户端用 json.dumps() 带默认空格 vs 不带,会导致 MD5 完全不同。建议固定使用 json.dumps(..., separators=(',', ':')) 紧凑格式,且服务端按你发的 raw body 计算 MD5(不会重新序列化)。
  4. AppSecret 包含多余空白:从控制台复制时复制到了换行或空格,建议在代码里 trim() 一下。
  5. 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);
}
鉴权 & 签名 - 物流巴巴开放平台文档 | 5688.cn