Skip to content

调用示例

说明

以下示例默认使用测试环境地址;正式环境请将 baseUrl 替换为 https://api.juyu.top

仓库内可运行脚本:docs/demo.mjs(Node)、docs/demo.py(Python)。

Java(JDK 8)

JDK 8 + Jackson(Spring 项目通常已内置)。HTTP 使用 HttpURLConnectionjava.net.http.HttpClient 需 JDK 11+)。签名规则见 基础约定

java
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 开放网关验签联调示例(JDK 8 + HttpURLConnection,与 docs/demo.py 行为对齐)。
 * 运行前设置环境变量 OPENAPI_ACCESS_KEY、OPENAPI_SECRET_KEY(可选 OPENAPI_BASE_URL)。
 */
public class OpenApiDemo {

    private static final ObjectMapper JSON = new ObjectMapper();
    private static final List<String> EXCLUDED = Arrays.asList("_t", "sign");
    private static final String BASE_URL = Optional.ofNullable(System.getenv("OPENAPI_BASE_URL"))
            .orElse("http://203.2.112.61:8095")
            .replaceAll("/+$", "");
    private static final String ACCESS_KEY = System.getenv("OPENAPI_ACCESS_KEY");
    private static final String SECRET_KEY = System.getenv("OPENAPI_SECRET_KEY");

    public static void main(String[] args) throws Exception {
        if (isBlank(ACCESS_KEY) || isBlank(SECRET_KEY)) {
            System.err.println("请设置环境变量 OPENAPI_ACCESS_KEY、OPENAPI_SECRET_KEY(可选 OPENAPI_BASE_URL)。");
            System.exit(1);
        }

        signedGet("/openapi/demo/ping", Collections.<String, String>emptyMap());

        Map<String, Object> body = new LinkedHashMap<String, Object>();
        body.put("cusSelfOid", "ORDER-001");
        signedPostJson("/openapi/order/createOrder", Collections.<String, String>emptyMap(), body);
    }

    /** GET:仅 Query 参与签名 */
    static void signedGet(String path, Map<String, String> query) throws Exception {
        String ts = String.valueOf(System.currentTimeMillis() / 1000);
        TreeMap<String, String> signParams = mergeSignParams(true, query, null);
        String canonical = buildCanonical(signParams, ts, ACCESS_KEY.trim());
        String sign = hmacSha256Hex(canonical, SECRET_KEY.trim());

        Map<String, String> headers = new LinkedHashMap<String, String>();
        headers.put("X-Access-Key", ACCESS_KEY.trim());
        headers.put("X-Timestamp", ts);
        headers.put("X-Sign", sign);

        HttpResult result = httpRequest("GET", BASE_URL + path + buildQueryString(query), headers, null);
        System.out.println(result.status + " " + result.body);
    }

    /** POST JSON:Query + Body 一级字段合并参与签名,同名以 Body 为准 */
    static void signedPostJson(String path, Map<String, String> query, Map<String, Object> bodyObj) throws Exception {
        String body = JSON.writeValueAsString(bodyObj);
        String ts = String.valueOf(System.currentTimeMillis() / 1000);
        TreeMap<String, String> signParams = mergeSignParams(false, query, body);
        String canonical = buildCanonical(signParams, ts, ACCESS_KEY.trim());
        String sign = hmacSha256Hex(canonical, SECRET_KEY.trim());

        Map<String, String> headers = new LinkedHashMap<String, String>();
        headers.put("Content-Type", "application/json; charset=UTF-8");
        headers.put("X-Access-Key", ACCESS_KEY.trim());
        headers.put("X-Timestamp", ts);
        headers.put("X-Sign", sign);

        HttpResult result = httpRequest("POST", BASE_URL + path + buildQueryString(query), headers,
                body.getBytes(StandardCharsets.UTF_8));
        System.out.println(result.status + " " + result.body);
    }

    static HttpResult httpRequest(String method, String url, Map<String, String> headers, byte[] body)
            throws Exception {
        HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
        conn.setConnectTimeout(10_000);
        conn.setReadTimeout(30_000);
        conn.setRequestMethod(method);
        if (headers != null) {
            for (Map.Entry<String, String> e : headers.entrySet()) {
                conn.setRequestProperty(e.getKey(), e.getValue());
            }
        }
        if (body != null) {
            conn.setDoOutput(true);
            OutputStream out = null;
            try {
                out = conn.getOutputStream();
                out.write(body);
            } finally {
                if (out != null) {
                    out.close();
                }
            }
        }
        int status = conn.getResponseCode();
        InputStream in = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
        String respBody = readUtf8(in);
        conn.disconnect();
        return new HttpResult(status, respBody);
    }

    static String readUtf8(InputStream in) throws Exception {
        if (in == null) {
            return "";
        }
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        byte[] b = new byte[4096];
        int n;
        try {
            while ((n = in.read(b)) != -1) {
                buf.write(b, 0, n);
            }
        } finally {
            in.close();
        }
        return buf.toString("UTF-8");
    }

    /** 合并 Query 与 Body 一级字段(GET 仅 Query) */
    static TreeMap<String, String> mergeSignParams(boolean isGet, Map<String, String> query, String jsonBody)
            throws Exception {
        TreeMap<String, String> merged = new TreeMap<String, String>();
        if (query != null) {
            for (Map.Entry<String, String> e : query.entrySet()) {
                if (e.getKey() != null && e.getValue() != null) {
                    merged.put(e.getKey(), e.getValue());
                }
            }
        }
        if (!isGet && !isBlank(jsonBody)) {
            Map<String, Object> raw = JSON.readValue(jsonBody, new TypeReference<Map<String, Object>>() {});
            for (Map.Entry<String, Object> e : raw.entrySet()) {
                if (e.getKey() == null || e.getValue() == null) {
                    continue;
                }
                merged.put(e.getKey(), toSignString(e.getValue()));
            }
        }
        return merged;
    }

    /** 与 OpenApiSignBodyFirstLevelValue 一致 */
    static String toSignString(Object value) throws Exception {
        if (value instanceof String) {
            return (String) value;
        }
        if (value instanceof Number || value instanceof Boolean || value instanceof Character) {
            return String.valueOf(value);
        }
        return JSON.writeValueAsString(value);
    }

    static String buildCanonical(TreeMap<String, String> params, String timestamp, String accessKey) {
        TreeMap<String, String> copy = new TreeMap<String, String>(params);
        for (String key : EXCLUDED) {
            copy.remove(key);
        }

        StringJoiner joiner = new StringJoiner("&");
        for (Map.Entry<String, String> e : copy.entrySet()) {
            joiner.add(e.getKey() + "=" + e.getValue());
        }
        return joiner.toString() + "|ts=" + timestamp + "|ak=" + accessKey;
    }

    static String hmacSha256Hex(String canonical, String secretKey) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] raw = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder(raw.length * 2);
        for (byte b : raw) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    static String buildQueryString(Map<String, String> query) throws Exception {
        if (query == null || query.isEmpty()) {
            return "";
        }
        StringJoiner joiner = new StringJoiner("&");
        for (Map.Entry<String, String> e : query.entrySet()) {
            if (e.getValue() != null) {
                joiner.add(urlEncode(e.getKey()) + "=" + urlEncode(e.getValue()));
            }
        }
        String qs = joiner.toString();
        return qs.isEmpty() ? "" : "?" + qs;
    }

    static String urlEncode(String s) throws Exception {
        return URLEncoder.encode(s, "UTF-8").replace("+", "%20");
    }

    static boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }

    static class HttpResult {
        final int status;
        final String body;

        HttpResult(int status, String body) {
            this.status = status;
            this.body = body;
        }
    }
}

C#(.NET 6.0)

.NET 6+ 内置 HttpClientHMACSHA256System.Text.Json ,零第三方依赖。签名规则见 基础约定

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;

/// <summary>
/// 开放网关验签联调示例(.NET 6+,与 docs/demo.py 行为对齐)。
/// 运行前设置环境变量 OPENAPI_ACCESS_KEY、OPENAPI_SECRET_KEY(可选 OPENAPI_BASE_URL)。
/// </summary>
class OpenApiDemo
{
    static readonly HashSet<string> Excluded = new() { "_t", "sign" };
    static readonly JsonSerializerOptions JsonOpts = new()
    {
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    };

    static string BaseUrl =>
        (Environment.GetEnvironmentVariable("OPENAPI_BASE_URL") ?? "http://203.2.112.61:8095").TrimEnd('/');

    static string AccessKey => Environment.GetEnvironmentVariable("OPENAPI_ACCESS_KEY") ?? "";
    static string SecretKey => Environment.GetEnvironmentVariable("OPENAPI_SECRET_KEY") ?? "";

    static async Task Main()
    {
        if (string.IsNullOrWhiteSpace(AccessKey) || string.IsNullOrWhiteSpace(SecretKey))
        {
            Console.Error.WriteLine("请设置环境变量 OPENAPI_ACCESS_KEY、OPENAPI_SECRET_KEY(可选 OPENAPI_BASE_URL)。");
            Environment.Exit(1);
        }

        using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };

        await SignedGet(http, "/openapi/demo/ping", new Dictionary<string, string>());

        var body = new Dictionary<string, object?> { ["cusSelfOid"] = "ORDER-001" };
        await SignedPostJson(http, "/openapi/order/createOrder", new Dictionary<string, string>(), body);
    }

    /// <summary>GET:仅 Query 参与签名</summary>
    static async Task SignedGet(HttpClient http, string path, Dictionary<string, string> query)
    {
        var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var signParams = MergeSignParams(isGet: true, query, bodyStr: null);
        var canonical = BuildCanonical(signParams, ts, AccessKey.Trim());
        var sign = HmacSha256Hex(canonical, SecretKey.Trim());

        using var request = new HttpRequestMessage(HttpMethod.Get, BaseUrl + path + BuildQueryString(query));
        request.Headers.Add("X-Access-Key", AccessKey.Trim());
        request.Headers.Add("X-Timestamp", ts);
        request.Headers.Add("X-Sign", sign);

        using var response = await http.SendAsync(request);
        var text = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"{(int)response.StatusCode} {text}");
    }

    /// <summary>POST JSON:Query + Body 一级字段合并参与签名,同名以 Body 为准</summary>
    static async Task SignedPostJson(
        HttpClient http, string path, Dictionary<string, string> query, Dictionary<string, object?> bodyObj)
    {
        var body = JsonSerializer.Serialize(bodyObj, JsonOpts);
        var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var signParams = MergeSignParams(isGet: false, query, body);
        var canonical = BuildCanonical(signParams, ts, AccessKey.Trim());
        var sign = HmacSha256Hex(canonical, SecretKey.Trim());

        using var request = new HttpRequestMessage(HttpMethod.Post, BaseUrl + path + BuildQueryString(query))
        {
            Content = new StringContent(body, Encoding.UTF8, "application/json")
        };
        request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "UTF-8" };
        request.Headers.Add("X-Access-Key", AccessKey.Trim());
        request.Headers.Add("X-Timestamp", ts);
        request.Headers.Add("X-Sign", sign);

        using var response = await http.SendAsync(request);
        var text = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"{(int)response.StatusCode} {text}");
    }

    /// <summary>合并 Query 与 Body 一级字段(GET 仅 Query)</summary>
    static SortedDictionary<string, string> MergeSignParams(
        bool isGet, Dictionary<string, string>? query, string? bodyStr)
    {
        var merged = new SortedDictionary<string, string>(StringComparer.Ordinal);
        if (query != null)
        {
            foreach (var (k, v) in query)
            {
                if (k != null && v != null)
                    merged[k] = v;
            }
        }
        if (!isGet && !string.IsNullOrWhiteSpace(bodyStr))
        {
            using var doc = JsonDocument.Parse(bodyStr);
            foreach (var prop in doc.RootElement.EnumerateObject())
            {
                if (prop.Value.ValueKind == JsonValueKind.Null)
                    continue;
                merged[prop.Name] = ToSignString(prop.Value);
            }
        }
        return merged;
    }

    /// <summary>与 OpenApiSignBodyFirstLevelValue 一致</summary>
    static string ToSignString(JsonElement value) => value.ValueKind switch
    {
        JsonValueKind.String => value.GetString() ?? "",
        _ => value.GetRawText()
    };

    static string BuildCanonical(SortedDictionary<string, string> signParams, string timestamp, string accessKey)
    {
        var paramPart = string.Join("&",
            signParams.Where(e => !Excluded.Contains(e.Key)).Select(e => $"{e.Key}={e.Value}"));
        return $"{paramPart}|ts={timestamp}|ak={accessKey}";
    }

    static string HmacSha256Hex(string canonical, string secretKey)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    static string BuildQueryString(Dictionary<string, string>? query)
    {
        if (query == null || query.Count == 0)
            return "";
        var qs = string.Join("&",
            query.Where(e => e.Value != null)
                 .Select(e => $"{Uri.EscapeDataString(e.Key)}={Uri.EscapeDataString(e.Value!)}"));
        return string.IsNullOrEmpty(qs) ? "" : "?" + qs;
    }
}

Program.cs 单文件即可运行;新建控制台项目示例:

bash
dotnet new console -n OpenApiDemo -f net6.0
# 将上方代码写入 Program.cs 后:
export OPENAPI_ACCESS_KEY=你的AK OPENAPI_SECRET_KEY=你的SK
dotnet run

Node.js(crypto + fetch

Node.js 18+,零依赖,与 docs/demo.mjs 一致。

javascript
import crypto from 'crypto';

const accessKey = process.env.OPENAPI_ACCESS_KEY;
const secretKey = process.env.OPENAPI_SECRET_KEY;
const baseUrl = (process.env.OPENAPI_BASE_URL || 'http://203.2.112.61:8095').replace(/\/+$/, '');

const EXCLUDED = new Set(['_t', 'sign']);

function buildCanonical(sortedKv, timestamp, ak) {
  const paramPart = sortedKv
    .filter(([k]) => !EXCLUDED.has(k))
    .map(([k, v]) => `${k}=${v}`)
    .join('&');
  return `${paramPart}|ts=${timestamp}|ak=${ak}`;
}

function signCanonical(canonical, secret) {
  return crypto.createHmac('sha256', secret).update(canonical, 'utf8').digest('hex');
}

function firstLevelStringMap(obj) {
  const out = [];
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return out;
  for (const [k, v] of Object.entries(obj)) {
    if (v === null || v === undefined) continue;
    const t = typeof v;
    if (t === 'string') out.push([k, v]);
    else if (t === 'number' || t === 'boolean') out.push([k, String(v)]);
    else out.push([k, JSON.stringify(v)]);
  }
  return out;
}

function mergeSignParams(isGet, queryKv, bodyObj) {
  const map = new Map();
  for (const [k, v] of queryKv) {
    if (v !== null && v !== undefined) map.set(k, String(v));
  }
  if (!isGet && bodyObj) {
    for (const [k, v] of firstLevelStringMap(bodyObj)) {
      map.set(k, v);
    }
  }
  return [...map.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
}

async function signedGet(path, queryKv = []) {
  const ts = String(Math.floor(Date.now() / 1000));
  const sorted = mergeSignParams(true, queryKv, null);
  const canonical = buildCanonical(sorted, ts, accessKey);
  const sign = signCanonical(canonical, secretKey);

  const qs = new URLSearchParams();
  for (const [k, v] of queryKv) {
    if (v !== null && v !== undefined) qs.append(k, v);
  }
  const url = `${baseUrl}${path}${qs.toString() ? `?${qs}` : ''}`;
  const res = await fetch(url, {
    headers: {
      'X-Access-Key': accessKey,
      'X-Timestamp': ts,
      'X-Sign': sign,
    },
  });
  console.log(res.status, await res.text());
}

async function signedPostJson(path, queryKv, bodyObj) {
  const ts = String(Math.floor(Date.now() / 1000));
  const bodyStr = JSON.stringify(bodyObj);
  const sorted = mergeSignParams(false, queryKv, JSON.parse(bodyStr));
  const canonical = buildCanonical(sorted, ts, accessKey);
  const sign = signCanonical(canonical, secretKey);

  const qs = new URLSearchParams();
  for (const [k, v] of queryKv) {
    if (v !== null && v !== undefined) qs.append(k, v);
  }
  const url = `${baseUrl}${path}${qs.toString() ? `?${qs}` : ''}`;

  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      'X-Access-Key': accessKey,
      'X-Timestamp': ts,
      'X-Sign': sign,
    },
    body: bodyStr,
  });
  console.log(res.status, await res.text());
}

if (!accessKey || !secretKey) {
  console.error('请设置环境变量 OPENAPI_ACCESS_KEY、OPENAPI_SECRET_KEY(可选 OPENAPI_BASE_URL)。');
  process.exit(1);
}

await signedGet('/openapi/demo/ping', []);
await signedPostJson('/openapi/order/createOrder', [], { cusSelfOid: 'ORDER-001' });

Python(hmac + hashlib

docs/demo.py 一致。

python
import hashlib
import hmac
import json
import os
import time
import urllib.parse
import urllib.request
from typing import Dict, List, Optional, Tuple

ACCESS_KEY = os.environ.get("OPENAPI_ACCESS_KEY", "").strip()
SECRET_KEY = os.environ.get("OPENAPI_SECRET_KEY", "").strip()
BASE = os.environ.get("OPENAPI_BASE_URL", "http://203.2.112.61:8095").rstrip("/")
EXCLUDED = {"_t", "sign"}


def sign_body_first_level_value(v):
    if v is None:
        return None
    if isinstance(v, bool):
        return str(v).lower()
    if isinstance(v, str):
        return v
    if isinstance(v, (int, float)):
        return str(v)
    return json.dumps(v, ensure_ascii=False, separators=(",", ":"))


def first_level_from_json(body_str: str) -> dict:
    if not body_str or not body_str.strip():
        return {}
    o = json.loads(body_str)
    out = {}
    for k, v in o.items():
        if v is None:
            continue
        out[k] = sign_body_first_level_value(v)
    return out


def merge_sign_params(is_get: bool, query: dict, body_str: Optional[str]) -> List[Tuple[str, str]]:
    m: Dict[str, str] = {}
    for k, v in (query or {}).items():
        if v is not None:
            m[k] = str(v)
    if not is_get and body_str:
        for k, v in first_level_from_json(body_str).items():
            m[k] = v
    return [(k, m[k]) for k in sorted(m.keys()) if k not in EXCLUDED]


def build_canonical(sorted_kv: List[Tuple[str, str]], ts: str, ak: str) -> str:
    param_part = "&".join(f"{k}={v}" for k, v in sorted_kv)
    return f"{param_part}|ts={ts}|ak={ak}"


def sign_canonical(canonical: str, secret: str) -> str:
    mac = hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256)
    return mac.hexdigest()


def signed_get(path: str, query: Optional[dict] = None) -> None:
    ts = str(int(time.time()))
    sorted_kv = merge_sign_params(True, query or {}, None)
    canonical = build_canonical(sorted_kv, ts, ACCESS_KEY)
    sign = sign_canonical(canonical, SECRET_KEY)

    qs = urllib.parse.urlencode(query or {}, doseq=True)
    url = f"{BASE}{path}"
    if qs:
        url = f"{url}?{qs}"
    req = urllib.request.Request(
        url,
        headers={
            "X-Access-Key": ACCESS_KEY,
            "X-Timestamp": ts,
            "X-Sign": sign,
        },
        method="GET",
    )
    with urllib.request.urlopen(req) as resp:
        print(resp.status, resp.read().decode("utf-8"))


def signed_post_json(path: str, query: Optional[dict], body_obj: dict) -> None:
    body_str = json.dumps(body_obj, ensure_ascii=False, separators=(",", ":"))
    ts = str(int(time.time()))
    sorted_kv = merge_sign_params(False, query or {}, body_str)
    canonical = build_canonical(sorted_kv, ts, ACCESS_KEY)
    sign = sign_canonical(canonical, SECRET_KEY)

    qs = urllib.parse.urlencode(query or {}, doseq=True)
    url = f"{BASE}{path}"
    if qs:
        url = f"{url}?{qs}"
    req = urllib.request.Request(
        url,
        data=body_str.encode("utf-8"),
        headers={
            "Content-Type": "application/json; charset=UTF-8",
            "X-Access-Key": ACCESS_KEY,
            "X-Timestamp": ts,
            "X-Sign": sign,
        },
        method="POST",
    )
    with urllib.request.urlopen(req) as resp:
        print(resp.status, resp.read().decode("utf-8"))


if __name__ == "__main__":
    if not ACCESS_KEY or not SECRET_KEY:
        raise SystemExit("请设置环境变量 OPENAPI_ACCESS_KEY、OPENAPI_SECRET_KEY(可选 OPENAPI_BASE_URL)。")
    signed_get("/openapi/demo/ping")
    signed_post_json("/openapi/order/createOrder", {}, {"cusSelfOid": "ORDER-001"})

具语物流开放平台