主题
调用示例
说明
以下示例默认使用测试环境地址;正式环境请将 baseUrl 替换为 https://api.juyu.top。
仓库内可运行脚本:docs/demo.mjs(Node)、docs/demo.py(Python)。
Java(JDK 8)
JDK 8 + Jackson(Spring 项目通常已内置)。HTTP 使用 HttpURLConnection(java.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+ 内置 HttpClient、HMACSHA256、System.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 runNode.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"})