概述
V2 是 VMOSCloud OpenAPI 的简化签名方案,与原 V4(HMAC-SHA256 派生密钥)方案并存,AK / SK 共用。
已经接入 V4 的客户无需迁移,老 SDK 长期保留。新对接客户建议直接使用 V2:3 个 Header + 1 行 SHA-256 即可完成签名,不需要派生密钥、不需要重排 JSON、不需要 UTC 时间格式化。
获取账号 (AK/SK)
前置准备
在开始使用 API 前,需要获取 Access Key ID 和 Secret Access Key,用于 API 请求鉴权。
操作步骤:
- 登录 VMOSCloud 平台
- 导航至菜单:开发者 -> API
- 复制对应的密钥信息:
AccessKeyID(即 AK)SecretAccessKey(即 SK)
请求 Header
每次请求 Headers 中必须包含以下 3 个鉴权参数:
| 参数名 | 类型 | 必填 | 参数描述 | 示例值 |
|---|---|---|---|---|
X-Access-Key | string | 必须 | Access Key ID 明文 | ak_xxxxxxxxxxxx |
X-Timestamp | string | 必须 | unix 秒级时间戳(10 位整数字符串),±5 分钟窗 | 1747555200 |
X-Sign | string | 必须 | 64 位小写 hex 签名 | 6069dfb4cb3ac57b… |
Content-Type | string | POST必须 | 资源 MIME 类型 | application/json |
签名算法
直接字符串拼接,无分隔符:
signString = SK + X-Timestamp + path + bodyOrQuery
X-Sign = lowerHex( SHA-256( signString_UTF8 ) )
| 字段 | 取值规则 |
|---|---|
SK | Secret Access Key 明文 |
X-Timestamp | 与请求头一致的 10 位 unix 秒(毫秒级会被判过期) |
path | 含 servlet 前缀的完整路径,例如 /vcpcloud/api/padApi/padInfo |
bodyOrQuery | 见下表 |
bodyOrQuery 取值规则
| 请求类型 | bodyOrQuery |
|---|---|
GET | 原始 query string,例如 startDate=2026-05-01&endDate=2026-05-31;无参则空串 |
POST / PUT(JSON) | 原始 body(发什么签什么,不要重排、不要去空格) |
文件上传接口 /uploadFile / /asyncCmd / /syncCmd | 空串(文件 body 不参与签名) |
query string 的参数顺序与 URL 编码必须与实际发送的完全一致,签名前不要重排、不要二次转码。
签名示例
输入:
SK = 9cucpjoyn4xxmkhj3q9el3ce
ts = 1747555200
path = /vcpcloud/api/padApi/padInfo
body = {"padCode":"AC32010601132"}
拼接后的 signString:
9cucpjoyn4xxmkhj3q9el3ce1747555200/vcpcloud/api/padApi/padInfo{"padCode":"AC32010601132"}
X-Sign = lowerHex( SHA-256( signString ) ),可以用任意语言的标准 SHA-256 库验证客户端实现是否正确。
错误码
| code | 含义 |
|---|---|
2019 | 验证签名失败 |
2031 | 无效的密钥(AccessKey 找不到) |
2032 | 请求头缺必要参数 |
2033 | 时间戳过期或格式错误 |
调用示例
curl
TS=$(date +%s)
AK="ak_xxxxxxxxxxxx"
SK="sk_xxxxxxxxxxxx"
URI="/vcpcloud/api/padApi/padInfo"
BODY='{"padCode":"AC32010601132"}'
SIGN=$(printf "%s" "${SK}${TS}${URI}${BODY}" | openssl dgst -sha256 -hex | awk '{print $2}')
curl -X POST "https://api.vmoscloud.com${URI}" \
-H "X-Access-Key: ${AK}" \
-H "X-Timestamp: ${TS}" \
-H "X-Sign: ${SIGN}" \
-H "Content-Type: application/json" \
--data-binary "${BODY}"
java
import com.alibaba.fastjson.JSONObject;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
public class V2ApiRequestUtils {
private static final String BASE_URL = "https://api.vmoscloud.com";
private static final String ACCESS_KEY = "Access Key ID";
private static final String SECRET_ACCESS_KEY = "Secret Access Key";
public static String sendPostRequest(String path, JSONObject params) {
try {
String body = params == null ? "" : params.toJSONString();
String ts = String.valueOf(System.currentTimeMillis() / 1000);
HttpPost req = new HttpPost(BASE_URL + path);
req.setHeader("X-Access-Key", ACCESS_KEY);
req.setHeader("X-Timestamp", ts);
req.setHeader("X-Sign", sign(ts, path, body));
req.setHeader("Content-Type", "application/json");
req.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
try (CloseableHttpClient client = HttpClients.createDefault();
CloseableHttpResponse resp = client.execute(req)) {
return EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
}
} catch (Exception e) {
throw new RuntimeException("V2 POST failed: " + path, e);
}
}
public static String sendGetRequest(String path, JSONObject params) {
try {
String query = buildQuery(params);
String ts = String.valueOf(System.currentTimeMillis() / 1000);
HttpGet req = new HttpGet(BASE_URL + path + (query.isEmpty() ? "" : "?" + query));
req.setHeader("X-Access-Key", ACCESS_KEY);
req.setHeader("X-Timestamp", ts);
req.setHeader("X-Sign", sign(ts, path, query));
try (CloseableHttpClient client = HttpClients.createDefault();
CloseableHttpResponse resp = client.execute(req)) {
return EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
}
} catch (Exception e) {
throw new RuntimeException("V2 GET failed: " + path, e);
}
}
private static String sign(String ts, String path, String body) throws Exception {
String signString = SECRET_ACCESS_KEY + ts + path + body;
byte[] raw = MessageDigest.getInstance("SHA-256")
.digest(signString.getBytes(StandardCharsets.UTF_8));
return String.format("%064x", new BigInteger(1, raw));
}
private static String buildQuery(JSONObject params) {
if (params == null || params.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> e : params.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(e.getKey()).append('=').append(e.getValue());
}
return sb.toString();
}
public static void main(String[] args) {
// POST 示例
JSONObject body = new JSONObject();
body.put("padCode", "AC32010601132");
System.out.println(sendPostRequest("/vcpcloud/api/padApi/padInfo", body));
// GET 示例
JSONObject query = new JSONObject();
query.put("startDate", "2026-05-01");
query.put("endDate", "2026-05-31");
System.out.println(sendGetRequest("/vcpcloud/api/padApi/getOrderEquipmentList", query));
}
}
python
import hashlib
import time
import json
import requests
BASE_URL = "https://api.vmoscloud.com"
ACCESS_KEY = "Access Key ID"
SECRET_ACCESS_KEY = "Secret Access Key"
def _sign(ts: str, path: str, body_or_query: str) -> str:
return hashlib.sha256(
(SECRET_ACCESS_KEY + ts + path + body_or_query).encode("utf-8")
).hexdigest()
def send_post(path: str, params: dict | None = None) -> str:
body = json.dumps(params, separators=(",", ":"), ensure_ascii=False) if params else ""
ts = str(int(time.time()))
headers = {
"X-Access-Key": ACCESS_KEY,
"X-Timestamp": ts,
"X-Sign": _sign(ts, path, body),
"Content-Type": "application/json",
}
# 用 data=bytes 避免 requests 改写 body
resp = requests.post(BASE_URL + path, headers=headers, data=body.encode("utf-8"))
return resp.text
def send_get(path: str, params: dict | None = None) -> str:
query = "&".join(f"{k}={v}" for k, v in params.items()) if params else ""
ts = str(int(time.time()))
url = BASE_URL + path + (f"?{query}" if query else "")
headers = {
"X-Access-Key": ACCESS_KEY,
"X-Timestamp": ts,
"X-Sign": _sign(ts, path, query),
}
return requests.get(url, headers=headers).text
if __name__ == "__main__":
# POST
print(send_post("/vcpcloud/api/padApi/padInfo", {"padCode": "AC32010601132"}))
# GET
print(send_get("/vcpcloud/api/padApi/getOrderEquipmentList",
{"startDate": "2026-05-01", "endDate": "2026-05-31"}))
node
const crypto = require("crypto");
const BASE_URL = "https://api.vmoscloud.com";
const ACCESS_KEY = "Access Key ID";
const SECRET_ACCESS_KEY = "Secret Access Key";
function sign(ts, path, bodyOrQuery) {
return crypto
.createHash("sha256")
.update(SECRET_ACCESS_KEY + ts + path + bodyOrQuery, "utf8")
.digest("hex");
}
async function sendPost(path, params) {
const body = params ? JSON.stringify(params) : "";
const ts = Math.floor(Date.now() / 1000).toString();
const resp = await fetch(BASE_URL + path, {
method: "POST",
headers: {
"X-Access-Key": ACCESS_KEY,
"X-Timestamp": ts,
"X-Sign": sign(ts, path, body),
"Content-Type": "application/json",
},
body,
});
return resp.text();
}
async function sendGet(path, params) {
const query = params
? Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&")
: "";
const ts = Math.floor(Date.now() / 1000).toString();
const resp = await fetch(BASE_URL + path + (query ? `?${query}` : ""), {
headers: {
"X-Access-Key": ACCESS_KEY,
"X-Timestamp": ts,
"X-Sign": sign(ts, path, query),
},
});
return resp.text();
}
(async () => {
// POST
console.log(await sendPost("/vcpcloud/api/padApi/padInfo",
{ padCode: "AC32010601132" }));
// GET
console.log(await sendGet("/vcpcloud/api/padApi/getOrderEquipmentList",
{ startDate: "2026-05-01", endDate: "2026-05-31" }));
})();
go
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
const (
baseURL = "https://api.vmoscloud.com"
accessKey = "Access Key ID"
secretAccessKey = "Secret Access Key"
)
func sign(ts, path, bodyOrQuery string) string {
sum := sha256.Sum256([]byte(secretAccessKey + ts + path + bodyOrQuery))
return hex.EncodeToString(sum[:])
}
func sendPost(path string, params map[string]interface{}) (string, error) {
body := ""
if params != nil {
b, _ := json.Marshal(params)
body = string(b)
}
ts := strconv.FormatInt(time.Now().Unix(), 10)
req, _ := http.NewRequest("POST", baseURL+path, bytes.NewReader([]byte(body)))
req.Header.Set("X-Access-Key", accessKey)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Sign", sign(ts, path, body))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
bs, _ := io.ReadAll(resp.Body)
return string(bs), nil
}
func sendGet(path string, params map[string]string) (string, error) {
parts := make([]string, 0, len(params))
for k, v := range params {
parts = append(parts, k+"="+v)
}
query := strings.Join(parts, "&")
ts := strconv.FormatInt(time.Now().Unix(), 10)
url := baseURL + path
if query != "" {
url += "?" + query
}
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("X-Access-Key", accessKey)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Sign", sign(ts, path, query))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
bs, _ := io.ReadAll(resp.Body)
return string(bs), nil
}
func main() {
// POST
out, _ := sendPost("/vcpcloud/api/padApi/padInfo",
map[string]interface{}{"padCode": "AC32010601132"})
fmt.Println(out)
// GET
out, _ = sendGet("/vcpcloud/api/padApi/getOrderEquipmentList",
map[string]string{"startDate": "2026-05-01", "endDate": "2026-05-31"})
fmt.Println(out)
}
php
<?php
const BASE_URL = "https://api.vmoscloud.com";
const ACCESS_KEY = "Access Key ID";
const SECRET_ACCESS_KEY = "Secret Access Key";
function v2_sign(string $ts, string $path, string $bodyOrQuery): string {
return hash("sha256", SECRET_ACCESS_KEY . $ts . $path . $bodyOrQuery);
}
function send_post(string $path, ?array $params = null): string {
$body = $params === null ? "" : json_encode($params, JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$ch = curl_init(BASE_URL . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
"X-Access-Key: " . ACCESS_KEY,
"X-Timestamp: " . $ts,
"X-Sign: " . v2_sign($ts, $path, $body),
"Content-Type: application/json",
],
]);
$resp = curl_exec($ch);
curl_close($ch);
return $resp;
}
function send_get(string $path, ?array $params = null): string {
$query = $params ? http_build_query($params) : "";
$ts = (string) time();
$url = BASE_URL . $path . ($query ? "?$query" : "");
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Access-Key: " . ACCESS_KEY,
"X-Timestamp: " . $ts,
"X-Sign: " . v2_sign($ts, $path, $query),
],
]);
$resp = curl_exec($ch);
curl_close($ch);
return $resp;
}
// POST
echo send_post("/vcpcloud/api/padApi/padInfo", ["padCode" => "AC32010601132"]) . PHP_EOL;
// GET
echo send_get("/vcpcloud/api/padApi/getOrderEquipmentList",
["startDate" => "2026-05-01", "endDate" => "2026-05-31"]) . PHP_EOL;
.net
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
public static class V2ApiRequestUtils
{
private const string BaseUrl = "https://api.vmoscloud.com";
private const string AccessKey = "Access Key ID";
private const string SecretAccessKey = "Secret Access Key";
private static readonly HttpClient Http = new HttpClient();
private static string Sign(string ts, string path, string bodyOrQuery)
{
using var sha = SHA256.Create();
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(SecretAccessKey + ts + path + bodyOrQuery));
var sb = new StringBuilder(64);
foreach (var b in hash) sb.Append(b.ToString("x2"));
return sb.ToString();
}
public static async Task<string> SendPostAsync(string path, object? body)
{
string bodyJson = body == null ? "" : JsonSerializer.Serialize(body);
string ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var req = new HttpRequestMessage(HttpMethod.Post, BaseUrl + path)
{
Content = new StringContent(bodyJson, Encoding.UTF8, "application/json")
};
req.Headers.TryAddWithoutValidation("X-Access-Key", AccessKey);
req.Headers.TryAddWithoutValidation("X-Timestamp", ts);
req.Headers.TryAddWithoutValidation("X-Sign", Sign(ts, path, bodyJson));
var resp = await Http.SendAsync(req);
return await resp.Content.ReadAsStringAsync();
}
public static async Task<string> SendGetAsync(string path, Dictionary<string, string>? query)
{
string queryStr = query == null ? ""
: string.Join("&", query.Select(kv => $"{kv.Key}={kv.Value}"));
string ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var url = BaseUrl + path + (queryStr.Length > 0 ? "?" + queryStr : "");
var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.TryAddWithoutValidation("X-Access-Key", AccessKey);
req.Headers.TryAddWithoutValidation("X-Timestamp", ts);
req.Headers.TryAddWithoutValidation("X-Sign", Sign(ts, path, queryStr));
var resp = await Http.SendAsync(req);
return await resp.Content.ReadAsStringAsync();
}
public static async Task Main()
{
// POST
Console.WriteLine(await SendPostAsync("/vcpcloud/api/padApi/padInfo",
new { padCode = "AC32010601132" }));
// GET
Console.WriteLine(await SendGetAsync("/vcpcloud/api/padApi/getOrderEquipmentList",
new Dictionary<string, string> { ["startDate"] = "2026-05-01", ["endDate"] = "2026-05-31" }));
}
}
常见问题
Q1:老客户是否需要迁移到 V2? 不需要。V4 长期保留,老 SDK 继续用即可。详见 V4 签名机制。
Q2:同一对 AK/SK 是否两套方案都能用? 可以。AK/SK 共用,按请求选协议。
Q3:POST body 为空时 bodyOrQuery 填什么? 空字符串。signString = SK + ts + path,末尾不再拼东西。
Q4:multipart 文件上传接口怎么签?bodyOrQuery 留空串,文件 body 不参与签名。涉及接口:/uploadFile、/asyncCmd、/syncCmd。
Q5:时间戳能用毫秒级吗? 不行。必须 10 位 unix 秒。13 位毫秒会因偏离当前时间超 5 分钟直接返回 2033。
Q6:X-Sign 大小写敏感吗? 服务端忽略大小写比较,大小写 hex 都接受。建议客户端统一输出小写。
Q7:如何分流到 V2 / V4? 带 X-Sign 头走 V2;带 Authorization 头走 V4。两套同时启用,按 Header 自动识别。