🔐 京东JOS API Sign签名算法详解与MD5实现(Java / Python / PHP 三版)
京东开放平台(JOS)统一使用 MD5签名,规则和1688/TOP非常接近但有两个关键差异:
- 时间戳用秒级(10位),不是毫秒
- 业务参数必须放
360buy_param_json(紧凑JSON字符串)参与签名
一、JOS 签名算法官方步骤(背下来)
假设
App Key = 123456,App Secret = abcdefg- 收集所有API参数(含公共参数):
app_key,method,timestamp,format,v,sign_method360buy_param_json(值为业务参数字典的紧凑JSON字符串)- 若有
access_token(订单类)也参入 - 剔除:
sign字段、值为None或空字符串、file二进制 - 按参数名 ASCII 升序排序
- 拼接 key+value(无
=无&)app_key123456formatjsonmethodjingdong.ware.read.gettimestamp1700000000v2.0360buy_param_json{"wareId":"1000123","fields":"title,price"} - 首尾拼 AppSecret:
AppSecret + 上步字符串 + AppSecret
- MD5 → 32位大写
sign = MD5( AppSecret + KV_ASCII_sorted + AppSecret ).upper()
⚠️360buy_param_json值必须是 JSON紧凑格式(无空格,中文ensure_ascii=False),原样参与签名,不 URL-Encode 再签名。
二、Python 实现(推荐直接用)
# jd_jos_sign.py
import hashlib
import json
from typing import Dict, Optional
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
def jd_sign(params: Dict[str, Optional[str]], app_secret: str) -> str:
"""
JOS MD5 Sign
params: 所有API参数(含 app_key/method/timestamp/360buy_param_json/access_token)
不包含 sign
空值/None 剔除
"""
filt = {
k: v for k, v in params.items()
if v is not None and str(v).strip() != "" and k != "sign"
}
# ASCII 升序
sorted_items = sorted(filt.items(), key=lambda x: x[0])
# 拼 key+value
qs = "".join(f"{k}{v}" for k, v in sorted_items)
# 首尾拼 secret → MD5 → 大写
raw = f"{app_secret}{qs}{app_secret}"
return hashlib.md5(raw.encode("utf-8")).hexdigest().upper()
# ===================== 验证示例 =====================
if __name__ == "__main__":
APP_SECRET = "YOUR_JD_APP_SECRET"
# 业务参数 → 紧凑JSON
biz = {"wareId": "100012345678",
"fields": "ware_id,title,price,jd_price,stock_num,skus_json"}
api_params = {
"app_key": "YOUR_JD_APP_KEY",
"method": "jingdong.ware.read.get",
"timestamp": str(int(__import__('time').time())), # ← 秒级!
"format": "json",
"v": "2.0",
"sign_method": "md5",
"360buy_param_json": json.dumps(biz, ensure_ascii=False,
separators=(',', ':')) # 紧凑
# "access_token": "SELLER_TOKEN" # 订单类接口加这句
}
sign = jd_sign(api_params, APP_SECRET)
api_params["sign"] = sign
print("✅ sign =", sign)
print("\n发送参数示例:")
for k, v in api_params.items():
print(f" {k} = {v if k!='sign' else sign}")三、Java 实现
// JdSignUtil.java
import java.security.MessageDigest;
import java.util.Map;
import java.util.TreeMap;
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
public class JdSignUtil {
public static String sign(Map<String, String> params, String appSecret)
throws Exception {
// 1. 剔除 sign + 空值
TreeMap<String, String> tree = new TreeMap<>();
for (Map.Entry<String, String> e : params.entrySet()) {
if (!"sign".equals(e.getKey())
&& e.getValue() != null
&& !e.getValue().trim().isEmpty()) {
tree.put(e.getKey(), e.getValue());
}
}
// 2. 拼 key+value
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : tree.entrySet()) {
sb.append(e.getKey()).append(e.getValue());
}
// 3. 首尾拼 secret
String toSign = appSecret + sb.toString() + appSecret;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(toSign.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : digest) {
hex.append(String.format("%02X", b & 0xFF));
}
return hex.toString(); // 已大写
}
// 用法:
// Map<String,String> p = new HashMap<>();
// p.put("method","jingdong.ware.read.get");
// p.put("app_key","KEY");
// p.put("timestamp", String.valueOf(System.currentTimeMillis()/1000)); // 秒!
// p.put("format","json"); p.put("v","2.0"); p.put("sign_method","md5");
// p.put("360buy_param_json", "{\"wareId\":\"1000123\",\"fields\":\"title,price\"}");
// String sign = JdSignUtil.sign(p, APP_SECRET);
}四、PHP 实现
<?php
// jd_sign.php
function jdSign(array $params, string $appSecret): string {
unset($params['sign']);
$filtered = array_filter($params, fn($v) => $v !== null && $v !== '');
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
ksort($filtered);
$str = '';
foreach ($filtered as $k => $v) {
$str .= $k . $v;
}
$toSign = $appSecret . $str . $appSecret;
return strtoupper(md5($toSign));
}
// ---- 示例 ----
$params = [
'method' => 'jingdong.ware.read.get',
'app_key' => 'YOUR_JD_APP_KEY',
'timestamp' => (string)(time()), // ← 秒级
'format' => 'json',
'v' => '2.0',
'sign_method' => 'md5',
'360buy_param_json'=> json_encode(
['wareId'=>'100012345678','fields'=>'ware_id,title,price'],
JSON_UNESCAPED_UNICODE
)
];
$sign = jdSign($params, 'YOUR_JD_APP_SECRET');
$params['sign'] = $sign;
echo "sign = {$sign}\n";
?>五、签名排错清单(JOS特有)
现象 | 原因 | 排查 |
|---|---|---|
Invalid Signature | timestamp 用毫秒 | 必须 int(time.time())秒级 |
Invalid Signature | 360buy_param_json含空格/\n | 用 separators=(',',':')/ JSON_UNESCAPED_UNICODE紧凑 |
Invalid Signature | 空值参入签名 | 严格过滤 None/'' |
sign fail偶发 | 系统时钟偏差大 | 服务器 NTP 同步(±5分钟内) |
商品接口OK订单403 | 非签名问题 | 订单需企业应用+卖家AccessToken,见之前订单对接文 |
360buy_param_jsonURL编码再签 | 签错 | 先签原始JSON串,签完再整体URL Encode放POST body |
六、面试/对接一句话
京东JOS签名 = 参数按key ASCII升序拼key+value(含360buy_param_json紧凑JSON串,空值/sign剔除)→ 首尾拼AppSecret→ MD5 → 大写;timestamp必须用秒级10位,不同于1688的毫秒。
需要我补 京东商品全量SKU翻页同步 或 京东订单增量同步APScheduler(断点续跑+Token刷新) 完整脚本吗?