调用说明
概述
OwlProxy OpenAPI 使用 V2 SHA-256 签名进行请求鉴权。
获取 AK/SK
调用 API 前,请联系技术对接人获取:
Access Key ID(AK)Secret Access Key(SK)
请妥善保管 SK,不要在前端代码、公开仓库或日志中暴露。
请求头
每个 OpenAPI 请求必须包含以下鉴权请求头:
| 请求头 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
X-Access-Key | string | 是 | Access Key ID | ak_xxxxxxxxxxxx |
X-Timestamp | string | 是 | 10 位 Unix 秒级时间戳 | 1747555200 |
X-Sign | string | 是 | 64 位 SHA-256 十六进制签名 | 6069dfb4cb3ac57b... |
Content-Type | string | POST/PUT 必填 | 请求体类型 | application/json |
X-Timestamp 与当前时间的偏差不能超过 5 分钟。
签名算法
签名字符串直接拼接,字段之间不添加分隔符:
signString = SK + X-Timestamp + path + bodyOrQuery
X-Sign = lowerHex(SHA-256(signString_UTF8))
| 字段 | 取值规则 |
|---|---|
SK | Secret Access Key |
X-Timestamp | 与请求头完全一致的秒级时间戳 |
path | 完整请求路径,例如 /owlproxy/api/openApi/vcProxy/queryByorderIdList |
bodyOrQuery | 根据请求类型取值,见下表 |
bodyOrQuery 取值
| 请求类型 | bodyOrQuery |
|---|---|
| GET | 原始 query string;无参数时为空字符串 |
| POST/PUT JSON | 实际发送的请求体字符串 |
| 无请求体 | 空字符串 |
GET 参数顺序及 URL 编码必须与实际请求一致。JSON 请求体建议使用单行格式,签名后不要再格式化或重新序列化。
签名示例
以下示例调用订单查询接口:
SK = sk_xxxxxxxxxxxx
X-Timestamp = 1747555200
path = /owlproxy/api/openApi/vcProxy/queryByorderIdList
bodyOrQuery = {"orderIds":["ORDER-00001"]}
待签名字符串:
sk_xxxxxxxxxxxx1747555200/owlproxy/api/openApi/vcProxy/queryByorderIdList{"orderIds":["ORDER-00001"]}
对该字符串的 UTF-8 字节计算 SHA-256:
X-Sign = 39c4d29bc564bdfdbffc8fba89bd088d79be40b29eb3a9f5050d007f416ab05e
调用示例
curl
timestamp=$(date +%s)
accessKey="ak_xxxxxxxxxxxx"
secretKey="sk_xxxxxxxxxxxx"
path="/owlproxy/api/openApi/vcProxy/queryByorderIdList"
body='{"orderIds":["ORDER-00001"]}'
sign=$(printf "%s" "${secretKey}${timestamp}${path}${body}" | openssl dgst -sha256 -hex | awk '{print $2}')
curl -X POST "https://api.owlproxy.com${path}" \
-H "X-Access-Key: ${accessKey}" \
-H "X-Timestamp: ${timestamp}" \
-H "X-Sign: ${sign}" \
-H "Content-Type: application/json" \
--data-binary "${body}"
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class OwlProxyOpenApiExample {
private static final String BASE_URL = "https://api.owlproxy.com";
private static final String ACCESS_KEY = "ak_xxxxxxxxxxxx";
private static final String SECRET_KEY = "sk_xxxxxxxxxxxx";
private static String sha256Hex(String value) throws Exception {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(digest.length * 2);
for (byte item : digest) {
hex.append(String.format("%02x", item & 0xff));
}
return hex.toString();
}
public static void main(String[] args) throws Exception {
String path = "/owlproxy/api/openApi/vcProxy/queryByorderIdList";
String body = "{\"orderIds\":[\"ORDER-00001\"]}";
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
String sign = sha256Hex(SECRET_KEY + timestamp + path + body);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.header("X-Access-Key", ACCESS_KEY)
.header("X-Timestamp", timestamp)
.header("X-Sign", sign)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println(response.body());
}
}
Python
import hashlib
import time
import requests
baseUrl = "https://api.owlproxy.com"
accessKey = "ak_xxxxxxxxxxxx"
secretKey = "sk_xxxxxxxxxxxx"
path = "/owlproxy/api/openApi/vcProxy/queryByorderIdList"
body = '{"orderIds":["ORDER-00001"]}'
timestamp = str(int(time.time()))
sign = hashlib.sha256(
(secretKey + timestamp + path + body).encode("utf-8")
).hexdigest()
response = requests.post(
baseUrl + path,
headers={
"X-Access-Key": accessKey,
"X-Timestamp": timestamp,
"X-Sign": sign,
"Content-Type": "application/json",
},
data=body.encode("utf-8"),
)
print(response.text)
Node.js
const crypto = require("crypto");
const baseUrl = "https://api.owlproxy.com";
const accessKey = "ak_xxxxxxxxxxxx";
const secretKey = "sk_xxxxxxxxxxxx";
const path = "/owlproxy/api/openApi/vcProxy/queryByorderIdList";
const body = '{"orderIds":["ORDER-00001"]}';
const timestamp = Math.floor(Date.now() / 1000).toString();
const sign = crypto
.createHash("sha256")
.update(secretKey + timestamp + path + body, "utf8")
.digest("hex");
async function main() {
const response = await fetch(baseUrl + path, {
method: "POST",
headers: {
"X-Access-Key": accessKey,
"X-Timestamp": timestamp,
"X-Sign": sign,
"Content-Type": "application/json",
},
body,
});
console.log(await response.text());
}
main().catch(console.error);
GET 请求示例
请求:
GET /owlproxy/api/openApi/vcProxyShop/getProxyTypeList?proxyMode=1
签名字段:
path = /owlproxy/api/openApi/vcProxyShop/getProxyTypeList
bodyOrQuery = proxyMode=1
常见错误码
| code | 说明 | 处理建议 |
|---|---|---|
2019 | 验证签名失败 | 检查路径、参数顺序和请求体 |
2031 | 无效的密钥 | 检查 Access Key ID |
2032 | 请求头缺少鉴权信息 | 补充三个 V2 请求头 |
2033 | 时间戳过期或格式错误 | 使用当前 Unix 秒级时间戳 |
常见问题
POST 请求体为空时如何签名?
bodyOrQuery 使用空字符串,待签名字符串以 path 结尾。
X-Sign 区分大小写吗?
建议统一输出 64 位小写十六进制字符串。
为什么相同参数仍然验签失败?
请依次检查完整请求路径、时间戳单位、GET 参数顺序、URL 编码及实际发送的 JSON 字符串。
数据加解密示例
Java AES GCM 解密
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
public class AESUtils {
private static final String AES = "AES";
private static final String AES_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 16;
private static final int GCM_IV_LENGTH = 12;
private static SecretKeySpec getKeyFromPassword(String password) throws NoSuchAlgorithmException {
MessageDigest sha = MessageDigest.getInstance("SHA-256");
byte[] key = sha.digest(password.getBytes());
return new SecretKeySpec(key, AES);
}
public static byte[] generateIv() {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
return iv;
}
public static String encrypt(String input, String key) {
try {
SecretKeySpec secretKeySpec = getKeyFromPassword(key);
byte[] iv = generateIv();
Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
byte[] cipherText = cipher.doFinal(input.getBytes());
String ivString = Base64.getEncoder().encodeToString(iv);
String cipherTextString = Base64.getEncoder().encodeToString(cipherText);
return ivString + ":" + cipherTextString;
} catch (Exception e) {
return null;
}
}
public static String decrypt(String encryptedData, String key) {
try {
SecretKeySpec secretKeySpec = getKeyFromPassword(key);
String[] parts = encryptedData.split(":");
byte[] iv = Base64.getDecoder().decode(parts[0]);
byte[] cipherText = Base64.getDecoder().decode(parts[1]);
Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
return new String(cipher.doFinal(cipherText));
} catch (Exception e) {
return null;
}
}
}