API 调用指南
OpenAPI 提供标准的 HTTP 接口,用于管理云手机实例、任务及相关资源。
快速开始
请在 控制台 -> 开发者 -> Open API 页面获取 API 凭证(Access Key ID / Secret Access Key),并在所有 API 请求中通过 X-Access-Key 请求头传递获取到的 Access Key ID。
API 规则
请求头
每个 API 请求都必须在 HTTP Header 中包含以下四个字段:
| Header 字段 | 必填 | 说明 |
|---|---|---|
| X-Access-Key | 是 | API 访问密钥 ID,用于标识调用方 |
| X-Timestamp | 是 | 毫秒级 Unix 时间戳(13 位),用于防止重放攻击 |
| X-Nonce | 是 | 随机字符串,10~32 位小写字母和数字,每次请求唯一 |
| X-Signature | 是 | 签名值,Base64 编码的 HMAC-SHA256 结果 |
WARNING
以上四个 Header 字段均为必填。缺失或无效的字段将导致 401 Unauthorized 响应。
回调
所有支持回调的 API 都会将数据发送到用户提供的回调 URL。
回调 URL 可在 控制台 -> 开发者 -> Open API 中修改。
回调消息携带签名,接收方应按 回调签名算法 验证签名,以确保消息来源可靠且未被篡改。
签名算法
签名采用 HMAC-SHA256 算法,计算过程如下:
步骤 1:构造签名字符串(SignString)
按固定顺序拼接以下四个部分,中间不加任何分隔符:
SignString = uriPath + timestamp + nonce + accessKey步骤 2:转换为小写
SignString = SignString.toLowerCase()步骤 3:HMAC-SHA256 计算
以 secretKey 为密钥,对转换为小写后的签名字符串进行 HMAC-SHA256 运算,得到字节数组。
步骤 4:Base64 编码
将 HMAC-SHA256 计算得到的字节数组进行 Base64 编码,得到最终的签名值。
签名公式
X-Signature = Base64( HMAC-SHA256( SignString.toLowerCase(), secretKey ) )参数说明
| 参数 | 类型 | 取值范围 | 说明 |
|---|---|---|---|
| uriPath | String | 例如 /api/v1/orders | 请求路径,不包含域名和查询参数 |
| timestamp | String | 13 位整数 | 毫秒级 Unix 时间戳 |
| nonce | String | 10~32 个字符 | 小写字母 a-z 和数字 0-9 的随机组合 |
| accessKey | String | - | API Access Key ID,与 X-Access-Key 一致 |
| secretKey | String | 机密 | API Secret Access Key,仅用于签名计算,绝不传输 |
签名示例
示例参数:
| 参数 | 示例值 |
|---|---|
| uriPath | /api/v1/orders |
| timestamp | 1719800000000 |
| nonce | a3f7b2c9d1e8 |
| accessKey | ak-7x9k2m4p |
| secretKey | sk-5n8q3r6t1w9y2u4i7o0p |
计算过程:
步骤 1:构造签名字符串
SignString = "/api/v1/orders" + "1719800000000" + "a3f7b2c9d1e8" + "ak-7x9k2m4p"
= "/api/v1/orders1719800000000a3f7b2c9d1e8ak-7x9k2m4p"
步骤 2:转换为小写
SignString = "/api/v1/orders1719800000000a3f7b2c9d1e8ak-7x9k2m4p".toLowerCase()
步骤 3 & 4:HMAC-SHA256 + Base64
signature = Base64( HMAC-SHA256(SignString, secretKey) )最终请求示例:
POST /api/v1/orders HTTP/1.1
Content-Type: application/json
X-Access-Key: ak-7x9k2m4p
X-Timestamp: 1719800000000
X-Nonce: a3f7b2c9d1e8
X-Signature: xZ9vK2mP3nQ8wR5tY7uI1oP4aS6dF3gH9jK2lM5nB8cX7vN1mQ4wE6rT9yU2iO3pL=
{"product_id": 123, "quantity": 2}回调签名算法
回调消息同样采用 HMAC-SHA256 签名。与请求签名的区别在于:签名字符串的四个字段之间以单个换行符 \n(ASCII 码 0x0A)分隔,拼接完成后需将整个字符串统一转换为全小写形式。
步骤 1:构造签名字符串(SignString)
按固定顺序拼接以下四个字段,字段之间以单个换行符 \n 分隔:
SignString = uriPath + "\n" + timestamp + "\n" + nonce + "\n" + accessKey步骤 2:转换为小写
SignString = SignString.toLowerCase()步骤 3:HMAC-SHA256 计算
以双方约定的 secretKey(对应 Web 端的 Secret Access Key,绝不传输)作为 HMAC 密钥,对转换为小写后的签名字符串执行 HmacSHA256 运算,得到 32 字节的二进制摘要。
步骤 4:Base64 编码
对摘要字节数组执行 Base64 编码,得到的字符串即为签名值。
签名公式:
sign = Base64( HmacSHA256( SecretKey, toLowerCase(uriPath + "\n" + timestamp + "\n" + nonce + "\n" + accessKey) ) )TIP
回调签名与请求签名使用相同的参数和密钥,但拼接方式不同:请求签名四个部分直接拼接、中间不加任何分隔符;回调签名四个字段之间以单个换行符 \n 分隔。
代码示例
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Random;
public class ApiSignatureUtil {
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final String CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789";
/**
* 生成带签名的请求头
*/
public static SignatureResult sign(String uriPath, String accessKey, String secretKey) {
// 1. 生成 13 位毫秒级时间戳
String timestamp = String.valueOf(System.currentTimeMillis());
// 2. 生成 10~32 位随机 nonce
String nonce = generateNonce(10, 32);
// 3. 构造签名字符串并转换为小写
String signString = uriPath + timestamp + nonce + accessKey;
signString = signString.toLowerCase();
// 4. HMAC-SHA256 + Base64 编码
String signature = hmacSha256Base64(signString, secretKey);
return new SignatureResult(accessKey, timestamp, nonce, signature);
}
private static String hmacSha256Base64(String data, String key) {
try {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(
key.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec);
byte[] rawBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(rawBytes);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("签名计算失败", e);
}
}
private static String generateNonce(int minLen, int maxLen) {
String chars = CHARSET;
Random random = new Random();
int len = random.nextInt(maxLen - minLen + 1) + minLen;
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
public static class SignatureResult {
public final String accessKey;
public final String timestamp;
public final String nonce;
public final String signature;
public SignatureResult(String accessKey, String timestamp,
String nonce, String signature) {
this.accessKey = accessKey;
this.timestamp = timestamp;
this.nonce = nonce;
this.signature = signature;
}
}
}使用方式:
String uriPath = "/api/v1/orders";
String accessKey = "ak-7x9k2m4p";
String secretKey = "sk-5n8q3r6t1w9y2u4i7o0p";
ApiSignatureUtil.SignatureResult result =
ApiSignatureUtil.sign(uriPath, accessKey, secretKey);
// 设置请求头
headers.put("X-Access-Key", result.accessKey);
headers.put("X-Timestamp", result.timestamp);
headers.put("X-Nonce", result.nonce);
headers.put("X-Signature", result.signature);Python
import hmac
import hashlib
import base64
import time
import random
import string
def generate_nonce(min_len=10, max_len=32):
"""生成随机 nonce"""
chars = string.ascii_lowercase + string.digits
length = random.randint(min_len, max_len)
return ''.join(random.choices(chars, k=length))
def sign_request(uri_path, access_key, secret_key):
"""生成签名和请求头参数"""
timestamp = str(int(time.time() * 1000))
nonce = generate_nonce()
# 构造签名字符串并转换为小写
sign_string = (uri_path + timestamp + nonce + access_key).lower()
# HMAC-SHA256 + Base64
raw_signature = hmac.new(
secret_key.encode('utf-8'),
sign_string.encode('utf-8'),
hashlib.sha256
).digest()
signature = base64.b64encode(raw_signature).decode('utf-8')
return {
'X-Access-Key': access_key,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature
}最佳实践
安全
DANGER
secretKey 是签名计算的核心密钥,请像对待密码一样妥善保管,切勿:
- 在前端代码(浏览器/App)中硬编码 secretKey
- 将 secretKey 提交到公开代码仓库(如 GitHub、GitLab 公开项目)
- 在日志或错误信息中打印 secretKey
- 通过 HTTP 明文传输 secretKey
建议使用环境变量或密钥管理系统(如 Vault、KMS)存储 secretKey,避免在代码中以明文形式出现。
Nonce 唯一性
X-Nonce 在其有效期内(通常与时间戳窗口对齐,建议 5 分钟)必须唯一。服务端会维护已使用的 nonce 列表进行去重,重复使用的 nonce 将被拒绝。
时间同步
服务端允许的时间偏差窗口建议设置为 ±5 分钟。请确保调用方服务器保持 NTP 时间同步,避免因时间漂移导致签名失败。
URI 路径规范
构造签名时,uriPath 必须遵循以下规则:
- 仅包含路径部分,不包含域名、端口和查询参数
- 保留原始大小写,然后将整个拼接后的字符串转换为小写
- 以
/开头,不以/结尾(除非 API 本身如此定义) - 不包含 URL 编码,使用原始路径字符
| 场景 | 正确 | 错误 |
|---|---|---|
| 完整 URL | /api/v1/users | /api/v1/users |
| 带查询参数 | /api/v1/search | /api/v1/search?q=test |
| 大小写混合路径 | /API/V1/Users → 拼接后转小写 | 直接以原始大小写拼接 |
