If you prefer not to use an SDK to call Alibaba Cloud OpenAPI, or if your application's runtime environment does not support SDKs, you can call Alibaba Cloud OpenAPI by self-signing your requests. This topic describes the v3 signature mechanism to help you call Alibaba Cloud OpenAPI directly using HTTP requests.
Usage notes
You can directly replace v2 signatures with v3 signatures for API calls.
The OpenAPI portal provides SDKs for Alibaba Cloud products. The APIs for these products support v3 signatures. Note that some cloud products use self-managed gateways and have authentication mechanisms that differ from the one described in this topic. When you send HTTP requests to these products, refer to their respective signature mechanism documents.
For more information about the SLS signature mechanism, see Request signature.
For more information about the OSS signature mechanism, see OSS signature mechanism guide.
HTTP request structure
A complete Alibaba Cloud OpenAPI request consists of the following parts.
Name | Required | Description | Example |
Protocol | Yes | You can configure this by referring to the API reference for different cloud products. Requests can be sent over | https:// |
Endpoint | Yes | The endpoint. You can find the endpoints for different areas where the endpoint is deployed in the endpoint documentation for each cloud product. | ecs.cn-shanghai.aliyuncs.com |
resource_URI_parameters | Yes | The API URL, which includes the API path and request parameters located in the path and query. | ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai |
RequestHeader | Yes | Common request headers, which usually include the API version, Host, and Authorization information. This is described in detail later. | Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0 x-acs-action: RunInstances host: ecs.cn-shanghai.aliyuncs.com x-acs-date: 2023-10-26T09:01:01Z x-acs-version: 2014-05-26 x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0 |
RequestBody | Yes | Business request parameters defined in the body. You can obtain them from the API metadata. | |
HTTPMethod | Yes | The request method. You can obtain it from the API metadata. | POST |
RequestHeader
When you call an Alibaba Cloud OpenAPI, the common request headers must include the following information.
Name | Type | Required | Description | Example |
host | String | Yes | The endpoint. For more information, see HTTP request structure. | ecs.cn-shanghai.aliyuncs.com |
x-acs-action | String | Yes | The name of the API. You can visit the Alibaba Cloud OpenAPI Developer Portal to search for the OpenAPI you want to call. | RunInstances |
x-acs-content-sha256 | String | Yes | The result of hashing the RequestBody and then Base16 encoding it. This value is the same as HashedRequestPayload. | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
x-acs-date | String | Yes | The UTC time in ISO 8601 format: yyyy-MM-ddTHH:mm:ssZ. For example, 2018-01-01T12:00:00Z. The value must be a time within 15 minutes before the request is sent. | 2023-10-26T10:22:32Z |
x-acs-signature-nonce | String | Yes | A unique random number for the signature. This number prevents replay attacks. Use a different random number for each request. This mechanism applies only to the HTTP protocol. | 3156853299f313e23d1673dc12e1703d |
x-acs-version | String | Yes | The API version. For information about how to obtain the version, see How do I get the API version (x-acs-version)?. | 2014-05-26 |
Authorization | String | Required for non-anonymous requests | The authentication information used to verify the request's legitimacy. The format is Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature. SignatureAlgorithm is the signature encryption method, which is ACS3-HMAC-SHA256. Credential is the user's AccessKey ID. You can view your AccessKey ID in the RAM console. To create an AccessKey pair, see Create an AccessKey pair. SignedHeaders specifies the keys of the request headers that are included in the signature. Note: For better security, sign all common request headers except for Authorization. Signature is the request signature. For its value, see Signature mechanism. | ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0 |
x-acs-security-token | String | Required for STS authentication | The value of SecurityToken in the response returned by calling the AssumeRole operation. |
Signature mechanism
Signatures are authenticated using an AccessKey pair. For each HTTP or HTTPS request, Alibaba Cloud API Gateway recalculates the signature based on the request parameters. The gateway then compares this signature with the one provided in the request to verify the requester's identity. This process ensures data integrity and security.
Requests and responses are encoded in the UTF-8 character set.
Step 1: Construct a canonical request
The following pseudocode shows how to construct a canonical request (CanonicalRequest):
CanonicalRequest =
HTTPRequestMethod + '\n' + // HTTP request method, in uppercase
CanonicalURI + '\n' + // Canonical URI
CanonicalQueryString + '\n' + // Canonical query string
CanonicalHeaders + '\n' + // Canonical headers
SignedHeaders + '\n' + // Signed headers
HashedRequestPayload // The value of the RequestBody after it is hashed
HTTPRequestMethod (request method)
The uppercase HTTP method name, such as GET or POST.
CanonicalURI (canonical URI)
The canonical URI is the encoded resource path of the URL. The resource path is the part of the URL between the host and the query string. It includes the /
after the host but not the ?
before the query string. You must encode each part of the URI (the strings separated by /
) using the UTF-8 character set according to the rules in RFC3986:
Characters A-Z, a-z, 0-9, and the characters
-
,_
,.
, and~
are not encoded.Other characters are encoded as
%
followed by the character's ASCII code in hexadecimal format. For example, a half-width double quotation mark ("
) is encoded as%22
. Note that some special characters require special handling.Before encoding
After encoding
Space ( )
%20
Asterisk (
*
)%2A
%7E
Tilde (
~
)
If you use java.net.URLEncoder
from the Java standard library, you can first encode the string using the encode
method. Then, you must replace the plus sign (+
) with %20
, the asterisk (*
) with %2A
, and %7E
with a tilde (~
) to obtain the encoded string that complies with the preceding rules.
For RPC-style APIs, use a forward slash (/
) as the canonical URI.
For ROA-style APIs, this parameter is the value of the path
parameter in the OpenAPI metadata, such as /api/v1/clusters
.
CanonicalQueryString (canonical query string)
In the API metadata, if a request parameter is specified to be in the query ("in":"query"
), you must concatenate all such parameters as follows:
Sort the request parameters by name in ascending alphabetical order.
URI-encode each parameter name and value using the UTF-8 character set according to the rules in RFC3986. The encoding rules are the same as the rules for encoding the canonical URI.
Connect the encoded parameter name and value with an equal sign (
=
). If a parameter has no value, use an empty string for its value.Connect multiple request parameters with ampersands (
&
).
If a request parameter is of the array or object type, you must convert the parameter value into indexed key-value pairs. For more information, see How do I pass parameters of the array or object type?.
If a request parameter is a JSON string, the order of parameters in the JSON string does not affect the signature calculation.
If no query string exists, use an empty string as the canonical query string.
Example:
ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
HashedRequestPayload
Hash the request body and then Base16-encode the hash value to obtain the hashed request payload. Update the value of the x-acs-content-sha256
header in the request header to the value of the hashed request payload. The following pseudocode shows how to calculate the hashed request payload:
HashedRequestPayload = HexEncode(Hash(RequestBody))
In the API metadata, if a request parameter for the API is set to
"in": "body"
or"in": "formData"
, you must pass the parameters in the RequestBody:NoteIf no request parameters are passed in the request body, the request body is an empty string.
If the request parameters include
"in": "formData"
, you must concatenate the parameters into a string in the formatkey1=value1&key2=value2&key3=value3
. You must also addcontent-type=application/x-www-form-urlencoded
to the request header. Note that if a request parameter is an array or object, you must convert the parameter value to indexed key-value pairs.If the request parameters include
"in": "body"
, you must add the `Content-Type` header to the request. The value of this header depends on the content type of the request. For example:If the request content is JSON data, the content-type is
application/json
.If the request content is a binary file stream, the content-type is
application/octet-stream
.
Hash represents the message digest function. Only the SHA256 algorithm is supported.
HexEncode represents the encoding function that returns the digest in lowercase hexadecimal format (Base16 encoding).
The following example shows the value of the hashed request payload when the request body is empty:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
CanonicalHeaders (canonical request headers)
Concatenate the parameters in the request header as follows:
Filter the request header to select parameters that have the
x-acs-
prefix, or are namedhost
orcontent-type
.Convert the parameter names to lowercase and sort them in alphabetical order.
Trim leading and trailing spaces from the parameter values.
Connect the parameter name and value with a colon (
:
) and add a line feed (\n
) at the end to form a canonical header entry.Concatenate multiple canonical header entries into a single string.
All request headers except for the Authorization header must be included in the signature if they meet the requirements.
The pseudocode is as follows:
CanonicalHeaderEntry = Lowercase(HeaderName) + ':' + Trim(HeaderValue) + '\n'
CanonicalHeaders =
CanonicalHeaderEntry0 + CanonicalHeaderEntry1 + ... + CanonicalHeaderEntryN
Example:
host:ecs.cn-shanghai.aliyuncs.com
x-acs-action:RunInstances
x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date:2023-10-26T10:22:32Z
x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
x-acs-version:2014-05-26
SignedHeaders (list of signed headers)
This parameter specifies the common request headers that are included in the signature for the request. It corresponds to the parameter names in the canonical headers. Construct the list of signed headers as follows:
Convert the names of the headers that are included in the canonical headers to lowercase.
Sort the header names in alphabetical order and separate them with semicolons (
;
).
The pseudocode is as follows:
SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ... + Lowercase(HeaderNameN)
Example:
host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version
Step 2: Construct the string to sign
Construct the string to sign (stringToSign) based on the following pseudocode:
StringToSign =
SignatureAlgorithm + '\n' +
HashedCanonicalRequest
SignatureAlgorithm
The signature protocol supports only the ACS3-HMAC-SHA256 algorithm.
HashedCanonicalRequest
The hashed canonical request string. The following pseudocode shows how to calculate the hashed canonical request:
HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
Hash represents the message digest function. Only the SHA256 algorithm is supported.
HexEncode represents the encoding function that returns the digest in lowercase hexadecimal format (Base16 encoding).
Example:
ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
Step 3: Calculate the signature
Calculate the signature value (Signature) based on the following pseudocode.
Signature = HexEncode(SignatureMethod(Secret, StringToSign))
StringToSign: The string to sign that is constructed in Step 2 and encoded in UTF-8.
SignatureMethod: Use HMAC-SHA256 as the signature algorithm.
Secret: The AccessKey secret.
HexEncode: The encoding function that returns the digest in lowercase hexadecimal format (Base16 encoding).
Example:
06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
Step 4: Add the signature to the request
After you calculate the signature, construct the Authorization request header in the following format: Authorization: <b>SignatureAlgorithm</b> Credential=<b>AccessKeyId</b>,SignedHeaders=<b>SignedHeaders</b>,Signature=<b>Signature</b>
.
Example:
ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
Signature example code
To help you better understand the signature mechanism, this topic provides complete implementations in major programming languages. The example code is provided to help you understand the signature mechanism and may not be universally applicable. Alibaba Cloud OpenAPI provides SDKs for various programming languages and development frameworks. If you use these SDKs, you do not need to perform the signature process. This lets you quickly build applications that are related to Alibaba Cloud. We recommend that you use the SDKs.
Before you sign a request, make sure to check the API metadata to obtain information such as the API's request method, request parameter names, request parameter types, and how parameters are passed. Otherwise, the signature is likely to fail.
Fixed parameter example
This example uses assumed parameter values to demonstrate the correct output at each step of the signature mechanism. You can use the assumed parameter values from this example in your code for calculation and compare your output with the content of this example to verify that your signature process is correct.
Required parameter name | Assumed parameter value |
AccessKeyID | YourAccessKeyId |
AccessKeySecret | YourAccessKeySecret |
x-acs-signature-nonce | 3156853299f313e23d1673dc12e1703d |
x-acs-date | 2023-10-26T10:22:32Z |
x-acs-action | RunInstances |
x-acs-version | 2014-05-26 |
host | ecs.cn-shanghai.aliyuncs.com |
API request parameters:
ImageId | win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd |
RegionId | cn-shanghai |
The signature flow is as follows:
Construct a canonical request.
POST
/
ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
host:ecs.cn-shanghai.aliyuncs.com
x-acs-action:RunInstances
x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date:2023-10-26T10:22:32Z
x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
x-acs-version:2014-05-26
host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Construct the string to sign.
ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
Calculate the signature.
06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
Add the signature to the request.
POST /?ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai HTTP/1.1
Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
x-acs-action: RunInstances
host: ecs.cn-shanghai.aliyuncs.com
x-acs-date: 2023-10-26T09:01:01Z
x-acs-version: 2014-05-26
x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0
user-agent: AlibabaCloud (Mac OS X; x86_64) Java/1.8.0_352-b08 tea-util/0.2.6 TeaDSL/1
accept: application/json
Java example
The example code runs in JDK 1.8. You may need to adjust the code based on your specific situation.
To run the Java example, add the following Maven dependencies to your pom.xml file.
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class SignatureDemo {
public static class SignatureRequest {
// HTTP Method
private final String httpMethod;
// Request path
private final String canonicalUri;
// Endpoint
private final String host;
// API name
private final String xAcsAction;
// API version
private final String xAcsVersion;
// Headers
private final Map<String, String> headers = new TreeMap<>();
// Body parameters
private byte[] body;
// Query parameters
private final Map<String, Object> queryParam = new TreeMap<>();
public SignatureRequest(String httpMethod, String canonicalUri, String host,
String xAcsAction, String xAcsVersion) {
this.httpMethod = httpMethod;
this.canonicalUri = canonicalUri;
this.host = host;
this.xAcsAction = xAcsAction;
this.xAcsVersion = xAcsVersion;
initHeader();
}
private void initHeader() {
headers.put("host", host);
headers.put("x-acs-action", xAcsAction);
headers.put("x-acs-version", xAcsVersion);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
headers.put("x-acs-date", sdf.format(new Date()));
headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
}
public String getHttpMethod() {
return httpMethod;
}
public String getCanonicalUri() {
return canonicalUri;
}
public String getHost() {
return host;
}
public Map<String, String> getHeaders() {
return headers;
}
public byte[] getBody() {
return body;
}
public Map<String, Object> getQueryParam() {
return queryParam;
}
public void setBody(byte[] body) {
this.body = body;
}
public void setQueryParam(String key, Object value) {
this.queryParam.put(key, value);
}
public void setHeaders(String key, String value) {
this.headers.put(key, value);
}
}
public static class SignatureService {
private static final String ALGORITHM = "ACS3-HMAC-SHA256";
/**
* Calculate the signature
*/
public static void getAuthorization(SignatureRequest signatureRequest,
String accessKeyId, String accessKeySecret, String securityToken) {
try {
// Process complex query parameters
Map<String, Object> processedQueryParams = new TreeMap<>();
processObject(processedQueryParams, "", signatureRequest.getQueryParam());
signatureRequest.getQueryParam().clear();
signatureRequest.getQueryParam().putAll(processedQueryParams);
// Step 1: Construct the canonical request string
String canonicalQueryString = buildCanonicalQueryString(signatureRequest.getQueryParam());
// Calculate the request body hash
String hashedRequestPayload = calculatePayloadHash(signatureRequest.getBody());
signatureRequest.setHeaders("x-acs-content-sha256", hashedRequestPayload);
// Add security token if it exists
if (securityToken != null && !securityToken.isEmpty()) {
signatureRequest.setHeaders("x-acs-security-token", securityToken);
}
// Build canonical headers and signed headers
CanonicalHeadersResult canonicalHeadersResult = buildCanonicalHeaders(signatureRequest.getHeaders());
// Build the canonical request
String canonicalRequest = String.join("\n",
signatureRequest.getHttpMethod(),
signatureRequest.getCanonicalUri(),
canonicalQueryString,
canonicalHeadersResult.canonicalHeaders,
canonicalHeadersResult.signedHeaders,
hashedRequestPayload);
System.out.println("canonicalRequest=========>\n" + canonicalRequest);
// Step 2: Construct the string to sign
String hashedCanonicalRequest = sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
System.out.println("stringToSign=========>\n" + stringToSign);
// Step 3: Calculate the signature
String signature = DatatypeConverter.printHexBinary(
hmac256(accessKeySecret.getBytes(StandardCharsets.UTF_8), stringToSign))
.toLowerCase();
System.out.println("signature=========>" + signature);
// Step 4: Build the Authorization header
String authorization = String.format("%s Credential=%s,SignedHeaders=%s,Signature=%s",
ALGORITHM, accessKeyId, canonicalHeadersResult.signedHeaders, signature);
System.out.println("authorization=========>" + authorization);
signatureRequest.getHeaders().put("Authorization", authorization);
} catch (Exception e) {
throw new RuntimeException("Failed to generate authorization", e);
}
}
/**
* Handle parameters of the formData request parameter type.
*/
private static String formDataToString(Map<String, Object> formData) {
Map<String, Object> tileMap = new HashMap<>();
processObject(tileMap, "", formData);
StringBuilder result = new StringBuilder();
boolean first = true;
String symbol = "&";
for (Map.Entry<String, Object> entry : tileMap.entrySet()) {
String value = String.valueOf(entry.getValue());
if (value != null && !value.isEmpty()) {
if (first) {
first = false;
} else {
result.append(symbol);
}
result.append(percentCode(entry.getKey()));
result.append("=");
result.append(percentCode(value));
}
}
return result.toString();
}
/**
* Build the canonical query string
*/
private static String buildCanonicalQueryString(Map<String, Object> queryParams) {
return queryParams.entrySet().stream()
.map(entry -> percentCode(entry.getKey()) + "=" +
percentCode(String.valueOf(entry.getValue())))
.collect(Collectors.joining("&"));
}
/**
* Calculate the request body hash value
*/
private static String calculatePayloadHash(byte[] body) throws Exception {
if (body != null) {
return sha256Hex(body);
} else {
return sha256Hex("".getBytes(StandardCharsets.UTF_8));
}
}
/**
* Build the canonical header information
*/
private static CanonicalHeadersResult buildCanonicalHeaders(Map<String, String> headers) {
List<Map.Entry<String, String>> signedHeaders = headers.entrySet().stream()
.filter(entry -> {
String key = entry.getKey().toLowerCase();
return key.startsWith("x-acs-") || "host".equals(key) || "content-type".equals(key);
})
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.toList());
StringBuilder canonicalHeaders = new StringBuilder();
StringBuilder signedHeadersString = new StringBuilder();
for (Map.Entry<String, String> entry : signedHeaders) {
String lowerKey = entry.getKey().toLowerCase();
String value = entry.getValue().trim();
canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
signedHeadersString.append(lowerKey).append(";");
}
if (signedHeadersString.length() > 0) {
signedHeadersString.setLength(signedHeadersString.length() - 1); // Remove the last semicolon
}
return new CanonicalHeadersResult(canonicalHeaders.toString(), signedHeadersString.toString());
}
private static class CanonicalHeadersResult {
final String canonicalHeaders;
final String signedHeaders;
CanonicalHeadersResult(String canonicalHeaders, String signedHeaders) {
this.canonicalHeaders = canonicalHeaders;
this.signedHeaders = signedHeaders;
}
}
/**
* Process complex object parameters
*/
private static void processObject(Map<String, Object> map, String key, Object value) {
if (value == null) {
return;
}
if (key == null) {
key = "";
}
if (value instanceof List<?>) {
List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); ++i) {
processObject(map, key + "." + (i + 1), list.get(i));
}
} else if (value instanceof Map<?, ?>) {
Map<?, ?> subMap = (Map<?, ?>) value;
for (Map.Entry<?, ?> entry : subMap.entrySet()) {
processObject(map, key + "." + entry.getKey().toString(), entry.getValue());
}
} else {
if (key.startsWith(".")) {
key = key.substring(1);
}
if (value instanceof byte[]) {
map.put(key, new String((byte[]) value, StandardCharsets.UTF_8));
} else {
map.put(key, String.valueOf(value));
}
}
}
/**
* HMAC-SHA256 calculation
*/
private static byte[] hmac256(byte[] secretKey, String str) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
}
/**
* SHA-256 hash calculation
*/
private static String sha256Hex(byte[] input) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(input);
return DatatypeConverter.printHexBinary(digest).toLowerCase();
}
/**
* URL encoding
*/
public static String percentCode(String str) {
if (str == null) {
return "";
}
try {
return URLEncoder.encode(str, "UTF-8")
.replace("+", "%20")
.replace("*", "%2A")
.replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding not supported", e);
}
}
}
/**
* Signature example. Replace the example parameters in the main method as needed.
* The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
* <p>
* Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
* 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
* 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. Note: For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
* 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
public static void main(String[] args) throws IOException {
// Get AccessKey from environment variables
String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
String securityToken = System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN");
if (accessKeyId == null || accessKeySecret == null) {
System.err.println("Set the ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables.");
return;
}
// RPC API example 1: Request parameters are in "query". This example uses DescribeInstanceStatus of ECS.
SignatureRequest signatureRequest = new SignatureRequest(
"POST",
"/",
"ecs.cn-hangzhou.aliyuncs.com",
"DescribeInstanceStatus",
"2014-05-26"
);
signatureRequest.setQueryParam("RegionId", "cn-hangzhou");
signatureRequest.setQueryParam("InstanceId", Arrays.asList("i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"));
/*// RPC API example 2: Request parameters are in "body" (file upload scenario). This example uses RecognizeGeneral of OCR.
SignatureRequest signatureRequest = new SignatureRequest(
"POST",
"/",
"ocr-api.cn-hangzhou.aliyuncs.com",
"RecognizeGeneral",
"2021-07-07");
signatureRequest.setBody(Files.readAllBytes(Paths.get("D:\\test.jpeg")));
signatureRequest.setHeaders("content-type", "application/octet-stream");*/
/*// RPC API example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario). This example uses TranslateGeneral of Machine Translation.
String httpMethod = "POST";
String canonicalUri = "/";
String host = "mt.aliyuncs.com";
String xAcsAction = "TranslateGeneral";
String xAcsVersion = "2018-10-12";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
Map<String, Object> body = new HashMap<>();
body.put("FormatType", "text");
body.put("SourceLanguage", "zh");
body.put("TargetLanguage", "en");
body.put("SourceText", "Hello");
body.put("Scene", "general");
String formDataToString = SignatureService.formDataToString(body);
signatureRequest.setBody(formDataToString.getBytes(StandardCharsets.UTF_8));
signatureRequest.setHeaders("content-type", "application/x-www-form-urlencoded");*/
/*// ROA API POST request example. This example uses creating a cluster in Container Service.
SignatureRequest signatureRequest = new SignatureRequest(
"POST",
"/clusters",
"cs.cn-chengdu.aliyuncs.com",
"CreateCluster",
"2015-12-15");
TreeMap<String, Object> body = new TreeMap<>();
body.put("name", "test");
body.put("cluster_type", "ManagedKubernetes");
body.put("kubernetes_version", "1.34.1-aliyun.1");
body.put("region_id", "cn-chengdu");
body.put("snat_entry", true);
body.put("deletion_protection", true);
body.put("proxy_mode", "ipvs");
body.put("profile", "Default");
body.put("timezone", "Asia/Shanghai");
body.put("cluster_spec", "ack.pro.small");
body.put("enable_rrsa", false);
body.put("service_cidr", "192.168.0.0/16");
body.put("zone_ids", Arrays.asList("cn-chengdu-b","cn-chengdu-b"));
Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
signatureRequest.setBody(gson.toJson(body).getBytes(StandardCharsets.UTF_8));
signatureRequest.setHeaders("content-type", "application/json");*/
/*// ROA API GET request. This example uses querying cluster information in Container Service.
SignatureRequest signatureRequest = new SignatureRequest(
"GET",
"/clusters/" + SignatureService.percentCode("c299f90b63b************") + "/resources",
"cs.cn-chengdu.aliyuncs.com",
"DescribeClusterResources",
"2015-12-15");
signatureRequest.setQueryParam("with_addon_resources", true);*/
/*// ROA API DELETE request. This example uses deleting a cluster.
SignatureRequest signatureRequest = new SignatureRequest(
"DELETE",
"/clusters/" + SignatureService.percentCode("c299f90b63b************"),
"cs.cn-chengdu.aliyuncs.com",
"DeleteCluster",
"2015-12-15");*/
// Generate the signature
SignatureService.getAuthorization(signatureRequest, accessKeyId, accessKeySecret, securityToken);
// Test if the API can be called successfully
callApi(signatureRequest);
}
/**
* For testing only
*/
private static void callApi(SignatureRequest signatureRequest) {
try {
String url = "https://" + signatureRequest.getHost() + signatureRequest.getCanonicalUri();
URIBuilder uriBuilder = new URIBuilder(url);
// Add query parameters
for (Map.Entry<String, Object> entry : signatureRequest.getQueryParam().entrySet()) {
uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
}
HttpUriRequest httpRequest;
switch (signatureRequest.getHttpMethod()) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (signatureRequest.getBody() != null) {
httpPost.setEntity(new ByteArrayEntity(signatureRequest.getBody(), ContentType.create(signatureRequest.getHeaders().get("content-type"))));
}
httpRequest = httpPost;
break;
case "DELETE":
httpRequest = new HttpDelete(uriBuilder.build());
break;
default:
System.out.println("Unsupported HTTP method: " + signatureRequest.getHttpMethod());
throw new IllegalArgumentException("Unsupported HTTP method");
}
// Add request headers
for (Map.Entry<String, String> entry : signatureRequest.getHeaders().entrySet()) {
httpRequest.addHeader(entry.getKey(), entry.getValue());
}
// Send the request
try (CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(httpRequest)) {
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println("API Response: " + result);
}
} catch (IOException | URISyntaxException e) {
throw new RuntimeException("Failed to call API", e);
}
}
}
Python example
The example code runs in Python 3.12.3. You may need to adjust the code based on your specific situation.
You must manually install pytz and requests. Run the following command in the terminal based on your Python version.
Python 3
pip3 install pytz
pip3 install requests
import hashlib
import hmac
import json
import os
import uuid
from collections import OrderedDict
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from urllib.parse import quote_plus, urlencode
import pytz
import requests
class SignatureRequest:
def __init__(
self,
http_method: str,
canonical_uri: str,
host: str,
x_acs_action: str,
x_acs_version: str
):
self.http_method = http_method
self.canonical_uri = canonical_uri
self.host = host
self.x_acs_action = x_acs_action
self.x_acs_version = x_acs_version
self.headers = self._init_headers()
self.query_param = OrderedDict() # type: Dict[str, Any]
self.body = None # type: Optional[bytes]
def _init_headers(self) -> Dict[str, str]:
current_time = datetime.now(pytz.timezone('Etc/GMT'))
headers = OrderedDict([
('host', self.host),
('x-acs-action', self.x_acs_action),
('x-acs-version', self.x_acs_version),
('x-acs-date', current_time.strftime('%Y-%m-%dT%H:%M:%SZ')),
('x-acs-signature-nonce', str(uuid.uuid4())),
])
return headers
def sorted_query_params(self) -> None:
"""Sorts query parameters by name and returns the encoded string."""
self.query_param = dict(sorted(self.query_param.items()))
def sorted_headers(self) -> None:
"""Sorts request headers by name and returns the encoded string."""
self.headers = dict(sorted(self.headers.items()))
def get_authorization(request: SignatureRequest) -> None:
try:
new_query_param = OrderedDict()
process_object(new_query_param, '', request.query_param)
request.query_param.clear()
request.query_param.update(new_query_param)
request.sorted_query_params()
# Step 1: Construct the canonical request string
canonical_query_string = "&".join(
f"{percent_code(quote_plus(k))}={percent_code(quote_plus(str(v)))}"
for k, v in request.query_param.items()
)
hashed_request_payload = sha256_hex(request.body or b'')
request.headers['x-acs-content-sha256'] = hashed_request_payload
if SECURITY_TOKEN:
signature_request.headers["x-acs-security-token"] = SECURITY_TOKEN
request.sorted_headers()
filtered_headers = OrderedDict()
for k, v in request.headers.items():
if k.lower().startswith("x-acs-") or k.lower() in ["host", "content-type"]:
filtered_headers[k.lower()] = v
canonical_headers = "\n".join(f"{k}:{v}" for k, v in filtered_headers.items()) + "\n"
signed_headers = ";".join(filtered_headers.keys())
canonical_request = (
f"{request.http_method}\n{request.canonical_uri}\n{canonical_query_string}\n"
f"{canonical_headers}\n{signed_headers}\n{hashed_request_payload}"
)
print(canonical_request)
# Step 2: Construct the string to sign
hashed_canonical_request = sha256_hex(canonical_request.encode("utf-8"))
string_to_sign = f"{ALGORITHM}\n{hashed_canonical_request}"
print(string_to_sign)
# Step 3: Calculate the signature
signature = hmac256(ACCESS_KEY_SECRET.encode("utf-8"), string_to_sign).hex().lower()
# Step 4: Construct the Authorization header
authorization = f'{ALGORITHM} Credential={ACCESS_KEY_ID},SignedHeaders={signed_headers},Signature={signature}'
request.headers["Authorization"] = authorization
except Exception as e:
print("Failed to get authorization")
print(e)
def form_data_to_string(form_data: Dict[str, Any]) -> str:
tile_map = OrderedDict()
process_object(tile_map, "", form_data)
return urlencode(tile_map)
def process_object(result_map: Dict[str, str], key: str, value: Any) -> None:
if value is None:
return
if isinstance(value, (list, tuple)):
for i, item in enumerate(value):
process_object(result_map, f"{key}.{i + 1}", item)
elif isinstance(value, dict):
for sub_key, sub_value in value.items():
process_object(result_map, f"{key}.{sub_key}", sub_value)
else:
key = key.lstrip(".")
result_map[key] = value.decode("utf-8") if isinstance(value, bytes) else str(value)
def hmac256(key: bytes, msg: str) -> bytes:
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def sha256_hex(s: bytes) -> str:
return hashlib.sha256(s).hexdigest()
def call_api(request: SignatureRequest) -> None:
url = f"https://{request.host}{request.canonical_uri}"
if request.query_param:
url += "?" + urlencode(request.query_param, doseq=True, safe="*")
headers = dict(request.headers)
data = request.body
try:
response = requests.request(
method=request.http_method, url=url, headers=headers, data=data
)
response.raise_for_status()
print(response.text)
except requests.RequestException as e:
print("Failed to send request")
print(e)
def percent_code(encoded_str: str) -> str:
return encoded_str.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
# Get Access Key ID and Access Key Secret from environment variables
ACCESS_KEY_ID = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID")
ACCESS_KEY_SECRET = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
SECURITY_TOKEN = os.environ.get("ALIBABA_CLOUD_SECURITY_TOKEN")
ALGORITHM = "ACS3-HMAC-SHA256"
"""
Signature example. When testing, you can select an example from the main function and modify the example values as needed. For example, to call SendSms, select Example 1 and then modify http_method, host, x_acs_action, x_acs_version, and query_param.
The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs.
Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the OpenAPI metadata, and encapsulate the parameters into SignatureRequest.
1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam without setting content-type. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body and set content-type as needed. Note: For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body with content-type as application/x-www-form-urlencoded.
"""
if __name__ == "__main__":
# RPC API request example 1: Request parameters are in "query"
http_method = "POST" # Request method, which can be obtained from the metadata. POST is recommended.
canonical_uri = "/" # RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
host = "ecs.cn-hangzhou.aliyuncs.com" # Cloud product endpoint
x_acs_action = "DescribeInstanceStatus" # API name
x_acs_version = "2014-05-26" # API version number
signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# DescribeInstanceStatus request parameters:
# RegionId is of type String in the metadata, "in":"query", required
signature_request.query_param['RegionId'] = 'cn-hangzhou'
# InstanceId is of type array in the metadata, "in":"query", not required
signature_request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX",
"i-bp1incuofvzxXXXXXXXX"]
# # RPC API request example 2: Request parameters are in "body" (file upload scenario)
# http_method = "POST"
# canonical_uri = "/"
# host = "ocr-api.cn-hangzhou.aliyuncs.com"
# x_acs_action = "RecognizeGeneral"
# x_acs_version = "2021-07-07"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# # Request parameters are shown as "in": "body" in the metadata, passed through the body.
# file_path = "D:\\test.png"
# with open(file_path, 'rb') as file:
# # Read the image content as a byte array
# signature_request.body = file.read()
# signature_request.headers["content-type"] = "application/octet-stream"
# # RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
# http_method = "POST"
# canonical_uri = "/"
# host = "mt.aliyuncs.com"
# x_acs_action = "TranslateGeneral"
# x_acs_version = "2018-10-12"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# # TranslateGeneral request parameters:
# # Context is of type String in the metadata, "in":"query", not required
# signature_request.query_param['Context'] = 'Morning'
# # Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
# form_data = OrderedDict()
# form_data["FormatType"] = "text"
# form_data["SourceLanguage"] = "zh"
# form_data["TargetLanguage"] = "en"
# form_data["SourceText"] = "Hello"
# form_data["Scene"] = "general"
# signature_request.body = bytes(form_data_to_string(form_data), 'utf-8')
# signature_request.headers["content-type"] = "application/x-www-form-urlencoded"
# # Example 4: ROA API POST request
# http_method = "POST"
# canonical_uri = "/clusters"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "CreateCluster"
# x_acs_version = "2015-12-15"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# Request parameters are shown as "in":"body" in the metadata, passed through the body.
# body = OrderedDict()
# body["name"] = "testDemo"
# body["region_id"] = "cn-beijing"
# body["cluster_type"] = "ExternalKubernetes"
# body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
# body["container_cidr"] = "172.16.1.0/20"
# body["service_cidr"] = "10.2.0.0/24"
# body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
# body["vswitch_ids"] = ["vsw-2zei30dhfldu8XXXXXXXX"]
# signature_request.body = bytes(json.dumps(body, separators=(',', ':')), 'utf-8')
# signature_request.headers["content-type"] = "application/json; charset=utf-8"
# # Example 5: ROA API GET request
# http_method = "GET"
# # If canonicalUri has a path parameter, encode the path parameter: percent_code({path_parameter})
# cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
# canonical_uri = f"/clusters/{cluster_id_encode}/resources"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "DescribeClusterResources"
# x_acs_version = "2015-12-15"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# signature_request.query_param['with_addon_resources'] = True
# # Example 6: ROA API DELETE request
# http_method = "DELETE"
# # If canonicalUri has a path parameter, encode the path parameter: percent_code({path_parameter})
# cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
# canonical_uri = f"/clusters/{cluster_id_encode}"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "DeleteCluster"
# x_acs_version = "2015-12-15"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
get_authorization(signature_request)
call_api(signature_request)
Go example
The example code runs in go1.22.2. You may need to adjust the code based on your specific situation.
Run the following command in the terminal:
go get github.com/google/uuid
go get golang.org/x/exp/maps
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"os"
"sort"
"golang.org/x/exp/maps"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/google/uuid"
)
type Request struct {
httpMethod string
canonicalUri string
host string
xAcsAction string
xAcsVersion string
headers map[string]string
body []byte
queryParam map[string]interface{}
}
func NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion string) *Request {
req := &Request{
httpMethod: httpMethod,
canonicalUri: canonicalUri,
host: host,
xAcsAction: xAcsAction,
xAcsVersion: xAcsVersion,
headers: make(map[string]string),
queryParam: make(map[string]interface{}),
}
req.headers["host"] = host
req.headers["x-acs-action"] = xAcsAction
req.headers["x-acs-version"] = xAcsVersion
req.headers["x-acs-date"] = time.Now().UTC().Format(time.RFC3339)
req.headers["x-acs-signature-nonce"] = uuid.New().String()
return req
}
// os.Getenv() gets the AccessKey ID and AccessKey secret from environment variables.
var (
AccessKeyId = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
SecurityToken = os.Getenv("ALIBABA_CLOUD_SECURITY_TOKEN")
ALGORITHM = "ACS3-HMAC-SHA256"
)
// Signature example. Replace the example parameters in the main method as needed.
// The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
// Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
// 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
// 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
// 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
func main() {
// RPC API request example 1: Request parameters are in "query"
httpMethod := "POST" // Request method. Most RPC APIs support both POST and GET. This example uses POST.
canonicalUri := "/" // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
host := "ecs.cn-hangzhou.aliyuncs.com" // Cloud product endpoint
xAcsAction := "DescribeInstanceStatus" // API name
xAcsVersion := "2014-05-26" // API version number
req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// DescribeInstanceStatus request parameters:
// RegionId is of type String in the metadata, "in":"query", required
req.queryParam["RegionId"] = "cn-hangzhou"
// InstanceId is of type array in the metadata, "in":"query", not required
instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
req.queryParam["InstanceId"] = instanceIds
// // RPC API request example 2: Request parameters are in "body" (file upload scenario)
// httpMethod := "POST"
// canonicalUri := "/"
// host := "ocr-api.cn-hangzhou.aliyuncs.com"
// xAcsAction := "RecognizeGeneral"
// xAcsVersion := "2021-07-07"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// // Read file content
// filePath := "D:\\test.png"
// bytes, err := os.ReadFile(filePath)
// if err != nil {
// fmt.Println("Error reading file:", err)
// return
// }
// req.body = bytes
// req.headers["content-type"] = "application/octet-stream"
// // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
// httpMethod := "POST"
// canonicalUri := "/"
// host := "mt.aliyuncs.com"
// xAcsAction := "TranslateGeneral"
// xAcsVersion := "2018-10-12"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// // TranslateGeneral request parameters:
// // Context is of type String in the metadata, "in":"query", not required
// req.queryParam["Context"] = "Morning"
// // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
// body := make(map[string]interface{})
// body["FormatType"] = "text"
// body["SourceLanguage"] = "zh"
// body["TargetLanguage"] = "en"
// body["SourceText"] = "Hello"
// body["Scene"] = "general"
// str := formDataToString(body)
// req.body = []byte(*str)
// req.headers["content-type"] = "application/x-www-form-urlencoded"
// // ROA API POST request
// httpMethod := "POST"
// canonicalUri := "/clusters"
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "CreateCluster"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// // Encapsulate request parameters. The request parameters are shown as "in": "body" in the metadata, which means the parameters are in the body.
// body := make(map[string]interface{})
// body["name"] = "testDemo"
// body["region_id"] = "cn-beijing"
// body["cluster_type"] = "ExternalKubernetes"
// body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
// body["container_cidr"] = "10.0.0.0/8"
// body["service_cidr"] = "172.16.1.0/20"
// body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
// vswitch_ids := []interface{}{"vsw-2zei30dhfldu8XXXXXXXX"}
// body["vswitch_ids"] = vswitch_ids
// jsonBytes, err := json.Marshal(body)
// if err != nil {
// fmt.Println("Error marshaling to JSON:", err)
// return
// }
// req.body = []byte(jsonBytes)
// req.headers["content-type"] = "application/json; charset=utf-8"
// // ROA API GET request
// httpMethod := "GET"
// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66") + "/resources"
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "DescribeClusterResources"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// req.queryParam["with_addon_resources"] = "true"
// // ROA API DELETE request
// httpMethod := "DELETE"
// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66")
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "DeleteCluster"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// Signature process
getAuthorization(req)
// Call the API
error := callAPI(req)
if error != nil {
println(error.Error())
}
}
func callAPI(req *Request) error {
urlStr := "https://" + req.host + req.canonicalUri
q := url.Values{}
keys := maps.Keys(req.queryParam)
sort.Strings(keys)
for _, k := range keys {
v := req.queryParam[k]
q.Set(k, fmt.Sprintf("%v", v))
}
urlStr += "?" + q.Encode()
fmt.Println(urlStr)
httpReq, err := http.NewRequest(req.httpMethod, urlStr, strings.NewReader(string(req.body)))
if err != nil {
return err
}
for key, value := range req.headers {
httpReq.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
var respBuffer bytes.Buffer
_, err = io.Copy(&respBuffer, resp.Body)
if err != nil {
return err
}
respBytes := respBuffer.Bytes()
fmt.Println(string(respBytes))
return nil
}
func getAuthorization(req *Request) {
// Process parameters of List and Map types in queryParam, and flatten the parameters
newQueryParams := make(map[string]interface{})
processObject(newQueryParams, "", req.queryParam)
req.queryParam = newQueryParams
// Step 1: Construct the canonical request string
canonicalQueryString := ""
keys := maps.Keys(req.queryParam)
sort.Strings(keys)
for _, k := range keys {
v := req.queryParam[k]
canonicalQueryString += percentCode(url.QueryEscape(k)) + "=" + percentCode(url.QueryEscape(fmt.Sprintf("%v", v))) + "&"
}
canonicalQueryString = strings.TrimSuffix(canonicalQueryString, "&")
fmt.Printf("canonicalQueryString========>%s\n", canonicalQueryString)
var bodyContent []byte
if req.body == nil {
bodyContent = []byte("")
} else {
bodyContent = req.body
}
hashedRequestPayload := sha256Hex(bodyContent)
req.headers["x-acs-content-sha256"] = hashedRequestPayload
if SecurityToken != "" {
req.headers["x-acs-security-token"] = SecurityToken
}
canonicalHeaders := ""
signedHeaders := ""
HeadersKeys := maps.Keys(req.headers)
sort.Strings(HeadersKeys)
for _, k := range HeadersKeys {
lowerKey := strings.ToLower(k)
if lowerKey == "host" || strings.HasPrefix(lowerKey, "x-acs-") || lowerKey == "content-type" {
canonicalHeaders += lowerKey + ":" + req.headers[k] + "\n"
signedHeaders += lowerKey + ";"
}
}
signedHeaders = strings.TrimSuffix(signedHeaders, ";")
canonicalRequest := req.httpMethod + "\n" + req.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
fmt.Printf("canonicalRequest========>\n%s\n", canonicalRequest)
// Step 2: Construct the string to sign
hashedCanonicalRequest := sha256Hex([]byte(canonicalRequest))
stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
fmt.Printf("stringToSign========>\n%s\n", stringToSign)
// Step 3: Calculate the signature
byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
if err != nil {
fmt.Println(err)
panic(err)
}
signature := strings.ToLower(hex.EncodeToString(byteData))
// Step 4: Construct the Authorization header
authorization := ALGORITHM + " Credential=" + AccessKeyId + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
req.headers["Authorization"] = authorization
}
func hmac256(key []byte, toSignString string) ([]byte, error) {
// Instantiate HMAC-SHA256 hash
h := hmac.New(sha256.New, key)
// Write the string to be signed
_, err := h.Write([]byte(toSignString))
if err != nil {
return nil, err
}
// Calculate and return the signature
return h.Sum(nil), nil
}
func sha256Hex(byteArray []byte) string {
// Instantiate SHA-256 hash function
hash := sha256.New()
// Write the string to the hash function
_, _ = hash.Write(byteArray)
// Calculate the SHA-256 hash value and convert it to a lowercase hexadecimal string
hexString := hex.EncodeToString(hash.Sum(nil))
return hexString
}
func percentCode(str string) string {
// Replace specific encoded characters
str = strings.ReplaceAll(str, "+", "%20")
str = strings.ReplaceAll(str, "*", "%2A")
str = strings.ReplaceAll(str, "%7E", "~")
return str
}
func formDataToString(formData map[string]interface{}) *string {
tmp := make(map[string]interface{})
processObject(tmp, "", formData)
res := ""
urlEncoder := url.Values{}
for key, value := range tmp {
v := fmt.Sprintf("%v", value)
urlEncoder.Add(key, v)
}
res = urlEncoder.Encode()
return &res
}
// processObject recursively processes an object, expanding complex objects (like Maps and Lists) into flat key-value pairs
func processObject(mapResult map[string]interface{}, key string, value interface{}) {
if value == nil {
return
}
switch v := value.(type) {
case []interface{}:
for i, item := range v {
processObject(mapResult, fmt.Sprintf("%s.%d", key, i+1), item)
}
case map[string]interface{}:
for subKey, subValue := range v {
processObject(mapResult, fmt.Sprintf("%s.%s", key, subKey), subValue)
}
default:
if strings.HasPrefix(key, ".") {
key = key[1:]
}
if b, ok := v.([]byte); ok {
mapResult[key] = string(b)
} else {
mapResult[key] = fmt.Sprintf("%v", v)
}
}
}
Node.js example
The example code runs in Node.js v20.13.1. You may need to adjust the code based on your specific situation.
This example uses Node.js.
const crypto = require('crypto');
const fs = require('fs');
class Request {
constructor(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion) {
this.httpMethod = httpMethod;
this.canonicalUri = canonicalUri || '/';
this.host = host;
this.xAcsAction = xAcsAction;
this.xAcsVersion = xAcsVersion;
this.headers = {};
this.body = null;
this.queryParam = {};
this.initHeader();
}
initHeader() {
const date = new Date();
this.headers = {
'host': this.host,
'x-acs-action': this.xAcsAction,
'x-acs-version': this.xAcsVersion,
'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
}
}
}
const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
const securityToken = process.env.ALIBABA_CLOUD_SECURITY_TOKEN;
const encoder = new TextEncoder()
if (!accessKeyId || !accessKeySecret) {
console.error('ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables must be set.');
process.exit(1);
}
function getAuthorization(signRequest) {
try {
newQueryParam = {};
processObject(newQueryParam, "", signRequest.queryParam);
signRequest.queryParam = newQueryParam;
// Step 1: Construct the canonical request string
const canonicalQueryString = Object.entries(signRequest.queryParam)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
.join('&');
// Request body. If the request body is empty, for example, in a GET request, RequestPayload is a fixed empty string.
const requestPayload = signRequest.body || encoder.encode('');
const hashedRequestPayload = sha256Hex(requestPayload);
signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;
if (securityToken) {
signRequest.headers['x-acs-security-token'] = securityToken;
}
// Convert all keys to lowercase
signRequest.headers = Object.fromEntries(
Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
);
const sortedKeys = Object.keys(signRequest.headers)
.filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
.sort();
// List of signed headers. Multiple request header names (lowercase) are sorted alphabetically and separated by semicolons (;).
const signedHeaders = sortedKeys.join(";")
// Construct request headers. Multiple canonical headers are concatenated after being sorted in ascending order by header name (lowercase).
const canonicalHeaders = sortedKeys.map(key => `${key}:${signRequest.headers[key]}`).join('\n') + '\n';
const canonicalRequest = [
signRequest.httpMethod,
signRequest.canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedRequestPayload
].join('\n');
console.log('canonicalRequest=========>\n', canonicalRequest);
// Step 2: Construct the string to sign
const hashedCanonicalRequest = sha256Hex(encoder.encode(canonicalRequest));
const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
console.log('stringToSign=========>', stringToSign);
// Step 3: Calculate the signature
const signature = hmac256(accessKeySecret, stringToSign);
console.log('signature=========>', signature);
// Step 4: Construct the Authorization header
const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
console.log('authorization=========>', authorization);
signRequest.headers['Authorization'] = authorization;
} catch (error) {
console.error('Failed to get authorization');
console.error(error);
}
}
async function callApi(signRequest) {
try {
let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
// Add request parameters
if (signRequest.queryParam) {
const query = new URLSearchParams(signRequest.queryParam);
url += '?' + query.toString();
}
console.log('url=========>', url);
// Configure request options
let options = {
method: signRequest.httpMethod.toUpperCase(),
headers: signRequest.headers
};
// Handle request body
if (signRequest.body && ['POST', 'PUT'].includes(signRequest.httpMethod.toUpperCase())) {
options.body = signRequest.body;
}
return (await fetch(url, options)).text();
} catch (error) {
console.error('Failed to send request:', error);
}
}
function percentCode(str) {
return encodeURIComponent(str)
.replace(/\+/g, '%20')
.replace(/\*/g, '%2A')
.replace(/~/g, '%7E');
}
function hmac256(key, data) {
const hmac = crypto.createHmac('sha256', key);
hmac.update(data, 'utf8');
return hmac.digest('hex').toLowerCase();
}
function sha256Hex(bytes) {
const hash = crypto.createHash('sha256');
const digest = hash.update(bytes).digest('hex');
return digest.toLowerCase();
}
function formDataToString(formData) {
const tmp = {};
processObject(tmp, "", formData);
let queryString = '';
for (let [key, value] of Object.entries(tmp)) {
if (queryString !== '') {
queryString += '&';
}
queryString += encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
return queryString;
}
function processObject(map, key, value) {
// If the value is null, no further processing is needed
if (value === null) {
return;
}
if (key === null) {
key = "";
}
// When the value is an Array, iterate through each element and process recursively
if (Array.isArray(value)) {
value.forEach((item, index) => {
processObject(map, `${key}.${index + 1}`, item);
});
} else if (typeof value === 'object' && value !== null) {
// When the value is an Object, iterate through each key-value pair and process recursively
Object.entries(value).forEach(([subKey, subValue]) => {
processObject(map, `${key}.${subKey}`, subValue);
});
} else {
// For keys starting with ".", remove the leading "." to maintain key continuity
if (key.startsWith('.')) {
key = key.slice(1);
}
map[key] = String(value);
}
}
/**
* Signature example. Replace the example parameters in the main method as needed.
* The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
*
* Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
* 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
* 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
* 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
// RPC API request example 1: Request parameters are in "query"
const httpMethod = 'POST'; // Request method. Most RPC APIs support both POST and GET. This example uses POST.
const canonicalUri = '/'; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
const host = 'ecs.cn-hangzhou.aliyuncs.com'; // Endpoint
const xAcsAction = 'DescribeInstanceStatus'; // API name
const xAcsVersion = '2014-05-26'; // API version number
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// DescribeInstanceStatus request parameters:
signRequest.queryParam = {
// RegionId is of type String in the metadata, "in":"query", required
RegionId: 'cn-hangzhou',
// InstanceId is of type array in the metadata, "in":"query", not required
InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}
// // RPC API request example 2: Request parameters are in "body" (file upload scenario)
// const httpMethod = 'POST';
// const canonicalUri = '/';
// const host = 'ocr-api.cn-hangzhou.aliyuncs.com';
// const xAcsAction = 'RecognizeGeneral';
// const xAcsVersion = '2021-07-07';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// const filePath = 'D:\\test.png';
// const bytes = fs.readFileSync(filePath);
// // Request parameters are shown as "in": "body" in the metadata, which means the parameters are in the body.
// signRequest.body = bytes;
// signRequest.headers['content-type'] = 'application/octet-stream';
// // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
// const httpMethod = 'POST'; // Request method. Most RPC APIs support both POST and GET. This example uses POST.
// const canonicalUri = '/'; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
// const host = 'mt.aliyuncs.com'; // Endpoint
// const xAcsAction = 'TranslateGeneral'; // API name
// const xAcsVersion = '2018-10-12'; // API version number
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// // TranslateGeneral request parameters:
// // Context is of type String in the metadata, "in":"query", not required
// signRequest.queryParam["Context"] = "Morning";
// // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
// const formData = {
// SourceLanguage: "zh",
// TargetLanguage: "en",
// FormatType: "text",
// Scene: "general",
// SourceText: 'Hello'
// }
// const str = formDataToString(formData)
// signRequest.body = encoder.encode(str);
// signRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
// // ROA API POST request
// const httpMethod = 'POST';
// const canonicalUri = '/clusters';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'CreateCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// // Request parameters are shown as "in": "body" in the metadata, which means the parameters are in the body.
// const body = {
// name: 'testDemo',
// region_id: 'cn-beijing',
// cluster_type: 'ExternalKubernetes',
// vpcid: 'vpc-2zeou1uod4ylaf35teei9',
// container_cidr: '10.0.0.0/8',
// service_cidr: '172.16.3.0/20',
// security_group_id: 'sg-2ze1a0rlgeo7dj37dd1q',
// vswitch_ids: [
// 'vsw-2zei30dhfldu8ytmtarro'
// ],
// }
// signRequest.body = encoder.encode(JSON.stringify(body));
// signRequest.headers['content-type'] = 'application/json';
// // ROA API GET request
// const httpMethod = 'GET';
// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96") + '/resources';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DescribeClusterResources';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// signRequest.queryParam = {
// with_addon_resources: true,
// }
// // ROA API DELETE request
// const httpMethod = 'DELETE';
// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96");
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DeleteCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
getAuthorization(signRequest);
// Call the API
callApi(signRequest).then(r => {
console.log(r);
}).catch(error => {
console.error(error);
});
PHP example
The example code runs in PHP 7.4.33. You may need to adjust the code based on your specific situation.
<?php
class SignatureDemo
{
// Encryption algorithm
private $ALGORITHM;
// Access Key ID
private $AccessKeyId;
// Access Key Secret
private $AccessKeySecret;
private $SecurityToken;
public function __construct()
{
date_default_timezone_set('UTC'); // Set timezone to GMT
$this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // getenv() gets the Access Key ID of the RAM user from an environment variable
$this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // getenv() gets the Access Key Secret of the RAM user from an environment variable
$this->SecurityToken = getenv('ALIBABA_CLOUD_SECURITY_TOKEN');
$this->ALGORITHM = 'ACS3-HMAC-SHA256'; // Set encryption algorithm
}
/**
* Signature example. Replace the example parameters in the main method as needed.
* The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
*
* Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
* 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
* 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
* 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
public function main()
{
// RPC API request example 1: Request parameters are in "query"
$request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
// DescribeInstanceStatus request parameters:
$request['queryParam'] = [
// RegionId is of type String in the metadata, "in":"query", required
'RegionId' => 'cn-hangzhou',
// InstanceId is of type array in the metadata, "in":"query", not required
'InstanceId' => ["i-bp11ht4h2kdXXXXXXXX", "i-bp16maz3h3xgXXXXXXXX", "i-bp10r67hmslXXXXXXXX"]
];
// // RPC API request example 2: Request parameters are in "body" (file upload scenario)
// $request = $this->createRequest('POST', '/', 'ocr-api.cn-hangzhou.aliyuncs.com', 'RecognizeGeneral', '2021-07-07');
// // Request parameters are shown as "in": "body" in the metadata, passed through the body.
// $filePath = 'D:\\test.png';
// // Use a file resource to pass the binary file
// $fileResource = fopen($filePath, 'rb');
// $request['body'] = stream_get_contents($fileResource);
// $request['headers']['content-type'] = 'application/octet-stream'; // Set Content-Type to application/octet-stream
// // Close the file resource
// fclose($fileResource);
// // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
// $request = $this->createRequest('POST', '/', 'mt.aliyuncs.com', 'TranslateGeneral', '2018-10-12');
// // TranslateGeneral request parameters:
// $request['queryParam'] = [
// // Context is of type String in the metadata, "in":"query", not required
// 'Context' => 'Morning',
// ];
// $formData = [
// 'FormatType' => 'text',
// 'SourceLanguage' => 'zh',
// 'TargetLanguage' => 'en',
// 'SourceText' => 'Hello',
// 'Scene' => 'general',
// ];
// $str = self::formDataToString($formData);
// $request['body'] = $str;
// $request['headers']['content-type'] = 'application/x-www-form-urlencoded';
// // ROA API POST request
// $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
// $bodyData = [
// 'name' => 'Test Cluster',
// 'region_id' => 'cn-beijing',
// 'cluster_type' => 'ExternalKubernetes',
// 'vpcid' => 'vpc-2zeou1uod4ylaXXXXXXXX',
// 'service_cidr' => '10.2.0.0/24',
// 'security_group_id' => 'sg-2ze1a0rlgeo7XXXXXXXX',
// "vswitch_ids" => [
// "vsw-2zei30dhfldu8XXXXXXXX"
// ]
// ];
// $request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
// $request['headers']['content-type'] = 'application/json; charset=utf-8';
// // ROA API GET request
// // If canonicalUri has a path parameter, encode the path parameter: rawurlencode({path_parameter})
// $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
// $canonicalUri = sprintf("/clusters/%s/resources", rawurlencode($cluster_id));
// $request = $this->createRequest('GET', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DescribeClusterResources', '2015-12-15');
// $request['queryParam'] = [
// 'with_addon_resources' => true,
// ];
// // ROA API DELETE request
// $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
// $canonicalUri = sprintf("/clusters/%s", rawurlencode($cluster_id));
// $request = $this->createRequest('DELETE', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DeleteCluster', '2015-12-15');
$this->getAuthorization($request);
// Call the API
$this->callApi($request);
}
private function createRequest($httpMethod, $canonicalUri, $host, $xAcsAction, $xAcsVersion)
{
$headers = [
'host' => $host,
'x-acs-action' => $xAcsAction,
'x-acs-version' => $xAcsVersion,
'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'),
'x-acs-signature-nonce' => bin2hex(random_bytes(16)),
];
return [
'httpMethod' => $httpMethod,
'canonicalUri' => $canonicalUri,
'host' => $host,
'headers' => $headers,
'queryParam' => [],
'body' => null,
];
}
private function getAuthorization(&$request)
{
$request['queryParam'] = $this->processObject($request['queryParam']);
$canonicalQueryString = $this->buildCanonicalQueryString($request['queryParam']);
$hashedRequestPayload = hash('sha256', $request['body'] ?? '');
$request['headers']['x-acs-content-sha256'] = $hashedRequestPayload;
if($this->SecurityToken){
$request['headers']['x-acs-security-token'] = $this->SecurityToken;
}
$canonicalHeaders = $this->buildCanonicalHeaders($request['headers']);
$signedHeaders = $this->buildSignedHeaders($request['headers']);
$canonicalRequest = implode("\n", [
$request['httpMethod'],
$request['canonicalUri'],
$canonicalQueryString,
$canonicalHeaders,
$signedHeaders,
$hashedRequestPayload,
]);
$hashedCanonicalRequest = hash('sha256', $canonicalRequest);
$stringToSign = "{$this->ALGORITHM}\n$hashedCanonicalRequest";
$signature = strtolower(bin2hex(hash_hmac('sha256', $stringToSign, $this->AccessKeySecret, true)));
$authorization = "{$this->ALGORITHM} Credential={$this->AccessKeyId},SignedHeaders=$signedHeaders,Signature=$signature";
$request['headers']['Authorization'] = $authorization;
}
private function callApi($request)
{
try {
// Send request via cURL
$url = "https://" . $request['host'] . $request['canonicalUri'];
// Add request parameters to the URL
if (!empty($request['queryParam'])) {
$url .= '?' . http_build_query($request['queryParam']);
}
echo $url;
// Initialize cURL session
$ch = curl_init();
// Set cURL options
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL certificate verification. Note that this reduces security and should not be used in a production environment. (Not recommended!!!)
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return content instead of outputting it
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // Add request headers
// Set cURL options based on request type
switch ($request['httpMethod']) {
case "GET":
break;
case "POST":
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
break;
case "DELETE":
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
break;
default:
echo "Unsupported HTTP method: " . $request['body'];
throw new Exception("Unsupported HTTP method");
}
// Send the request
$result = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
echo "Failed to send request: " . curl_error($ch);
} else {
echo $result;
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
} finally {
// Close cURL session
curl_close($ch);
}
}
function formDataToString($formData)
{
$res = self::processObject($formData);
return http_build_query($res);
}
function processObject($value)
{
// If the value is null, no further processing is needed
if ($value === null) {
return;
}
$tmp = [];
foreach ($value as $k => $v) {
if (0 !== strpos($k, '_')) {
$tmp[$k] = $v;
}
}
return self::flatten($tmp);
}
private static function flatten($items = [], $delimiter = '.', $prepend = '')
{
$flatten = [];
foreach ($items as $key => $value) {
$pos = \is_int($key) ? $key + 1 : $key;
if (\is_object($value)) {
$value = get_object_vars($value);
}
if (\is_array($value) && !empty($value)) {
$flatten = array_merge(
$flatten,
self::flatten($value, $delimiter, $prepend . $pos . $delimiter)
);
} else {
if (\is_bool($value)) {
$value = true === $value ? 'true' : 'false';
}
$flatten["$prepend$pos"] = $value;
}
}
return $flatten;
}
private function convertHeadersToArray($headers)
{
$headerArray = [];
foreach ($headers as $key => $value) {
$headerArray[] = "$key: $value";
}
return $headerArray;
}
private function buildCanonicalQueryString($queryParams)
{
ksort($queryParams);
// Build and encode query parameters
$params = [];
foreach ($queryParams as $k => $v) {
if (null === $v) {
continue;
}
$str = rawurlencode($k);
if ('' !== $v && null !== $v) {
$str .= '=' . rawurlencode($v);
} else {
$str .= '=';
}
$params[] = $str;
}
return implode('&', $params);
}
private function buildCanonicalHeaders($headers)
{
// Sort headers by key and concatenate them
uksort($headers, 'strcasecmp');
$canonicalHeaders = '';
foreach ($headers as $key => $value) {
$canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n";
}
return $canonicalHeaders;
}
private function buildSignedHeaders($headers)
{
// Build the signed headers string
$signedHeaders = array_keys($headers);
sort($signedHeaders, SORT_STRING | SORT_FLAG_CASE);
return implode(';', array_map('strtolower', $signedHeaders));
}
}
$demo = new SignatureDemo();
$demo->main();
.NET example
The example code runs in .NET 8.0.302. You may need to adjust the code based on your specific situation.
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace SignatureDemo
{
public class Request
{
public string HttpMethod { get; private set; }
public string CanonicalUri { get; private set; }
public string Host { get; private set; }
public string XAcsAction { get; private set; }
public string XAcsVersion { get; private set; }
public SortedDictionary<string, object> Headers { get; private set; }
public byte[]? Body { get; set; }
public Dictionary<string, object> QueryParam { get; set; }
public Request(string httpMethod, string canonicalUri, string host, string xAcsAction, string xAcsVersion)
{
HttpMethod = httpMethod;
CanonicalUri = canonicalUri;
Host = host;
XAcsAction = xAcsAction;
XAcsVersion = xAcsVersion;
Headers = [];
QueryParam = [];
Body = null;
InitHeader();
}
private void InitHeader()
{
Headers["host"] = Host;
Headers["x-acs-action"] = XAcsAction;
Headers["x-acs-version"] = XAcsVersion;
DateTime utcNow = DateTime.UtcNow;
Headers["x-acs-date"] = utcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
Headers["x-acs-signature-nonce"] = Guid.NewGuid().ToString();
}
}
public class Program
{
private static readonly string AccessKeyId = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID") ?? throw new InvalidOperationException("The ALIBABA_CLOUD_ACCESS_KEY_ID environment variable is not set");
private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ?? throw new InvalidOperationException("The ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variable is not set");
private static readonly string? SecurityToken = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_SECURITY_TOKEN");
private const string Algorithm = "ACS3-HMAC-SHA256";
private const string ContentType = "content-type";
/**
* Signature example. Replace the example parameters in the main method as needed.
* The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
*
* Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
* 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
* 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
* 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
public static void Main(string[] args)
{
// RPC API request example 1: Request parameters are in "query"
string httpMethod = "POST"; // Request method. Most RPC APIs support both POST and GET. This example uses POST.
string canonicalUri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
string host = "ecs.cn-hangzhou.aliyuncs.com"; // Cloud product endpoint
string xAcsAction = "DescribeInstanceStatus"; // API name
string xAcsVersion = "2014-05-26"; // API version number
var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// DescribeInstanceStatus request parameters:
// RegionId is of type String in the metadata, "in":"query", required
request.QueryParam["RegionId"] = "cn-hangzhou";
// InstanceId is of type array in the metadata, "in":"query", not required
List<string> instanceIds = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"];
request.QueryParam["InstanceId"] = instanceIds;
// // RPC API request example 2: Request parameters are in "body" (file upload scenario)
// string httpMethod = "POST";
// string canonicalUri = "/";
// string host = "ocr-api.cn-hangzhou.aliyuncs.com";
// string xAcsAction = "RecognizeGeneral";
// string xAcsVersion = "2021-07-07";
// var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // Request parameters are shown as "in": "body" in the metadata, passed through the body.
// request.Body = File.ReadAllBytes(@"D:\test.png");
// request.Headers["content-type"] = "application/octet-stream";
// // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
// string httpMethod = "POST";
// string canonicalUri = "/";
// string host = "mt.aliyuncs.com";
// string xAcsAction = "TranslateGeneral";
// string xAcsVersion = "2018-10-12";
// var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // TranslateGeneral request parameters:
// // Context is of type String in the metadata, "in":"query", not required
// request.QueryParam["Context"] = "Morning";
// // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
// var body = new Dictionary<string, object>
// {
// { "FormatType", "text" },
// { "SourceLanguage", "zh" },
// { "TargetLanguage", "en" },
// { "SourceText", "Hello" },
// { "Scene", "general" },
// };
// var str = FormDataToString(body);
// request.Body = Encoding.UTF8.GetBytes(str);
// request.Headers[ContentType] = "application/x-www-form-urlencoded";
// // ROA API POST request
// String httpMethod = "POST";
// String canonicalUri = "/clusters";
// String host = "cs.cn-beijing.aliyuncs.com";
// String xAcsAction = "CreateCluster";
// String xAcsVersion = "2015-12-15";
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // Request body. Convert the body to a JSON string using JsonConvert.
// var body = new SortedDictionary<string, object>
// {
// { "name", "testDemo" },
// { "region_id", "cn-beijing" },
// { "cluster_type", "ExternalKubernetes" },
// { "vpcid", "vpc-2zeou1uod4ylaXXXXXXXX" },
// { "container_cidr", "10.0.0.0/8" },
// { "service_cidr", "172.16.1.0/20" },
// { "security_group_id", "sg-2ze1a0rlgeo7XXXXXXXX" },
// { "vswitch_ids", new List<string>{"vsw-2zei30dhfldu8XXXXXXXX"} },
// };
// string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
// request.Body = Encoding.UTF8.GetBytes(jsonBody);
// request.Headers[ContentType] = "application/json; charset=utf-8";
// // ROA API GET request
// String httpMethod = "GET";
// // If canonicalUri has a path parameter, encode the path parameter: PercentCode({path_parameter})
// String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX") + "/resources";
// String host = "cs.cn-beijing.aliyuncs.com";
// String xAcsAction = "DescribeClusterResources";
// String xAcsVersion = "2015-12-15";
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// request.QueryParam["with_addon_resources"]=true;
// // ROA API DELETE request
// String httpMethod = "DELETE";
// // If canonicalUri has a path parameter, encode the path parameter: PercentCode({path_parameter})
// String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX");
// String host = "cs.cn-beijing.aliyuncs.com";
// String xAcsAction = "DeleteCluster";
// String xAcsVersion = "2015-12-15";
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
GetAuthorization(request);
// Call the API
var result = CallApiAsync(request);
Console.WriteLine($"result:{result.Result}");
}
private static async Task<string?> CallApiAsync(Request request)
{
try
{
// Declare httpClient
using var httpClient = new HttpClient();
// Build the URL
string url = $"https://{request.Host}{request.CanonicalUri}";
var uriBuilder = new UriBuilder(url);
var query = new List<string>();
// Add request parameters
foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
{
string value = entry.Value?.ToString() ?? "";
query.Add($"{entry.Key}={Uri.EscapeDataString(value)}");
}
uriBuilder.Query = string.Join("&", query);
Console.WriteLine(uriBuilder.Uri);
var requestMessage = new HttpRequestMessage
{
Method = new HttpMethod(request.HttpMethod),
RequestUri = uriBuilder.Uri,
};
// Set request headers
foreach (var entry in request.Headers)
{
if (entry.Key == "Authorization")
{
requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ;
}
else if (entry.Key == ContentType) // Must be consistent with the definition in main
{
continue;
}
else
{
requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
}
}
if (request.Body != null)
{
HttpContent content = new ByteArrayContent(request.Body);
string contentType = request.Headers["content-type"].ToString();
content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
requestMessage.Content = content;
}
// Send the request
HttpResponseMessage response = await httpClient.SendAsync(requestMessage);
// Read the response content
string result = await response.Content.ReadAsStringAsync();
return result;
}
catch (UriFormatException e)
{
Console.WriteLine("Invalid URI syntax");
Console.WriteLine(e.Message);
return null;
}
catch (Exception e)
{
Console.WriteLine("Failed to send request");
Console.WriteLine(e);
return null;
}
}
private static void GetAuthorization(Request request)
{
try
{
// Process parameters of List and Map types in queryParam, and flatten the parameters
request.QueryParam = FlattenDictionary(request.QueryParam);
// Step 1: Construct the canonical request string
StringBuilder canonicalQueryString = new();
foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
{
if (canonicalQueryString.Length > 0)
{
canonicalQueryString.Append('&');
}
canonicalQueryString.Append($"{PercentCode(entry.Key)}={PercentCode(entry.Value?.ToString() ?? "")}");
}
byte[] requestPayload = request.Body ?? Encoding.UTF8.GetBytes("");
string hashedRequestPayload = Sha256Hash(requestPayload);
request.Headers["x-acs-content-sha256"] = hashedRequestPayload;
if (!string.IsNullOrEmpty(SecurityToken))
{
request.Headers["x-acs-security-token"] = SecurityToken;
}
StringBuilder canonicalHeaders = new();
StringBuilder signedHeadersSb = new();
foreach (var entry in request.Headers.OrderBy(e => e.Key.ToLower()))
{
if (entry.Key.StartsWith("x-acs-", StringComparison.CurrentCultureIgnoreCase) || entry.Key.Equals("host", StringComparison.OrdinalIgnoreCase) || entry.Key.Equals(ContentType, StringComparison.OrdinalIgnoreCase))
{
string lowerKey = entry.Key.ToLower();
string value = (entry.Value?.ToString() ?? "").Trim();
canonicalHeaders.Append($"{lowerKey}:{value}\n");
signedHeadersSb.Append($"{lowerKey};");
}
}
string signedHeaders = signedHeadersSb.ToString().TrimEnd(';');
string canonicalRequest = $"{request.HttpMethod}\n{request.CanonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
Console.WriteLine($"canonicalRequest:{canonicalRequest}");
// Step 2: Construct the string to sign
string hashedCanonicalRequest = Sha256Hash(Encoding.UTF8.GetBytes(canonicalRequest));
string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
Console.WriteLine($"stringToSign:{stringToSign}");
// Step 3: Calculate the signature
string signature = HmacSha256(AccessKeySecret, stringToSign);
// Step 4: Construct the Authorization header
string authorization = $"{Algorithm} Credential={AccessKeyId},SignedHeaders={signedHeaders},Signature={signature}";
request.Headers["Authorization"] = authorization;
Console.WriteLine($"authorization:{authorization}");
}
catch (Exception ex)
{
Console.WriteLine("Failed to get authorization");
Console.WriteLine(ex.Message);
}
}
private static string FormDataToString(Dictionary<string, object> formData)
{
Dictionary<string, object> tileMap = FlattenDictionary( formData);
StringBuilder result = new StringBuilder();
bool first = true;
string symbol = "&";
foreach (var entry in tileMap)
{
string value = entry.Value?.ToString() ?? "";
if (!string.IsNullOrEmpty(value))
{
if (!first)
{
result.Append(symbol);
}
first = false;
result.Append(PercentCode(entry.Key));
result.Append("=");
result.Append(PercentCode(value));
}
}
return result.ToString();
}
private static Dictionary<string, object> FlattenDictionary(Dictionary<string, object> dictionary, string prefix = "")
{
var result = new Dictionary<string, object>();
foreach (var kvp in dictionary)
{
string key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";
if (kvp.Value is Dictionary<string, object> nestedDict)
{
var nestedResult = FlattenDictionary(nestedDict, key);
foreach (var nestedKvp in nestedResult)
{
result[nestedKvp.Key] = nestedKvp.Value;
}
}
else if (kvp.Value is List<string> list)
{
for (int i = 0; i < list.Count; i++)
{
result[$"{key}.{i + 1}"] = list[i];
}
}
else
{
result[key] = kvp.Value;
}
}
return result;
}
private static string HmacSha256(string key, string message)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
{
byte[] hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
}
}
private static string Sha256Hash(byte[] input)
{
byte[] hashBytes = SHA256.HashData(input);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
private static string PercentCode(string str)
{
if (string.IsNullOrEmpty(str))
{
throw new ArgumentException("Input string cannot be null or empty");
}
return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
}
}
}
Rust example
The example code runs in rustc 1.82.0. You may need to adjust the code based on your specific situation.
To run the Rust example, add the following dependencies to your Cargo.toml file.
[dependencies]
serde = { version = "1.0" }
serde_json = "1.0"
rand = "0.8"
base64 = "0.21"
sha2 = "0.10"
chrono = "0.4"
hmac = "0.12"
hex = "0.4"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
percent-encoding = "2.1"
use core::str;
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::time::{SystemTime, SystemTimeError};
use chrono::DateTime;
use hmac::{Hmac, Mac};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use rand::Rng;
use serde_json::{json, Value};
use std::borrow::Cow;
use reqwest::{
Client,
header::{HeaderMap, HeaderValue}, Method, Response, StatusCode,
};
use sha2::{Digest, Sha256};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
// Generate x-acs-date
pub fn current_timestamp() -> Result<u64, SystemTimeError> {
Ok(SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs())
}
// URL encoding
pub fn percent_code(encode_str: &str) -> Cow<'_, str> {
let encoded = utf8_percent_encode(encode_str, NON_ALPHANUMERIC)
.to_string()
.replace("+", "20%")
.replace("%5F", "_")
.replace("%2D", "-")
.replace("%2E", ".")
.replace("%7E", "~");
Cow::Owned(encoded) // Returns a Cow<str> that can hold a String or &str
}
fn flatten_target_ops(
targets: Vec<HashMap<&str, &str>>,
base_key: &str,
) -> Vec<(&'static str, &'static str)> {
let mut result = Vec::new();
for (idx, item) in targets.iter().enumerate() {
let prefix = format!("{}.{}", base_key, idx + 1);
for (&k, &v) in item {
let key = format!("{}.{}", prefix, k);
let key_static: &'static str = Box::leak(key.into_boxed_str());
let value_static: &'static str = Box::leak(v.to_string().into_boxed_str());
result.push((key_static, value_static));
}
}
result
}
/// Calculate SHA-256 hash
pub fn sha256_hex(message: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(message);
format!("{:x}", hasher.finalize()).to_lowercase()
}
// HMAC-SHA256
pub fn hmac256(key: &[u8], message: &str) -> Result<Vec<u8>, String> {
let mut mac = Hmac::<Sha256>::new_from_slice(key)
.map_err(|e| format!("use data key on sha256 fail:{}", e))?;
mac.update(message.as_bytes());
let signature = mac.finalize();
Ok(signature.into_bytes().to_vec())
}
// Generate a unique random number for the signature
pub fn generate_random_string(length: usize) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut rng = rand::thread_rng();
(0..length)
.map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char)
.collect()
}
pub fn generate_nonce() -> String {
generate_random_string(32)
}
// Construct a canonical query string (encoded)
pub fn build_sored_encoded_query_string(query_params: &[(&str, &str)]) -> String {
let sorted_query_params: BTreeMap<_, _> = query_params.iter().copied().collect();
let encoded_params: Vec<String> = sorted_query_params
.into_iter()
.map(|(k, v)| {
let encoded_key = percent_code(k);
let encoded_value = percent_code(v);
format!("{}={}", encoded_key, encoded_value)
})
.collect();
encoded_params.join("&")
}
// Read the response
pub async fn read_response(result: Response) -> Result<(StatusCode, String), String> {
let status = result.status();
let data = result.bytes().await.map_err(|e| format!("Read response body failed: {}", e))?;
let res = match str::from_utf8(&data) {
Ok(s) => s.to_string(),
Err(_) => return Err("Body contains non UTF-8 characters".to_string()),
};
Ok((status, res))
}
// Define the value type for FormData
#[derive(Debug, Clone)]
pub enum FormValue {
String(String),
Vec(Vec<String>),
HashMap(HashMap<String, String>),
}
// Define a request body enumeration to uniformly handle request body types, including Json, Binary, and FormData.
pub enum RequestBody {
Json(HashMap<String, Value>), // Json
Binary(Vec<u8>), // Binary
FormData(HashMap<String, FormValue>), // FormData
None,
}
// Canonical request
pub async fn call_api(
client: Client,
method: Method,
host: &str,
canonical_uri: &str,
query_params: &[(&str, &str)],
action: &str,
version: &str,
body: RequestBody,
access_key_id: &str,
access_key_secret: &str,
) -> Result<String, String> {
// Process the request body content based on the body type and store the processed content in the body_content variable.
let body_content = match &body {
RequestBody::Json(body_map) => json!(body_map).to_string(),
RequestBody::Binary(binary_data) => {
STANDARD.encode(binary_data)
},
RequestBody::FormData(form_data) => {
let params: Vec<String> = form_data
.iter()
.flat_map(|(k, v)| {
match v {
FormValue::String(s) => {
vec![format!("{}={}", percent_code(k), percent_code(&s))]
},
FormValue::Vec(vec) => {
vec.iter()
.map(|s| format!("{}={}", percent_code(k), percent_code(s)))
.collect::<Vec<_>>()
},
FormValue::HashMap(map) => {
map.iter()
.map(|(sk, sv)| format!("{}={}", percent_code(sk), percent_code(sv)))
.collect::<Vec<_>>()
},
}
})
.collect();
params.join("&")
},
RequestBody::None => String::new(),
};
// Calculate x-acs-content-sha256 for the request body; prepare x-acs-date, x-acs-signature-nonce, and the request headers to be signed.
let hashed_request_payload = if body_content.is_empty() {
sha256_hex("")
} else {
sha256_hex(&body_content)
};
// x-acs-date
let now_time = current_timestamp().map_err(|e| format!("Get current timestamp failed: {}", e))?;
let datetime = DateTime::from_timestamp(now_time as i64, 0).ok_or_else(|| format!("Get datetime from timestamp failed: {}", now_time))?;
let datetime_str = datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string();
// x-acs-signature-nonce
let signature_nonce = generate_nonce();
println!("Signature Nonce: {}", signature_nonce);
// Request headers to be signed
let sign_header_arr = &[
"host",
"x-acs-action",
"x-acs-content-sha256",
"x-acs-date",
"x-acs-signature-nonce",
"x-acs-version",
];
let sign_headers = sign_header_arr.join(";");
// 1. Construct canonical request headers
let mut headers = HeaderMap::new();
headers.insert("Host", HeaderValue::from_str(host).unwrap());
headers.insert("x-acs-action", HeaderValue::from_str(action).unwrap());
headers.insert("x-acs-version", HeaderValue::from_str(version).unwrap());
headers.insert("x-acs-date", HeaderValue::from_str(&datetime_str).unwrap());
headers.insert("x-acs-signature-nonce", HeaderValue::from_str(&signature_nonce).unwrap());
headers.insert("x-acs-content-sha256", HeaderValue::from_str(&hashed_request_payload).unwrap());
// 2. Construct the request headers to be signed
let canonical_query_string = build_sored_encoded_query_string(query_params); // Encode and concatenate parameters
println!("CanonicalQueryString: {}", canonical_query_string);
let canonical_request = format!(
"{}\n{}\n{}\n{}\n\n{}\n{}",
method.as_str(),
canonical_uri,
canonical_query_string,
sign_header_arr.iter().map(|&header| format!("{}:{}", header, headers[header].to_str().unwrap())).collect::<Vec<_>>().join("\n"),
sign_headers,
hashed_request_payload
);
println!("Canonical Request: {}", canonical_request);
// 3. Calculate the SHA-256 hash of the request headers to be signed;
let result = sha256_hex(&canonical_request);
// 4. Construct the string to sign
let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", result);
// 5. Calculate the signature
let signature = hmac256(access_key_secret.as_bytes(), &string_to_sign)?;
let data_sign = hex::encode(&signature);
let auth_data = format!(
"ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}",
access_key_id, sign_headers, data_sign
);
// 6. Construct the Authorization header
headers.insert("Authorization", HeaderValue::from_str(&auth_data).unwrap());
// Construct the URL and concatenate request parameters
let url: String;
if !query_params.is_empty() {
url = format!("https://{}{}?{}", host, canonical_uri,canonical_query_string);
} else {
url = format!("https://{}{}", host, canonical_uri);
}
// Call to send the request
let response = send_request(
&client,
method,
&url,
headers,
query_params,
&body,
&body_content,
)
.await?;
// Read the response
let (_, res) = read_response(response).await?;
Ok(res)
}
/// Send the request
async fn send_request(
client: &Client,
method: Method,
url: &str,
headers: HeaderMap,
query_params: &[(&str, &str)], // Receive query parameters
body: &RequestBody, // Used to determine the body data type
body_content: &str, // Receive body request parameters (FormData/Json/Binary) when body is not empty
) -> Result<Response, String> {
let mut request_builder = client.request(method.clone(), url);
// Add request headers
for (k, v) in headers.iter() {
request_builder = request_builder.header(k, v.clone());
}
// Add request body
match body {
RequestBody::Binary(_) => {
request_builder = request_builder.header("Content-Type", "application/octet-stream");
request_builder = request_builder.body(body_content.to_string()); // Move the value here
}
RequestBody::Json(_) => {
// If the body is a map and not empty, convert it to JSON, store it in the body_content variable, and set application/json; charset=utf-8
if !body_content.is_empty() {
request_builder = request_builder.body(body_content.to_string());
request_builder = request_builder.header("Content-Type", "application/json; charset=utf-8");
}
}
RequestBody::FormData(_) => {
// Handle form-data type, set content-type
if !body_content.is_empty() {
request_builder = request_builder.header("Content-Type", "application/x-www-form-urlencoded");
request_builder = request_builder.body(body_content.to_string());
}
}
RequestBody::None => {
request_builder = request_builder.body(String::new());
}
}
// Build the request
let request = request_builder
.build()
.map_err(|e| format!("build request fail: {}", e))?;
// Send the request
let response = client
.execute(request)
.await
.map_err(|e| format!("execute request fail: {}", e))?;
// Return the result
Ok(response)
}
/**
*
* Signature example. Replace the example parameters in the main method as needed.
* <p>
* Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata.
* 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using query_params. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
* 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
* 2. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
#[tokio::main]
async fn main() {
// Create an HTTP client
let client = Client::new();
// env::var() gets the Access Key ID and Access Key Secret from environment variables
let access_key_id = env::var("ALIBABA_CLOUD_ACCESS_KEY_ID").expect("Cannot get access key id.");
let access_key_secret = env::var("ALIBABA_CLOUD_ACCESS_KEY_SECRET").expect("Cannot get access key id.");
let access_key_id: &str = &access_key_id;
let access_key_secret: &str = &access_key_secret;
// RPC API request example 1: Request parameters are in "query" POST
let method = Method::POST; // Request method
let host = "ecs.cn-hangzhou.aliyuncs.com"; // Endpoint
let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
let action = "DescribeInstanceStatus"; // API name
let version = "2014-05-26"; // API version number
let region_id = "cn-hangzhou";
let instance_ids = vec![
"i-bp11ht4XXXXXXXX",
"i-bp16mazXXXXXXXX",
];
let mut query: Vec<(&str, &str)> = Vec::new();
query.push(("RegionId", region_id));
for (index, instance_id) in instance_ids.iter().enumerate() {
let key = format!("InstanceId.{}", index + 1);
query.push((Box::leak(key.into_boxed_str()), instance_id));
}
// Query parameters
let query_params: &[(&str, &str)] = &query;
// When the request body is empty
let body = RequestBody:: None;
// RPC API "in":"query" with complex query parameters POST
// let method = Method::POST; // Request method
// let host = "tds.cn-shanghai.aliyuncs.com"; // Endpoint
// let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
// let action = "AddAssetSelectionCriteria"; // API name
// let version = "2018-12-03"; // API version number
// Define parameters
// let mut target_op = HashMap::new();
// target_op.insert("Operation", "add");
// target_op.insert("Target", "i-2ze1j7ocdXXXXXXXX");
// Define parameter TargetOperationList, passing a map type into the collection
// let target_operation_list = vec![target_op];
// Flatten parameters
// let mut query = flatten_target_ops(target_operation_list, "TargetOperationList");
// Normal parameters
// query.push(("SelectionKey", "85a561b7-27d5-47ad-a0ec-XXXXXXXX"));
// let query_params: &[(&str, &str)] = &query;
// let body = RequestBody:: None;
// RPC API request example 2: Request parameters are in "body" (file upload scenario) POST
// let method = Method::POST; // Request method
// let host = "ocr-api.cn-hangzhou.aliyuncs.com";
// let canonical_uri = "/";
// let action = "RecognizeGeneral";
// let version = "2021-07-07";
// Request parameter "in":"body" binary file type
// let binary_data = std::fs::read("<FILE_PATH>").expect("Failed to read file"); // Replace <FILE_PATH> with the actual file path
// When body is of binary type
// let body = RequestBody::Binary(binary_data);
// Query parameters are empty
// let query_params = &[];
// RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario) POST
// let method = Method::POST; // Request method
// let host = "mt.aliyuncs.com";
// let canonical_uri = "/";
// let action = "TranslateGeneral";
// let version = "2018-10-12";
// // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
// let mut form_data = HashMap::new(); // body type is FormData(HashMap<String, FormValue>). FormValue supports Vec<String>, HashSet<String>, or HashMap<String, String>, etc. More types can be added in the FormValue enum.
// form_data.insert(String::from("FormatType"),FormValue::String(String::from("text")));
// form_data.insert(String::from("SourceLanguage"),FormValue::String(String::from("zh")));
// form_data.insert(String::from("TargetLanguage"),FormValue::String(String::from("en")));
// form_data.insert(String::from("SourceText"),FormValue::String(String::from("Hello")));
// form_data.insert(String::from("Scene"),FormValue::String(String::from("general")));
// // Query parameters
// let query_params = &[("Context", "Morning")];
// // When body is of FormData type, "in":"formdata"
// let body = RequestBody::FormData(form_data);
// ROA API POST request API: CreateCluster
// Define API request constants
// let method = Method::POST; // Request method
// let host = "cs.cn-hangzhou.aliyuncs.com";
// let canonical_uri = "/clusters";
// let action = "CreateCluster";
// let version = "2015-12-15";
// // Set request body parameters
// let mut body_json = HashMap::new(); // body type is Json(HashMap<String, Value>). Value supports types: Value::String("test".to_string()) // String, Value::Number(serde_json::Number::from(42)) // Number, Value::Bool(true) // Boolean, Value::Null // Null, Value::Array(vec![Value::from(1), Value::from(2), Value::from(3)]) //Array, json!({"nested_key": "nested_value"})
// body_json.insert(String::from("name"),json!("Test Cluster"));
// body_json.insert(String::from("region_id"),json!("cn-hangzhou"));
// body_json.insert(String::from("cluster_type"),json!("ExternalKubernetes"));
// body_json.insert(String::from("vpcid"),json!("vpc-2zeou1uodXXXXXXXX"));
// body_json.insert(String::from("container_cidr"),json!("10.X.X.X/X"));
// body_json.insert(String::from("service_cidr"),json!("10.X.X.X/X"));
// body_json.insert(String::from("security_group_id"),json!("sg-2ze1a0rlgXXXXXXXX"));
// body_json.insert(
// String::from("vswitch_ids"),
// Value::Array(vec![
// Value::from("vsw-2zei30dhflXXXXXXXX"),
// Value::from("vsw-2zei30dhflXXXXXXXX"),
// Value::from("vsw-2zei30dhflXXXXXXXX"),
// ]),
// );
// // Query parameters are empty
// let query_params = &[];
// // When body is of Json type
// let body = RequestBody::Json(body_json);
// ROA API GET request API: DeleteCluster Query linked resources of a specified cluster
// let method = Method::GET; // Request method
// let host = "cs.cn-hangzhou.aliyuncs.com"; // Endpoint
// // Concatenate resource path
// let uri = format!("/clusters/{}/resources", percent_code("ce196d21571a64be9XXXXXXXX").as_ref());
// let canonical_uri = uri.as_str(); // Resource path, converted to &str type
// let action = "DescribeClusterResources"; // API name
// let version = "2015-12-15"; // API version number
// // Set query parameters
// let query_params = &[("with_addon_resources", if true { "true" } else { "false" })]; // "true" or "false"
// // Set body parameter to empty
// let body = RequestBody:: None;
// ROA API DELETE request API: DeleteCluster DELETE request to delete a pay-as-you-go cluster
// let method = Method::DELETE;
// let host = "cs.cn-hangzhou.aliyuncs.com";
// let uri = format!("/clusters/{}", percent_code("ce0138ff31ad044f8XXXXXXXX").as_ref());
// let canonical_uri = uri.as_str(); // Resource path, converted to &str type
// let action = "DeleteCluster";
// let version = "2015-12-15";
// // Query parameters
// let query_params = &[];
// // When body parameter is empty
// let body = RequestBody:: None;
// SendSms API
// let method = Method::POST; // Request method
// let host = "dysmsapi.aliyuncs.com"; // Endpoint
// let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
// let action = "SendSms"; // API name
// let version = "2017-05-25"; // API version number
// let mut query: Vec<(&str, &str)> = Vec::new();
// query.push(("PhoneNumbers", "<YOUR_PHONENUMBERS>"));
// query.push(("TemplateCode", "<YOUR_TEMPLATECODE>"));
// query.push(("SignName", "<YOUR_SIGNNAME>"));
// query.push(("TemplateParam", "<YOUR_TEMPLATEPARAM>"));
// // Query parameters
// let query_params: &[(&str, &str)] = &query;
// // When the request body is empty
// let body = RequestBody:: None;
// Send the request
match call_api(
client.clone(),
method, // API request method POST/GET/DELETE
host, // API endpoint
canonical_uri, // API resource path
query_params, // "in":"query" query parameters
action, // API name
version, // API version number
body, // "in":"body" request body parameters, supporting Json/FormData/Binary types
access_key_id,
access_key_secret,
)
.await {
Ok(response) => println!("Response: {}", response),
Err(error) => eprintln!("Exception: {}", error),
}
}
Shell script example
#!/bin/bash
accessKey_id="<YOUR-ACCESSKEY-ID>"
accessKey_secret="<YOUR-ACCESSKEY-SECRET>"
algorithm="ACS3-HMAC-SHA256"
# Request parameters -- Modify this section as needed
httpMethod="POST"
host="dns.aliyuncs.com"
queryParam=("DomainName=example.com" "RRKeyWord=@")
action="DescribeDomainRecords"
version="2015-01-09"
canonicalURI="/"
# Pass body-type or formdata-type parameters through the body
# body-type parameter: The value of body is a JSON string: "{'key1':'value1','key2':'value2'}", and content-type:application/json; charset=utf-8 must be added to the signature header.
# When the body-type parameter is a binary file: body does not need to be modified, just add content-type:application/octet-stream to the signature header, and add the --data-binary parameter to curl_command.
# formdata-type parameter: The body parameter format is "key1=value1&key2=value2", and content-type:application/x-www-form-urlencoded must be added to the signature header.
body=""
# UTC time in ISO 8601 format
utc_timestamp=$(date +%s)
utc_date=$(date -u -d @${utc_timestamp} +"%Y-%m-%dT%H:%M:%SZ")
# x-acs-signature-nonce random number
random=$(uuidgen | sed 's/-//g')
# Signature header
headers="host:${host}
x-acs-action:${action}
x-acs-version:${version}
x-acs-date:${utc_date}
x-acs-signature-nonce:${random}"
# URL encoding function
urlencode() {
local string="${1}"
local strlen=${#string}
local encoded=""
local pos c o
for (( pos=0 ; pos<strlen ; pos++ )); do
c=${string:$pos:1}
case "$c" in
[-_.~a-zA-Z0-9] ) o="${c}" ;;
* ) printf -v o '%%%02X' "'$c"
esac
encoded+="${o}"
done
echo "${encoded}"
}
# Step 1: Construct the canonical request string
# Flatten all parameters in queryParam
newQueryParam=()
# Traverse each original parameter
for param in "${queryParam[@]}"; do
# Check if it contains an equal sign to determine if it is a key-value pair
if [[ "$param" == *"="* ]]; then
# Split key and value
IFS='=' read -r key value <<< "$param"
# URL-encode the value
value=$(urlencode "$value")
# Check if the value is a list (by looking for parentheses)
if [[ "$value" =~ ^\(.+\)$ ]]; then
# Remove the parentheses
value="${value:1:-1}"
# Use IFS to split the value list
IFS=' ' read -ra values <<< "$value"
# Add an index for each value
index=1
for val in "${values[@]}"; do
# Remove double quotes
val="${val%\"}"
val="${val#\"}"
# Add to the new array
newQueryParam+=("$key.$index=$val")
((index++))
done
else
# If it is not a list, add it directly
newQueryParam+=("$key=$value")
fi
else
# If there is no equal sign, keep it as is
newQueryParam+=("$param")
fi
done
# Process and sort the new query parameters
sortedParams=()
declare -A paramsMap
for param in "${newQueryParam[@]}"; do
IFS='=' read -r key value <<< "$param"
paramsMap["$key"]="$value"
done
# Sort by key
for key in $(echo ${!paramsMap[@]} | tr ' ' '\n' | LC_ALL=C sort); do
sortedParams+=("$key=${paramsMap[$key]}")
done
# 1.1 Construct the canonical query string
canonicalQueryString=""
first=true
for item in "${sortedParams[@]}"; do
[ "$first" = true ] && first=false || canonicalQueryString+="&"
# Check if an equal sign exists
if [[ "$item" == *=* ]]; then
canonicalQueryString+="$item"
else
canonicalQueryString+="$item="
fi
done
# 1.2 Process the request body
hashedRequestPayload=$(echo -n "$body" | openssl dgst -sha256 | awk '{print $2}')
headers="${headers}
x-acs-content-sha256:$hashedRequestPayload"
# 1.3 Construct the canonical request headers
canonicalHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
value=$(echo "$line" | cut -d':' -f2-)
echo "${key}:${value}"
done | sort | tr '\n' '\n')
signedHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
echo "$key"
done | sort | tr '\n' ';' | sed 's/;$//')
# 1.4 Construct the canonical request
canonicalRequest="${httpMethod}\n${canonicalURI}\n${canonicalQueryString}\n${canonicalHeaders}\n\n${signedHeaders}\n${hashedRequestPayload}"
echo -e "canonicalRequest=${canonicalRequest}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
str=$(echo "$canonicalRequest" | sed 's/%/%%/g')
hashedCanonicalRequest=$(printf "${str}" | openssl sha256 -hex | awk '{print $2}')
# Step 2: Construct the string to sign
stringToSign="${algorithm}\n${hashedCanonicalRequest}"
echo -e "stringToSign=$stringToSign"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
# Step 3: Calculate the signature
signature=$(printf "${stringToSign}" | openssl dgst -sha256 -hmac "${accessKey_secret}" | sed 's/^.* //')
echo -e "signature=${signature}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
# Step 4: Construct the Authorization header
authorization="${algorithm} Credential=${accessKey_id},SignedHeaders=${signedHeaders},Signature=${signature}"
echo -e "authorization=${authorization}"
# Construct the curl command
url="https://$host$canonicalURI"
curl_command="curl -X $httpMethod '$url?$canonicalQueryString'"
# Add request headers
IFS=$'\n' # Set newline as the new IFS
for header in $headers; do
curl_command="$curl_command -H '$header'"
done
curl_command+=" -H 'Authorization:$authorization'"
# When the body-type parameter is a binary file, comment out the following line
curl_command+=" -d '$body'"
# When the body-type parameter is a binary file, uncomment the following line
#curl_command+=" --data-binary @"/root/001.png" "
echo "$curl_command"
# Execute the curl command
eval "$curl_command"
C example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>
#include <stdint.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/rand.h>
#include <curl/curl.h>
// getenv() gets the Access Key ID and Access Key Secret from environment variables
#define ACCESS_KEY_ID getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
#define ACCESS_KEY_SECRET getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
#define ALGORITHM "ACS3-HMAC-SHA256"
#define BUFFER_SIZE 4096
// Struct for sorting
typedef struct {
char key[256];
char value[256];
} KeyValuePair;
// Comparison function: sort by key in lexicographical order
int compare_pairs(const void *a, const void *b) {
return strcmp(((const KeyValuePair *)a)->key, ((const KeyValuePair *)b)->key);
}
// URL encoding
char* percentEncode(const char* str) {
if (str == NULL) {
fprintf(stderr, "Input string cannot be null\n");
return NULL;
}
size_t len = strlen(str);
char* encoded = (char*)malloc(len * 3 + 1);
if (encoded == NULL) {
fprintf(stderr, "Memory allocation failed\n");
free(encoded);
return NULL;
}
char* ptr = encoded;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
*ptr++ = c;
} else {
ptr += sprintf(ptr, "%%%02X", c);
}
}
*ptr = '\0';
char* finalEncoded = malloc(strlen(encoded) + 1);
if (finalEncoded) {
char* fptr = finalEncoded;
for (size_t j = 0; j < strlen(encoded); j++) {
if (encoded[j] == '+') {
strcpy(fptr, "%20");
fptr += 3;
} else if (encoded[j] == '*') {
strcpy(fptr, "%2A");
fptr += 3;
} else if (encoded[j] == '~') {
*fptr++ = '~';
} else {
*fptr++ = encoded[j];
}
}
*fptr = '\0';
}
free(encoded);
return finalEncoded;
}
/**
* @brief URL-encodes query parameters, sorts them lexicographically, and generates a canonical query string.
* @param query_params The original query parameter string (e.g., "key1=value1&key2=value2").
* @return char* The sorted and encoded canonical query string (caller must free the memory).
*/
char* generate_sorted_encoded_query(const char* query_params) {
if (query_params == NULL || strlen(query_params) == 0) {
return strdup(""); // Return an empty string for empty parameters
}
KeyValuePair pairs[100]; // Supports up to 100 key-value pairs
int pair_count = 0;
char* copy = strdup(query_params);
if (!copy) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
char* token = NULL;
char* saveptr = NULL;
token = strtok_r(copy, "&", &saveptr);
while (token != NULL && pair_count < 100) {
char* eq = strchr(token, '=');
if (eq) {
size_t key_len = eq - token;
char key[256], value[256];
strncpy(key, token, key_len);
key[key_len] = '\0';
const char* val = eq + 1;
strncpy(value, val, sizeof(value) - 1);
value[sizeof(value) - 1] = '\0';
char* encoded_key = percentEncode(key);
char* encoded_value = percentEncode(value);
strncpy(pairs[pair_count].key, encoded_key, sizeof(pairs[pair_count].key));
strncpy(pairs[pair_count].value, encoded_value, sizeof(pairs[pair_count].value));
pair_count++;
free(encoded_key);
free(encoded_value);
}
token = strtok_r(NULL, "&", &saveptr);
}
free(copy);
// Sort by key
qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);
// Concatenate the sorted query string
char* query_sorted = malloc(BUFFER_SIZE);
if (!query_sorted) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
query_sorted[0] = '\0';
for (int i = 0; i < pair_count; ++i) {
if (i == 0) {
snprintf(query_sorted, BUFFER_SIZE, "%s=%s", pairs[i].key, pairs[i].value);
} else {
char temp[512];
snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
strncat(query_sorted, temp, BUFFER_SIZE - strlen(query_sorted) - 1);
}
}
return query_sorted;
}
// HMAC-SHA256 calculation
void hmac256(const char *key, const char *message, char *output) {
unsigned char hmac[SHA256_DIGEST_LENGTH];
unsigned int result_len;
HMAC(EVP_sha256(), key, strlen(key), (unsigned char *)message, strlen(message), hmac, &result_len);
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
sprintf(output + (i * 2), "%02x", hmac[i]);
}
output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// Calculate SHA-256 hash
void sha256_hex(const char *input, char *output) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char *)input, strlen(input), hash);
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
sprintf(output + (i * 2), "%02x", hash[i]);
}
output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// Used to generate x-acs-signature-nonce
void generate_uuid(char *uuid, size_t size) {
if (size < 37) {
fprintf(stderr, "Buffer size too small for UUID\n");
return;
}
unsigned char random_bytes[16];
RAND_bytes(random_bytes, sizeof(random_bytes));
random_bytes[6] &= 0x0f; // Keep the high 4 bits
random_bytes[6] |= 0x40; // Set version to 4
random_bytes[8] &= 0x3f; // Keep the high 2 bits
random_bytes[8] |= 0x80; // Set variant to 10xx
snprintf(uuid, size,
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
}
// Upload file
size_t read_file(const char *file_path, char **buffer) {
FILE *file = fopen(file_path, "rb");
if (!file) {
fprintf(stderr, "Cannot open file %s\n", file_path);
return 0; // Read failed
}
fseek(file, 0, SEEK_END);
size_t file_size = ftell(file);
fseek(file, 0, SEEK_SET);
*buffer = (char *)malloc(file_size);
if (!*buffer) {
fprintf(stderr, "Failed to allocate memory for file buffer\n");
fclose(file);
return 0; // Read failed
}
fread(*buffer, 1, file_size, file);
fclose(file);
return file_size; // Return the number of bytes read
}
// Calculate Authorization
char* get_authorization(const char *http_method, const char *canonical_uri, const char *host,
const char *x_acs_action, const char *x_acs_version, const char *query_params,
const char *body, char *authorization_header,
char *hashed_payload, char *x_acs_date, char *uuid) {
// Prepare x-acs-signature-nonce, x-acs-date, x-acs-content-sha256, and the string to be signed
generate_uuid(uuid, 37);
// x-acs-date format is yyyy-MM-ddTHH:mm:ssZ, for example, 2025-04-17T07:19:10Z
time_t now = time(NULL);
struct tm *utc_time = gmtime(&now);
strftime(x_acs_date, 64, "%Y-%m-%dT%H:%M:%SZ", utc_time);
// String to be signed
char signed_headers[] = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
// x-acs-content-sha256
sha256_hex(body ? body : "", hashed_payload);
printf("Generated x-acs-content-sha256: %s\n", hashed_payload);
// 1. Construct canonical request headers
char canonical_headers[BUFFER_SIZE];
snprintf(canonical_headers, sizeof(canonical_headers),
"host:%s\nx-acs-action:%s\nx-acs-content-sha256:%s\nx-acs-date:%s\nx-acs-signature-nonce:%s\nx-acs-version:%s",
host, x_acs_action, hashed_payload, x_acs_date, uuid, x_acs_version);
printf("Canonical Headers:\n%s\n", canonical_headers);
// 2. Construct the request headers to be signed
// Sort and encode query parameters
char* sorted_query_params = generate_sorted_encoded_query(query_params);
if (!sorted_query_params) {
fprintf(stderr, "Failed to generate sorted query string\n");
return NULL;
}
char canonical_request[BUFFER_SIZE];
snprintf(canonical_request, sizeof(canonical_request),
"%s\n%s\n%s\n%s\n\n%s\n%s",
http_method,
canonical_uri,
sorted_query_params ? sorted_query_params : "",
canonical_headers,
signed_headers,
hashed_payload);
printf("Canonical Request:\n%s\n", canonical_request);
// 3. Calculate the SHA-256 hash of the canonical request
char hashed_canonical_request[SHA256_DIGEST_LENGTH * 2 + 1];
sha256_hex(canonical_request, hashed_canonical_request);
printf("hashedCanonicalRequest: %s\n", hashed_canonical_request);
// 4. Construct the string to sign
char string_to_sign[BUFFER_SIZE];
snprintf(string_to_sign, sizeof(string_to_sign), "%s\n%s", ALGORITHM, hashed_canonical_request);
printf("stringToSign:\n%s\n", string_to_sign);
// 5. Calculate the signature
char signature[SHA256_DIGEST_LENGTH * 2 + 1];
hmac256(ACCESS_KEY_SECRET, string_to_sign, signature);
printf("Signature: %s\n", signature);
// 6. Construct the Authorization header
snprintf(authorization_header, BUFFER_SIZE,
"%s Credential=%s,SignedHeaders=%s,Signature=%s",
ALGORITHM, ACCESS_KEY_ID, signed_headers, signature);
printf("Authorization: %s\n", authorization_header);
return sorted_query_params;
}
// Send the request
void call_api(const char *http_method, const char *canonical_uri, const char *host,
const char *x_acs_action, const char *x_acs_version, const char *query_params,
const char *body,const char *content_type, size_t body_length) {
// Get the parameter values required for signature calculation
char authorization_header[BUFFER_SIZE];
char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
char x_acs_date[64];
char uuid[37];
// 1. Initialize curl
CURL *curl = curl_easy_init();
if (!curl) {
fprintf(stderr, "curl_easy_init() failed\n");
goto cleanup;
}
// 2. Calculate the signature (returns sorted and encoded query parameters)
char *signed_query_params = get_authorization(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, authorization_header, hashed_payload, x_acs_date, uuid);
// 3. Add request parameters
char url[BUFFER_SIZE];
if (signed_query_params && strlen(signed_query_params) > 0) {
snprintf(url, sizeof(url), "https://%s%s?%s", host, canonical_uri, signed_query_params);
} else {
snprintf(url, sizeof(url), "https://%s%s", host, canonical_uri);
}
printf("Request URL: %s\n", url);
// Free memory
if (signed_query_params) {
free(signed_query_params); // Free memory
}
// 4. Add headers
struct curl_slist *headers = NULL;
char header_value[BUFFER_SIZE];
snprintf(header_value, sizeof(header_value), "Content-Type: %s", content_type);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "Authorization: %s", authorization_header);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "host: %s", host);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-action: %s", x_acs_action);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-content-sha256: %s", hashed_payload);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-date: %s", x_acs_date);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-signature-nonce: %s", uuid);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-version: %s", x_acs_version);
headers = curl_slist_append(headers, header_value);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method);
curl_easy_setopt(curl, CURLOPT_URL, url);
// Other CURL settings: disable SSL verification, add debug information
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
// 5. Add body
if (body) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_length);
if (strcmp(content_type, "application/octet-stream") == 0) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
} else if (strcmp(content_type, "application/x-www-form-urlencoded") == 0) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
} else if (strcmp(content_type, "application/json; charset=utf-8") == 0) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
}
}
printf("RequestBody:%s\n",body);
// 6. Send the request
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
goto cleanup;
}
cleanup:
if (headers) curl_slist_free_all(headers);
if (curl) curl_easy_cleanup(curl);
}
/**
*
* Signature example. Replace the example parameters in the main method as needed.
* <p>
* Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata.
* 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using query_params. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
* 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
* 2. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
int main() {
// Set response format to UTF-8
SetConsoleOutputCP(CP_UTF8);
srand((unsigned int)time(NULL));
/**
* RPC API request example: Request parameters are in "query", and the query type is complex.
*/
const char *http_method = "POST";
const char *canonical_uri = "/";
const char *host = "tds.cn-shanghai.aliyuncs.com";
const char *x_acs_action = "AddAssetSelectionCriteria";
const char *x_acs_version = "2018-12-03";
// Define parameter SelectionKey, string type
const char *selection_key = "85a561b7-27d5-47ad-a0ec-XXXXXXXX";
// Define parameter TargetOperationList, a collection of target objects (can be extended)
struct {
const char *operation;
const char *target;
} targetOperation_list[] = {
{"add", "i-2ze1j7ocdg9XXXXXXXX"},
// More entries can be added
// {"add", "i-abc123xyzXXXXX"},
};
int count = sizeof(targetOperation_list) / sizeof(targetOperation_list[0]);
KeyValuePair pairs[100]; // Store original keys and values
int pair_count = 0;
for (int i = 0; i < count; ++i) {
char op_key[128], target_key[128];
snprintf(op_key, sizeof(op_key), "TargetOperationList.%d.Operation", i + 1);
snprintf(target_key, sizeof(target_key), "TargetOperationList.%d.Target", i + 1);
strncpy(pairs[pair_count].key, op_key, sizeof(pairs[pair_count].key));
strncpy(pairs[pair_count].value, targetOperation_list[i].operation, sizeof(pairs[pair_count].value));
pair_count++;
strncpy(pairs[pair_count].key, target_key, sizeof(pairs[pair_count].key));
strncpy(pairs[pair_count].value, targetOperation_list[i].target, sizeof(pairs[pair_count].value));
pair_count++;
}
// Add SelectionKey parameter
snprintf(pairs[pair_count].key, sizeof(pairs[pair_count].key), "SelectionKey");
snprintf(pairs[pair_count].value, sizeof(pairs[pair_count].value), "%s", selection_key);
pair_count++;
// Sorting and encoding are done in get_authorization()
qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);
// Construct the original query string (unencoded)
char query_params[BUFFER_SIZE] = {0};
for (int i = 0; i < pair_count; ++i) {
if (i == 0) {
snprintf(query_params, sizeof(query_params), "%s=%s", pairs[i].key, pairs[i].value);
} else {
char temp[512];
snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
strncat(query_params, temp, sizeof(query_params) - strlen(query_params) - 1);
}
}
const char *body = ""; // Request body is empty
const char *content_type = "application/json; charset=utf-8";
call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* RPC API request example: Request parameters are in "query"
*/
// Define API request parameters
// const char *http_method = "POST";
// const char *canonical_uri = "/";
// const char *host = "ecs.cn-hangzhou.aliyuncs.com";
// const char *x_acs_action = "DescribeInstanceStatus";
// const char *x_acs_version = "2014-05-26";
// // Define parameter InstanceId, an array. InstanceId is an optional parameter.
// const char *instance_ids[] = {
// "i-bp11ht4hXXXXXXXX",
// "i-bp16maz3XXXXXXXX"
// };
// // Concatenate the InstanceId array
// char InstanceId[BUFFER_SIZE];
// snprintf(InstanceId, sizeof(InstanceId),
// "InstanceId.1=%s&InstanceId.2=%s",
// instance_ids[0],
// instance_ids[1]);
// // Define query parameters. Required parameter: RegionId=cn-hangzhou. const char *query_params = "RegionId=cn-hangzhou";
// char query_params[BUFFER_SIZE];
// snprintf(query_params, sizeof(query_params),
// "%s&RegionId=cn-hangzhou", InstanceId);
// const char *body = "";
// const char *content_type = "application/json; charset=utf-8";
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* RPC API request example: Request parameters are in "body" (file upload scenario)
*/
// Declare a pointer to store the read file content
// char *body = NULL;
// size_t body_length = read_file("<YOUR_FILE_PATH>", &body);
// if (body_length > 0) {
// const char *http_method = "POST";
// const char *canonical_uri = "/";
// const char *host = "ocr-api.cn-hangzhou.aliyuncs.com";
// const char *x_acs_action = "RecognizeGeneral";
// const char *x_acs_version = "2021-07-07";
// const char *query_params = "";
// const char *content_type = "application/octet-stream";
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, body_length);
// free(body);
// } else {
// fprintf(stderr, "File read error\n");
// }
/**
* RPC API request example: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
*/
// const char *http_method = "POST";
// const char *canonical_uri = "/";
// const char *host = "mt.aliyuncs.com";
// const char *x_acs_action = "TranslateGeneral";
// const char *x_acs_version = "2018-10-12";
// char query_params[BUFFER_SIZE];
// snprintf(query_params, sizeof(query_params), "Context=%s", "Morning");
// const char *format_type = "text";
// const char *source_language = "zh";
// const char *target_language = "en";
// const char *source_text = "Hello";
// const char *scene = "general";
// char body[BUFFER_SIZE];
// snprintf(body, sizeof(body),
// "FormatType=%s&SourceLanguage=%s&TargetLanguage=%s&SourceText=%s&Scene=%s",
// percentEncode(format_type), percentEncode(source_language), percentEncode(target_language),
// percentEncode(source_text), percentEncode(scene));
// const char *content_type = "application/x-www-form-urlencoded";
// printf("formdate_body: %s\n", body);
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
// RPC API request example 3: Request parameters are in "formData"
// const char *http_method = "POST";
// const char *canonical_uri = "/";
// const char *host = "sasti.aliyuncs.com";
// const char *x_acs_action = "AskTextToTextMsg";
// const char *x_acs_version = "2020-05-12";
// // query
// const char *query_params = "";
// // body
// const char *Memory = "false";
// const char *Stream = "true";
// const char *ProductCode = "sddp_pre";
// const char *Feature = "{}";
// const char *Model = "yunsec-llm-latest";
// const char *Type = "Chat";
// const char *TopP = "0.9";
// const char *Temperature = "0.01";
// const char *Prompt = "Who are you";
// const char *Application = "sddp_pre";
// char body[BUFFER_SIZE];
// snprintf(body, sizeof(body),
// "Memory=%s&Stream=%s&ProductCode=%s&Feature=%s&Model=%s&Type=%s&TopP=%s&Temperature=%s&Prompt=%s&Application=%s",
// Memory, Stream, ProductCode, Feature, Model, Type, TopP, Temperature, Prompt, Application);
// const char *content_type = "application/x-www-form-urlencoded";
// printf("formdate_body: %s\n", body);
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* ROA API POST request "in" "body"
*/
// const char *http_method = "POST";
// const char *canonical_uri = "/clusters";
// const char *host = "cs.cn-beijing.aliyuncs.com";
// const char *x_acs_action = "CreateCluster";
// const char *x_acs_version = "2015-12-15";
// const char *query_params = "";
// char body[BUFFER_SIZE];
// snprintf(body, sizeof(body),
// "{\"name\":\"%s\",\"region_id\":\"%s\",\"cluster_type\":\"%s\","
// "\"vpcid\":\"%s\",\"container_cidr\":\"%s\","
// "\"service_cidr\":\"%s\",\"security_group_id\":\"%s\","
// "\"vswitch_ids\":[\"%s\"]}",
// "Test Cluster", "cn-beijing", "ExternalKubernetes",
// "vpc-2zeou1uod4yXXXXXXXX", "10.X.X.X/XX",
// "10.X.X.X/XX", "sg-2ze1a0rlgeXXXXXXXX",
// "vsw-2zei30dhflXXXXXXXX");
// const char *content_type = "application/json; charset=utf-8";
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* ROA API GET request
*/
// const char *http_method = "GET";
// char canonical_uri[BUFFER_SIZE];
// snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s/resources", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
// const char *host = "cs.cn-beijing.aliyuncs.com";
// const char *x_acs_action = "DescribeClusterResources";
// const char *x_acs_version = "2015-12-15";
// const char *query_params = "with_addon_resources=true";
// const char *body = "";
// const char *content_type = "";
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* ROA API DELETE request
*/
// const char *http_method = "DELETE";
// char canonical_uri[BUFFER_SIZE];
// snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
// const char *host = "cs.cn-beijing.aliyuncs.com";
// const char *x_acs_action = "DeleteCluster";
// const char *x_acs_version = "2015-12-15";
// const char *query_params = "";
// const char *body = "";
// const char *content_type = "";
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
// Variables to store generated values
char authorization_header[BUFFER_SIZE];
char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
char x_acs_date[64];
char uuid[37];
return 0;
}
FAQ
Why do I receive the "Specified signature does not match our calculation." or "The request signature does not conform to Aliyun standards." error when the signature fails?
How do I test with Postman?
How do I pass request parameters?
How do you determine the API style if the value of style
in the API metadata is not RPC or ROA?
How do I pass parameters of the array or object type?
How do I get the API version (x-acs-version)?
If I can successfully debug with GET when self-signing, can I use POST?
Why do I receive the "You are not authorized to do this operation." error when calling an API?
How do I get an AccessKey pair?
Contact us
If you encounter issues that you cannot resolve when you calculate signatures, join the DingTalk group with the ID 147535001692
to contact our on-duty engineers for assistance.
Do not join this group for issues that are unrelated to signature calculation. Otherwise, you may not receive an effective response.