简介
ArmCloud 的服务端 OpenAPI 为开发者提供了一套 RESTful API,用于管理和操作云手机服务资源。通过这些 API,您可以实现云手机实例的创建、管理、监控等功能,从而更好地集成和扩展您的应用。
每次请求会对您的签名进行验证,确保您的云手机实例信息安全,请妥善保管好您账号的Access Key ID 和 Secret Access Key信息(后续简称AK/SK)。
开发必要参数说明
每次 API 请求的 Headers 中必须包含以下参数进行身份验证:
参数名 | 用途和获取说明 |
---|---|
x-date | 发送请求的时间戳,使用UTC时间,精确到秒如:20240301T093700Z |
x-host | 接口访问域名固定值openapi-hk.armcloud.net |
content-type | 资源的MIME类型, 以实际接口指定值为准,如:application/json |
authorization | 每次请求的授权签名信息,具体生成规则请参考authorization授权签名。 如:HMAC-SHA256 Credential={AK}/{xDate}/armcloud-paas/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature={signature} |
Authorization授权签名
Authorization授权签名信息生成,为方便客户接入,官方提供SDK-JAVA、SDK-PYTHON参考模板,内部已经实现加签验证,通过调用AuthorizationUtil.authorizationCreate传入(AK,SK,请求的时间戳,固定域名,请求报文格式,请求报文信息,请求类型)会自动计算生成请求authorization授权签名。
获取账号(AK/SK)
在开始生成授权签名前,需要获取 Access Key ID 和 Secret Access Key,用于 API请求鉴权。
操作步骤:在ARMCloud平台登录后,用户管理 -> 账户信息 菜单下,appKey为AK, appSecret为SK。

AuthorizationUtil签名生成类说明
SDK-JAVA Authorization.authorizationCreate签名生成方法解析。
类名:AuthorizationUtil
授权签名生成方法: AuthorizationUtil.authorizationCreate(String AK, String SK, String xDate, String xHost, String contentTypeJson, Map<String,Object> requestBody, String httpMethodStr)
参数名 | 类型 | 示例值 | 参数描述 |
---|---|---|---|
AK | String | xxxx | 账号的 Access Key ID |
SK | String | xxxx | 账号的 Secret Access Key |
xDate | String | 20240301T093700Z | 请求的时间戳,使用UTC时间,精确到秒 |
xHost | String | openapi-hk.armcloud.net | 固定域名 |
contentTypeJson | String | application/json | 请求报文格式 |
requestBody | Map<String,Object> | new Map<String,Object> | 请求报文信息 |
httpMethodStr | String | POST | 请求方式:GET或POST |
/**
* 签名工具类
*/
public static class AuthorizationUtil {
private static final String service = "armcloud-paas"; //固定服务名
private static final String algorithm = "HMAC-SHA256"; //固定签名算法
/**
* authorization生成工具方法
*
* @param AK 账号的 Access Key ID
* @param SK 账号的 Secret Access Key
* @param xDate 遵循 ISO 8601 标准时间的格式:YYYYMMDD'T'HHMMSS'Z' 例如:20201103T104027Z
* @param xHost 固定域名
* @param contentTypeJson 请求内容格式
* @param requestBody 请求json数据
* @param httpMethodStr 请求方式:GET或POST
* @return authorization
* @throws Exception
*/
public static String authorizationCreate(String AK, String SK, String xDate, String xHost, String contentTypeJson
, Map<String,Object> requestBody, String httpMethodStr) throws Exception {
byte[] body = new byte[0];
if (requestBody != null) {
if ("GET".equals(httpMethodStr)) {
StringBuilder sb = new StringBuilder();
Iterator<String> keys = requestBody.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
sb.append(key).append("=").append(requestBody.get(key)).append("&");
}
// 移除最后一个'&'字符(如果存在)
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
body = sb.toString().getBytes(StandardCharsets.UTF_8);
} else {
body = MapToJsonUtil.mapToJson(requestBody).getBytes(StandardCharsets.UTF_8);
}
}
final String xContentSha256 = hashSHA256(body);
//短时间格式
final String shortDate = xDate.substring(0, 8);
String signedHeaders = "content-type;host;x-content-sha256;x-date";
String canonicalStringBuilder =
"host:" + xHost + "\n" + "x-date:" + xDate + "\n" + "content-type:" + contentTypeJson + "\n"
+ "signedHeaders:" + signedHeaders + "\n" + "x-content-sha256:" + xContentSha256;
String hashCanonicalString = hashSHA256(canonicalStringBuilder.getBytes());
String credentialScope = shortDate + "/" + service + "/request";
String signString = algorithm + "\n" + xDate + "\n" + credentialScope + "\n" + hashCanonicalString;
byte[] signKey = genSigningSecretKeyV4(SK, shortDate, service);
String signature = bytesToHex(hmacSHA256(signKey, signString));
//authorization
String authorization = "HMAC-SHA256 Credential=" + AK + "/" + xDate + "/" + service
+ "/request, SignedHeaders=" + signedHeaders + ", Signature=" + signature;
return authorization;
}
private static byte[] genSigningSecretKeyV4(String secretKey, String shortXDate, String service) throws Exception {
byte[] kDate = hmacSHA256((secretKey).getBytes(), shortXDate);
byte[] kService = hmacSHA256(kDate, service);
return hmacSHA256(kService, "request");
}
private static String hashSHA256(byte[] content) throws Exception {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(content));
} catch (Exception e) {
throw new Exception("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}
private static String bytesToHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return "";
}
final StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
private static byte[] hmacSHA256(byte[] key, String content) throws Exception {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(content.getBytes());
} catch (Exception e) {
throw new Exception("Unable to calculate a request signature: " + e.getMessage(), e);
}
}
}
SDK-JAVA参考模板
package net.armcloud.console.sdk.test;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 签名测试类
*/
public class AuthorizationTest {
//GET签名请求测试--POST签名请求测试
public static void main(String[] args) throws Exception {
//替换成自己账号的AK
final String AK = "xxxxxx";
//替换成自己账号的SK
final String SK = "xxxx";
//固定域名
final String xHost = "openapi-hk.armcloud.net";
//请求报文格式
final String contentType = "application/json";
//查询支持的回调类型接口
selectList( AK, SK, xHost, contentType);
//实例分组列表接口
groupInfos( AK, SK, xHost, contentType);
}
/**
* 查询支持的回调类型接口
* @param AK
* @param SK
* @param xHost
* @param contentType
* @throws Exception
*/
private static void selectList ( String AK, String SK, String xHost, String contentType) throws Exception {
String postUrlAddress = "https://"+xHost+"/openapi/open/config/selectList";
Map<String,Object> getRequestBody = new HashMap<>();
get(postUrlAddress, AK, SK, xHost, contentType, getRequestBody);
}
/**
* 实例分组列表接口
* @param AK
* @param SK
* @param xHost
* @param contentType
* @throws Exception
*/
private static void groupInfos ( String AK, String SK, String xHost, String contentType) throws Exception {
String postUrlAddress = "https://"+xHost+"/openapi/open/group/infos";
List<Integer> jsonArray = new ArrayList<>();
jsonArray.add(1);
Map<String,Object> postRequestBody = new HashMap<>();
postRequestBody.put("padCode", "AC32010180376");
postRequestBody.put("groupIds", jsonArray);
post(postUrlAddress, AK, SK, xHost, contentType, postRequestBody);
}
/**
* GET封装请求方法
*
* @param urlAddress 请求地址
* @param AK AK
* @param SK SK
* @param xHost 固定域名
* @param contentType 请求报文格式
* @param requestBody 请求报文
* @throws Exception
*/
public static String get(String urlAddress, String AK, String SK, String xHost, String contentType
, Map<String,Object> requestBody) throws Exception {
LocalDateTime localDateTime = LocalDateTime.now(); // 当前时间
String xDate = localDateTimeToStringUTC(localDateTime); // UTC时间格式
URL url = new URL(urlAddress + getParamString(requestBody)); // 创建URL对象
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // 打开连接
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000); // 设置连接超时时间为5秒
connection.setReadTimeout(5000); // 设置读取超时时间为5秒
connection.setRequestProperty("content-type", contentType); // 设置请求头
connection.setRequestProperty("x-host", xHost);
connection.setRequestProperty("x-date", xDate);
//SDK AuthorizationUtil工具类authorizationCreate方法获取认证authorization接口
String authorization = AuthorizationUtil.authorizationCreate(AK, SK, xDate, xHost, contentType
, requestBody, "GET");
connection.setRequestProperty("authorization", authorization);
try {
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println("GET Response Body: " + response);
return String.valueOf(response);
} catch (IOException e) {
e.printStackTrace();
} finally {
connection.disconnect();
}
return null;
}
/**
* post封装请求方法
*
* @param urlAddress 请求地址
* @param AK AK
* @param SK SK
* @param xHost 固定域名
* @param contentType 请求报文格式
* @param requestBody 请求报文
* @throws Exception
*/
public static String post(String urlAddress, String AK, String SK, String xHost, String contentType
, Map<String,Object> requestBody) throws Exception {
LocalDateTime localDateTime = LocalDateTime.now(); //当前时间
String xDate = localDateTimeToStringUTC(localDateTime); //UTC时间格式
URL url = new URL(urlAddress); // 创建URL对象
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // 打开连接
connection.setRequestMethod("POST");
connection.setConnectTimeout(5000); // 设置连接超时时间为5秒
connection.setReadTimeout(5000); // 设置读取超时时间为5秒
connection.setRequestProperty("content-type", contentType); // 设置请求头
connection.setRequestProperty("x-host", xHost);
connection.setRequestProperty("x-date", xDate);
connection.setDoOutput(true); // 允许写入请求体
String authorization = AuthorizationUtil.authorizationCreate(AK, SK, xDate, xHost, contentType
, requestBody, "POST");
connection.setRequestProperty("authorization", authorization);
// 将 JSON 数据转换为字节数组
byte[] outputInBytes = MapToJsonUtil.mapToJson(requestBody).getBytes(StandardCharsets.UTF_8);
try {
// 获取输出流并写入数据
OutputStream os = connection.getOutputStream();
os.write(outputInBytes);
Scanner scanner = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8.name());
StringBuilder response = new StringBuilder();
while (scanner.hasNext()) {
response.append(scanner.next());
}
System.out.println("POST Response Body: " + response);
return String.valueOf(response);
} catch (Exception e) {
e.printStackTrace();
} finally {
connection.disconnect();
}
return null;
}
/**
* 时区转换
*
* @param localDateTime
* @return
*/
public static String localDateTimeToStringUTC(LocalDateTime localDateTime) {
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zoneId);
ZonedDateTime utcDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("UTC"));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
return utcDateTime.format(formatter);
}
/**
* get请求参数简单解析拼接
*
* @param map
* @return
*/
public static String getParamString(Map<String, Object> map) {
StringBuilder sb = new StringBuilder();
if (map != null) {
sb.append("?");
Iterator<String> keys = map.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
sb.append(key).append("=").append(map.get(key)).append("&");
}
// 移除最后一个'&'字符(如果存在)
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
}
return sb.toString();
}
/**
* map转换JsonUtil工具类
*/
public static class MapToJsonUtil {
public static String mapToJson(Map<String, Object> map) {
StringBuilder jsonString = new StringBuilder();
jsonString.append("{");
Set<Map.Entry<String, Object>> entrySet = map.entrySet();
boolean firstEntry = true;
for (Map.Entry<String, Object> entry : entrySet) {
if (!firstEntry) {
jsonString.append(",");
}
firstEntry = false;
jsonString.append("\"");
jsonString.append(entry.getKey());
jsonString.append("\":");
Object value = entry.getValue();
if (value instanceof String) {
jsonString.append("\"");
jsonString.append(value);
jsonString.append("\"");
} else if (value instanceof Number || value instanceof Boolean) {
jsonString.append(value);
} else if (value instanceof Map) {
jsonString.append(mapToJson((Map<String, Object>) value));
} else if (value instanceof Iterable<?>) {
jsonString.append(iterableToJson((Iterable<?>) value));
} else if (value == null) {
jsonString.append("null");
} else {
// Handle other types if necessary
jsonString.append("\"");
jsonString.append(value.toString());
jsonString.append("\"");
}
}
jsonString.append("}");
return jsonString.toString();
}
private static String iterableToJson(Iterable<?> iterable) {
StringBuilder jsonArray = new StringBuilder();
jsonArray.append("[");
boolean firstElement = true;
for (Object element : iterable) {
if (!firstElement) {
jsonArray.append(",");
}
firstElement = false;
if (element instanceof String) {
jsonArray.append("\"");
jsonArray.append(element);
jsonArray.append("\"");
} else if (element instanceof Number || element instanceof Boolean) {
jsonArray.append(element);
} else if (element instanceof Map) {
jsonArray.append(mapToJson((Map<String, Object>) element));
} else if (element instanceof Iterable<?>) {
jsonArray.append(iterableToJson((Iterable<?>) element));
} else if (element == null) {
jsonArray.append("null");
} else {
// Handle other types if necessary
jsonArray.append("\"");
jsonArray.append(element.toString());
jsonArray.append("\"");
}
}
jsonArray.append("]");
return jsonArray.toString();
}
}
/**
* 签名工具类
*/
public static class AuthorizationUtil {
private static final String service = "armcloud-paas"; //固定服务名
private static final String algorithm = "HMAC-SHA256"; //固定签名算法
/**
* authorization生成工具方法
*
* @param AK 账号的 Access Key ID
* @param SK 账号的 Secret Access Key
* @param xDate 遵循 ISO 8601 标准时间的格式:YYYYMMDD'T'HHMMSS'Z' 例如:20201103T104027Z
* @param xHost 固定域名
* @param contentTypeJson 请求内容格式
* @param requestBody 请求json数据
* @param httpMethodStr 请求方式:GET或POST
* @return authorization
* @throws Exception
*/
public static String authorizationCreate(String AK, String SK, String xDate, String xHost
, String contentTypeJson, Map<String,Object> requestBody, String httpMethodStr) throws Exception {
byte[] body = new byte[0];
if (requestBody != null) {
if ("GET".equals(httpMethodStr)) {
StringBuilder sb = new StringBuilder();
Iterator<String> keys = requestBody.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
sb.append(key).append("=").append(requestBody.get(key)).append("&");
}
// 移除最后一个'&'字符(如果存在)
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
body = sb.toString().getBytes(StandardCharsets.UTF_8);
} else {
body = MapToJsonUtil.mapToJson(requestBody).getBytes(StandardCharsets.UTF_8);
}
}
final String xContentSha256 = hashSHA256(body);
//短时间格式
final String shortDate = xDate.substring(0, 8);
String signedHeaders = "content-type;host;x-content-sha256;x-date";
String canonicalStringBuilder =
"host:" + xHost + "\n" + "x-date:" + xDate + "\n" + "content-type:" + contentTypeJson + "\n"
+ "signedHeaders:" + signedHeaders + "\n" + "x-content-sha256:" + xContentSha256;
String hashCanonicalString = hashSHA256(canonicalStringBuilder.getBytes());
String credentialScope = shortDate + "/" + service + "/request";
String signString = algorithm + "\n" + xDate + "\n" + credentialScope + "\n" + hashCanonicalString;
byte[] signKey = genSigningSecretKeyV4(SK, shortDate, service);
String signature = bytesToHex(hmacSHA256(signKey, signString));
//authorization
String authorization = "HMAC-SHA256 Credential=" + AK + "/" + xDate + "/" + service
+ "/request, SignedHeaders=" + signedHeaders + ", Signature=" + signature;
return authorization;
}
private static byte[] genSigningSecretKeyV4(String secretKey, String shortXDate
, String service) throws Exception {
byte[] kDate = hmacSHA256((secretKey).getBytes(), shortXDate);
byte[] kService = hmacSHA256(kDate, service);
return hmacSHA256(kService, "request");
}
private static String hashSHA256(byte[] content) throws Exception {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(content));
} catch (Exception e) {
throw new Exception("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}
private static String bytesToHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return "";
}
final StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
private static byte[] hmacSHA256(byte[] key, String content) throws Exception {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(content.getBytes());
} catch (Exception e) {
throw new Exception("Unable to calculate a request signature: " + e.getMessage(), e);
}
}
}
}
SDK-PYTHON参考模板
import hmac
import hashlib
import json
from typing import Dict, Optional
# 定义常量
SERVICE = "armcloud-paas"
ALGORITHM = "HMAC-SHA256"
X_HOST = "openapi-hk.armcloud.net"
CONTENT_TYPE_JSON = "application/json"
def authorization_create(ak: str, sk: str, request_body: Optional[Dict], x_date: str, http_method: str) -> str:
"""
生成 Authorization 请求头
:param ak: Access Key
:param sk: Secret Key
:param request_body: 请求体(字典形式)
:param x_date: 时间戳
:param http_method: HTTP 方法(GET/POST)
:return: Authorization 请求头
"""
body = b""
if request_body is not None:
if http_method.upper() == "GET":
# GET 请求时,将参数拼接为 key1=value1&key2=value2 的形式
body = "&".join(f"{key}={value}" for key, value in request_body.items()).encode("utf-8")
else:
# POST 请求时,将请求体转换为 JSON 字符串
body = map_to_json(request_body).encode("utf-8")
x_content_sha256 = hash_sha256(body)
short_date = x_date[:8]
signed_headers = "content-type;host;x-content-sha256;x-date"
canonical_string = (
f"host:{X_HOST}\n"
f"x-date:{x_date}\n"
f"content-type:{CONTENT_TYPE_JSON}\n"
f"signedHeaders:{signed_headers}\n"
f"x-content-sha256:{x_content_sha256}"
)
hash_canonical_string = hash_sha256(canonical_string.encode("utf-8"))
credential_scope = f"{short_date}/{SERVICE}/request"
sign_string = f"{ALGORITHM}\n{x_date}\n{credential_scope}\n{hash_canonical_string}"
sign_key = gen_signing_secret_key_v4(sk, short_date, SERVICE)
signature = bytes_to_hex(hmac_sha256(sign_key, sign_string.encode("utf-8")))
authorization = (
f"{ALGORITHM} Credential={ak}/{x_date}/{SERVICE}/request, "
f"SignedHeaders={signed_headers}, Signature={signature}"
)
return authorization
def gen_signing_secret_key_v4(secret_key: str, short_x_date: str, service: str) -> bytes:
"""
生成签名密钥
:param secret_key: 密钥
:param short_x_date: 短日期(yyyyMMdd)
:param service: 服务名称
:return: 签名密钥的字节数据
"""
k_date = hmac_sha256(secret_key.encode("utf-8"), short_x_date.encode("utf-8"))
k_service = hmac_sha256(k_date, service.encode("utf-8"))
return hmac_sha256(k_service, b"request")
def hash_sha256(content: bytes) -> str:
"""
计算 SHA-256 哈希值
:param content: 输入内容(字节数据)
:return: 哈希值的十六进制字符串
"""
sha256 = hashlib.sha256()
sha256.update(content)
return sha256.hexdigest()
def bytes_to_hex(bytes_data: bytes) -> str:
"""
将字节数据转换为十六进制字符串
:param bytes_data: 字节数据
:return: 十六进制字符串
"""
if not bytes_data:
return ""
return "".join(f"{byte:02x}" for byte in bytes_data)
def hmac_sha256(key: bytes, content: bytes) -> bytes:
"""
计算 HMAC-SHA256
:param key: 密钥(字节数据)
:param content: 输入内容(字节数据)
:return: HMAC 结果的字节数据
"""
return hmac.new(key, content, hashlib.sha256).digest()
def map_to_json(map_obj):
json_string = "{"
first_entry = True
for key, value in map_obj.items():
if not first_entry:
json_string += ","
first_entry = False
json_string += f'"{key}":'
if isinstance(value, str):
json_string += f'"{value}"'
elif isinstance(value, (int, float, bool)):
json_string += str(value).lower() if isinstance(value, bool) else str(value)
elif isinstance(value, dict):
json_string += map_to_json(value)
elif isinstance(value, (list, tuple)):
json_string += iterable_to_json(value)
elif value is None:
json_string += "null"
else:
json_string += f'"{str(value)}"'
json_string += "}"
return json_string
def iterable_to_json(iterable):
json_string = "["
first_item = True
for item in iterable:
if not first_item:
json_string += ","
first_item = False
if isinstance(item, str):
json_string += f'"{item}"'
elif isinstance(item, (int, float, bool)):
json_string += str(item).lower() if isinstance(item, bool) else str(item)
elif isinstance(item, dict):
json_string += map_to_json(item)
elif isinstance(item, (list, tuple)):
json_string += iterable_to_json(item)
elif item is None:
json_string += "null"
else:
json_string += f'"{str(item)}"'
json_string += "]"
return json_string
# 示例用法
if __name__ == "__main__":
ak = "ak"
sk = "sk"
request_body = {} # 请求体
x_date = "20250126T230940Z" # 时间戳
http_method = "GET" # HTTP 方法
authorization = authorization_create(ak, sk, request_body, x_date, http_method)
print("Authorization Header:", authorization)