概述
ArmCloud的服务端OpenAPI为开发者提供了一套RESTful API,用于管理和操作云手机服务资源。通过这些API,您可以实现云手机实例的创建、管理、监控等功能,从而更好地集成和扩展您的应用。
为了确保API的安全性,ArmCloud提供了两个版本的签名验证方案:
- V2.0版本 ⭐:简化的签名方案,推荐使用
- V1.0版本:传统的完整签名方案,详见接口签名v1.0
版本推荐
推荐使用V2.0版本,它提供了更简单的请求头设置和签名计算流程,同时保持高安全性。
获取账号(AK/SK)
在开始使用API前,需要获取Access Key ID和Secret Access Key,用于API请求鉴权。
操作步骤:
- 登录ARMCloud平台
- 进入 用户管理 -> 账户信息 菜单
- 其中appKey为AK,appSecret为SK

V2.0签名验证(推荐) ⭐
API域名
重要提示
请根据您的服务区域选择正确的API域名
| 区域 | 域名 | 适用范围 |
|---|---|---|
| 国内 | 中国大陆地区用户 | |
| 海外 | 海外及港澳台地区用户 |
请求头设置
V2.0版本需要在HTTP请求中添加以下四个自定义请求头:
| 请求头名称 | 说明 | 示例 |
|---|---|---|
| 鉴权版本,固定值"2.0" | 2.0 | |
| 访问密钥ID,由ArmCloud平台分配 | LTAI4FzK8888888888888 | |
| 请求发起的时间戳(毫秒) | 1618900299000 | |
| 请求的签名结果 | 5d9f846a5254865894069d9a864a96696115f0edcac66de5f3a3515f58c44200 |
签名计算流程
1. 构建待签名字符串
核心公式
待签名字符串 = timestamp + path + body
其中body的内容根据请求方法不同:
- GET请求:body = 查询参数字符串(如果有查询参数)
- POST请求:body = JSON请求体字符串
详细说明
参数组成:
| 参数 | 说明 | 示例 |
|---|---|---|
timestamp | 13位毫秒时间戳,与x-timestamp请求头相同 | 1618900299000 |
path | 完整的API路径,必须以/开头 | /openapi/open/device/list |
body | 根据请求类型的不同内容(见下方说明) | 见具体示例 |
body内容规则:
GET请求:
- 如果有查询参数:
body = 查询参数字符串 - 如果无查询参数:
body = ""(空字符串)
- 如果有查询参数:
POST请求:
body = JSON请求体的紧凑字符串- JSON必须是紧凑格式,无空格和换行
构建示例
GET请求示例:
请求URL: https://api.xiaosuanyun.com/openapi/open/user/info?id=12345&type=basic
timestamp: 1618900299000
path: /openapi/open/user/info
body: id=12345&type=basic
待签名字符串: 1618900299000/openapi/open/user/infoid=12345&type=basic
POST请求示例:
请求URL: https://api.xiaosuanyun.com/openapi/open/device/list
请求体: {"page": 1, "rows": 10}
timestamp: 1618900400000
path: /openapi/open/device/list
body: {"page":1,"rows":10}
待签名字符串: 1618900400000/openapi/open/device/list{"page":1,"rows":10}
2. 计算签名
使用HMAC-SHA256算法,以SecretKey作为密钥,对第1步构建的待签名字符串进行签名计算:
重要
计算得到的签名值需以形式表示,长度为64个字符。
计算示例
待签名字符串: 1618900400000/openapi/open/device/list{"page":1,"rows":10}
SecretKey: your_secret_key
signature = hmac_sha256(待签名字符串, SecretKey).toLowerCase()
结果: 3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1
错误响应
当签名验证失败时,API网关会返回以下错误:
{
"code": 100005,
"msg": "验证签名失败",
"data": null
}
常见错误情况:
- AccessKey不存在或已禁用
- 请求时间戳与服务器时间相差超过5分钟
- 签名计算错误
- 请求头格式不正确
接口限流说明
为了确保平台服务的稳定性和可靠性,ArmCloud对所有OpenAPI接口实施了访问频率限制。
限流机制
限流目的
接口限流的主要目的是让客户使用的平台更加稳定、可靠,确保所有用户能够公平地享受服务资源。
限流策略:
限流计算方式
限流采用固定时间窗口计算,每个Access Key独立计算限制。同时满足QPS和每分钟两个维度的限制。
| 用户类型 | QPS限制(每秒) | RPM限制(每分) | 说明 |
|---|---|---|---|
| 测试用户 | 200次/秒 | 5,000次/分钟 | 免费用户默认限制 |
| 付费用户 | 2,000次/秒 | 30,000次/分钟 | 付费升级后限制 |
限流维度说明:
- QPS限制:在任意1秒钟时间窗口内的最大请求数
- 每分钟限制:在任意1分钟时间窗口内的最大请求数
- Access Key级别:每个Access Key独立计算,不同Key之间不共享额度
- 所有接口共享:单个Access Key下所有接口调用共享同一个限流额度
限流响应
当触发限流时,API会返回HTTP状态码429 Too Many Requests,响应内容如下:
{
"msg": "Too many requests. Please try again later..",
"code": 429,
"data": null
}
响应头说明:
| 响应头 | 说明 | 示例 |
|---|---|---|
X-RateLimit-Limit | 当前限流周期的请求上限,根据限流类型而定 | 200 |
X-RateLimit-Remaining | 当前限流周期剩余可用请求数 | 150 |
X-RateLimit-Reset | 当前限流周期重置时间戳(Unix时间戳) | 1618900361 |
X-RateLimit-Type | 触发的限流类型 | QPS/RPM |
最佳实践
建议
- 合理控制请求频率:建议在应用中实现请求频率控制,避免短时间内发送大量请求
- 实现重试机制:当收到429错误时,应等待适当时间后重试,建议使用指数退避算法
- 监控响应头:关注
X-RateLimit-*响应头,主动控制请求节奏 - 批量操作优化:尽量使用支持批量操作的接口,减少单次请求数量
重试策略示例:
import time
import random
def api_request_with_retry(api_func, max_retries=3):
"""带重试的API请求示例"""
for attempt in range(max_retries + 1):
try:
response = api_func()
if response.status_code == 429:
if attempt < max_retries:
# 指数退避 + 随机抖动
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
continue
else:
raise Exception("请求频率限制,已达到最大重试次数")
return response
except Exception as e:
if attempt == max_retries:
raise e
time.sleep(1)
注意事项
重要提醒
- 每个请求都必须重新计算签名,不可重复使用
- 签名计算需包含完整的请求路径,包括最前面的斜杠
- 所有字符串都必须使用UTF-8编码
客户端Demo(Java/Python/NodeJs)
重要说明
API域名选择
根据您的服务区域选择对应的域名:
- 国内用户:
- 海外用户:
接口路径说明
以下代码示例中使用的接口路径(如 /openapi/open/device/list、/openapi/open/pad/net/storage/res/create)仅为演示用途。
Java实现示例
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ArmCloudSignatureV2 {
private static final String ALGORITHM = "HmacSHA256";
private final String accessKeyId;
private final String secretKey;
private final ObjectMapper objectMapper = new ObjectMapper();
public ArmCloudSignatureV2(String accessKeyId, String secretKey) {
this.accessKeyId = accessKeyId;
this.secretKey = secretKey;
}
// GET请求签名
public Map<String, String> signGetRequest(String path, String queryString) throws Exception {
String timestamp = String.valueOf(Instant.now().toEpochMilli());
String body = queryString != null ? queryString : "";
String signature = calculateSignature(timestamp, path, body);
Map<String, String> headers = new HashMap<>();
headers.put("authver", "2.0");
headers.put("x-ak", accessKeyId);
headers.put("x-timestamp", timestamp);
headers.put("x-sign", signature);
return headers;
}
// POST请求签名
public Map<String, String> signPostRequest(String path, Object requestData) throws Exception {
String timestamp = String.valueOf(Instant.now().toEpochMilli());
String requestBody = objectMapper.writeValueAsString(requestData);
String signature = calculateSignature(timestamp, path, requestBody);
Map<String, String> headers = new HashMap<>();
headers.put("authver", "2.0");
headers.put("x-ak", accessKeyId);
headers.put("x-timestamp", timestamp);
headers.put("x-sign", signature);
headers.put("Content-Type", "application/json");
return headers;
}
private String calculateSignature(String timestamp, String path, String body) throws Exception {
String stringToSign = timestamp + path + (body != null ? body : "");
Mac hmacSha256 = Mac.getInstance(ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM);
hmacSha256.init(secretKeySpec);
byte[] hash = hmacSha256.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
}
private String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
// 使用示例
public static void main(String[] args) throws Exception {
ArmCloudSignatureV2 signer = new ArmCloudSignatureV2("LTAI4FzK8888888888888", "your_secret_key");
// POST请求示例 - 板卡列表查询(接口路径请参考OpenAPI文档)
Map<String, Object> deviceListData = new HashMap<>();
deviceListData.put("page", 1);
deviceListData.put("rows", 10);
deviceListData.put("padCodes", Arrays.asList("AC21020010391"));
deviceListData.put("vmStatus", "1");
deviceListData.put("deviceStatus", "0");
// 注意:此处路径仅为示例,实际路径请查看OpenAPI文档
Map<String, String> deviceListHeaders = signer.signPostRequest("/openapi/open/device/list", deviceListData);
System.out.println("板卡列表查询请求头: " + deviceListHeaders);
// POST请求示例 - 创建云实例(接口路径和参数请参考OpenAPI文档)
Map<String, Object> createInstanceData = new HashMap<>();
createInstanceData.put("clusterCode", "001");
createInstanceData.put("specificationCode", "m2-3");
createInstanceData.put("imageId", "img-25021828327");
createInstanceData.put("screenLayoutCode", "realdevice_1440x3120x560");
createInstanceData.put("number", 2);
createInstanceData.put("dns", "8.8.8.8");
createInstanceData.put("storageSize", 16);
createInstanceData.put("realPhoneTemplateId", 36);
// 注意:此处路径仅为示例,实际路径请查看OpenAPI文档
Map<String, String> createHeaders = signer.signPostRequest("/openapi/open/pad/net/storage/res/create", createInstanceData);
System.out.println("创建云实例请求头: " + createHeaders);
}
}
Python实现示例
import hmac
import hashlib
import time
import json
import requests
from typing import Dict, Any, Optional
class ArmCloudSignatureV2:
def __init__(self, access_key_id: str, secret_key: str, base_url: str = "https://api.xiaosuanyun.com"):
self.access_key_id = access_key_id
self.secret_key = secret_key
self.base_url = base_url # 国内: https://api.xiaosuanyun.com 海外: https://openapi-hk.armcloud.net
def _calculate_signature(self, timestamp: str, path: str, body: str = "") -> str:
"""计算HMAC-SHA256签名"""
string_to_sign = timestamp + path + (body if body else "")
signature = hmac.new(
self.secret_key.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature
def sign_get_request(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, str]:
"""为GET请求生成签名头"""
timestamp = str(int(time.time() * 1000))
query_string = ""
if params:
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
body = query_string
else:
body = ""
signature = self._calculate_signature(timestamp, path, body)
return {
"authver": "2.0",
"x-ak": self.access_key_id,
"x-timestamp": timestamp,
"x-sign": signature
}
def sign_post_request(self, path: str, data: Dict[str, Any]) -> Dict[str, str]:
"""为POST请求生成签名头"""
timestamp = str(int(time.time() * 1000))
request_body = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
signature = self._calculate_signature(timestamp, path, request_body)
return {
"authver": "2.0",
"x-ak": self.access_key_id,
"x-timestamp": timestamp,
"x-sign": signature,
"Content-Type": "application/json"
}
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> requests.Response:
"""发起GET请求"""
headers = self.sign_get_request(path, params)
url = self.base_url + path
return requests.get(url, params=params, headers=headers)
def post(self, path: str, data: Dict[str, Any]) -> requests.Response:
"""发起POST请求"""
headers = self.sign_post_request(path, data)
url = self.base_url + path
request_body = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
return requests.post(url, data=request_body, headers=headers)
# 使用示例
if __name__ == "__main__":
# 初始化客户端 - 根据地区选择域名
# 国内用户
client = ArmCloudSignatureV2("LTAI4FzK8888888888888", "your_secret_key", "https://api.xiaosuanyun.com")
# 海外用户
# client = ArmCloudSignatureV2("LTAI4FzK8888888888888", "your_secret_key", "https://openapi-hk.armcloud.net")
# POST请求示例 - 板卡列表查询(接口路径请参考OpenAPI文档)
device_list_data = {
"page": 1,
"rows": 10,
"padCodes": ["AC21020010391"],
"vmStatus": "1",
"deviceStatus": "0"
}
try:
# 注意:此处路径仅为示例,实际路径请查看OpenAPI文档
response = client.post("/openapi/open/device/list", device_list_data)
print(f"板卡列表查询 Response: {response.status_code}")
print(f"Response Body: {response.text}")
except Exception as e:
print(f"板卡列表查询 Error: {e}")
# POST请求示例 - 创建云实例(接口路径和参数请参考OpenAPI文档)
create_instance_data = {
"clusterCode": "001",
"specificationCode": "m2-3",
"imageId": "img-25021828327",
"screenLayoutCode": "realdevice_1440x3120x560",
"number": 2,
"dns": "8.8.8.8",
"storageSize": 16,
"realPhoneTemplateId": 36
}
try:
# 注意:此处路径仅为示例,实际路径请查看OpenAPI文档
response = client.post("/openapi/open/pad/net/storage/res/create", create_instance_data)
print(f"创建云实例 Response: {response.status_code}")
print(f"Response Body: {response.text}")
except Exception as e:
print(f"创建云实例 Error: {e}")
Node.js实现示例
const crypto = require('crypto');
const axios = require('axios');
class ArmCloudSignatureV2 {
constructor(accessKeyId, secretKey, baseUrl = 'https://api.xiaosuanyun.com') {
this.accessKeyId = accessKeyId;
this.secretKey = secretKey;
this.baseUrl = baseUrl; // 国内: https://api.xiaosuanyun.com 海外: https://openapi-hk.armcloud.net
}
calculateSignature(timestamp, path, body = '') {
const stringToSign = timestamp + path + body;
return crypto
.createHmac('sha256', this.secretKey)
.update(stringToSign, 'utf8')
.digest('hex');
}
signGetRequest(path, params = {}) {
const timestamp = Date.now().toString();
const queryString = new URLSearchParams(params).toString();
const body = queryString ? queryString : '';
const signature = this.calculateSignature(timestamp, path, body);
return {
'authver': '2.0',
'x-ak': this.accessKeyId,
'x-timestamp': timestamp,
'x-sign': signature
};
}
signPostRequest(path, data) {
const timestamp = Date.now().toString();
const requestBody = JSON.stringify(data);
const signature = this.calculateSignature(timestamp, path, requestBody);
return {
'authver': '2.0',
'x-ak': this.accessKeyId,
'x-timestamp': timestamp,
'x-sign': signature,
'Content-Type': 'application/json'
};
}
async get(path, params = {}) {
const headers = this.signGetRequest(path, params);
return await axios.get(this.baseUrl + path, { headers, params });
}
async post(path, data) {
const headers = this.signPostRequest(path, data);
return await axios.post(this.baseUrl + path, data, { headers });
}
}
// 使用示例 - 根据地区选择域名
// 国内用户
const client = new ArmCloudSignatureV2('LTAI4FzK8888888888888', 'your_secret_key', 'https://api.xiaosuanyun.com');
// 海外用户
// const client = new ArmCloudSignatureV2('LTAI4FzK8888888888888', 'your_secret_key', 'https://openapi-hk.armcloud.net');
// POST请求 - 板卡列表查询(接口路径请参考OpenAPI文档)
const deviceListData = {
page: 1,
rows: 10,
padCodes: ['AC21020010391'],
vmStatus: '1',
deviceStatus: '0'
};
// 注意:此处路径仅为示例,实际路径请查看OpenAPI文档
client.post('/openapi/open/device/list', deviceListData)
.then(response => console.log('板卡列表查询 Response:', response.data))
.catch(error => console.error('板卡列表查询 Error:', error.message));
// POST请求 - 创建云实例(接口路径和参数请参考OpenAPI文档)
const createInstanceData = {
clusterCode: '001',
specificationCode: 'm2-3',
imageId: 'img-25021828327',
screenLayoutCode: 'realdevice_1440x3120x560',
number: 2,
dns: '8.8.8.8',
storageSize: 16,
realPhoneTemplateId: 36
};
// 注意:此处路径仅为示例,实际路径请查看OpenAPI文档
client.post('/openapi/open/pad/net/storage/res/create', createInstanceData)
.then(response => console.log('创建云实例 Response:', response.data))
.catch(error => console.error('创建云实例 Error:', error.message));