All Products
Search
Document Center

Alibaba Cloud SDK:Request syntax and signature method V3

Last Updated:Dec 13, 2024

This topic describes the request syntax and signature method V3. This version uses common request headers to pass the required parameters of API operations. This eliminates the differences between different API styles, and allows you to use the same signature method for different API styles and call Alibaba Cloud API operations in a more standard and easier way. This topic helps you understand and use the request syntax and signature method V3 of Alibaba Cloud SDKs. This topic also describes how to construct a canonicalized HTTP request and how to use a correct signature algorithm to verify the identity of a request sender. This ensures the integrity and security of the transmitted data. You can refer to this topic to generate a signature for an Alibaba Cloud API request.

Note

The signature method V3 can be used to call API operations of Alibaba Cloud services that use SDKs provided by OpenAPI Portal.

HTTP request syntax

The following table describes the components of an Alibaba Cloud API request.

Component

Required

Description

Example

Protocol

Yes

The protocol that is used to send the API request. You can read the API reference of each Alibaba Cloud service to obtain information about the protocol that is used. You can send a request over HTTP or HTTPS. To ensure data security, we recommend that you send requests over HTTPS. Valid values: https:// and http://.

https://

Endpoint

Yes

The endpoint of the Alibaba Cloud service API. Topics that list the endpoints of each Alibaba Cloud service are available. You can view the endpoints of a service in different regions in these topics.

cs.aliyuncs.com

resource_URI_parameters

Yes

The identifier of the resource that you want to access, including the resource path and the request parameters whose parameter location is path or query.

/clusters/{cluster_id}/triggers

RequestHeader

Yes

The request headers. In most cases, information such as the API version number, endpoint, and authentication information is included. For more information, see the "Request headers" section of this topic.

x-acs-action

RequestBody

Yes

The operation-specific parameters in the request body. For more information, visit OpenAPI Explorer.

cluster_id

HTTPMethod

Yes

The HTTP method that is used to send the request. Valid values: PUT, POST, GET, and DELETE.

POST

Request headers

The following table describes the request headers of an Alibaba Cloud API request.

Header

Type

Required

Description

Example

x-acs-action

String

Yes

The operation that you want to perform. You can search for the API operation that you want to call in OpenAPI Portal.

RunInstances

x-acs-version

String

Yes

The version number of the API. You can view the API version of the service that you want to access in OpenAPI Portal.

2014-05-26

Authorization

String

Yes if the request is non-anonymous

The authentication information that is used to validate the request. Format: Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature.

SignatureAlgorithm: the encryption method of the signature string. Set the value to ACS3-HMAC-SHA256.

Credential: the AccessKey ID provided to you by Alibaba Cloud. You can view your AccessKey ID in the Resource Access Management (RAM) console. For more information about how to create an AccessKey pair, see Obtain an AccessKey pair.

SignedHeaders: the names of the request headers that are used for signature calculation. We recommend that you use all common request headers except Authorization for signature calculation to improve security.

Signature: the signature string of the current request. For more information, see the "Signature method" section of this topic.

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=e521358f7776c97df52e6b2891a8bc73026794a071b50c3323388c4e0df64804

x-acs-signature-nonce

String

Yes

A unique, random number used to prevent network replay attacks. You must use different numbers for different requests.

d410180a5abf7fe235dd9b74aca91fc0

x-acs-date

String

Yes

The timestamp of the request. Specify the time in the ISO 8601 standard in the yyyy-MM-ddTHH:mm:ssZ format. The time must be in UTC. Example: 2018-01-01T12:00:00Z. The timestamp must be within 15 minutes before the request is sent.

2023-10-26T09:01:01Z

host

String

Yes

The endpoint of the Alibaba Cloud service API. For more information, see the HTTP request syntax section of this topic.

ecs.cn-shanghai.aliyuncs.com

x-acs-content-sha256

String

Yes

The hash value of the request body. The hash value is encoded in Base16. The value of this header is the same as that of the HashedRequestPayload parameter.

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-security-token

String

Yes if Security Token Service (STS) is used for authentication

The STS token. Set this header to the value of the SecurityToken parameter in the response of the AssumeRole operation.

Request signature

You must sign all API requests to ensure security. Alibaba Cloud uses the request signature to verify the identity of the API caller. Each API request must contain a signature, regardless of whether the request is sent over HTTP or HTTPS.

Important

All requests and responses are encoded in UTF-8.

For each HTTP or HTTPS request, Alibaba Cloud uses the request signature to verify the identity of the API caller. You can perform the following steps to sign a request:

Step 1: Construct a canonicalized request

To use an AccessKey pair to calculate or verify a signature, you must construct a canonicalized request. The client and API gateway must use the same request specifications so that they can obtain the same signature string for an HTTP request to complete identity verification.

Construct a canonicalized request based on the following pseudocode:

CanonicalRequest =
  HTTPRequestMethod + '\n' +    // The HTTP request method in uppercase letters.
  CanonicalURI + '\n' +         // The canonicalized uniform resource identifier (URI).
  CanonicalQueryString + '\n' + // The canonicalized query string.
  CanonicalHeaders + '\n' +     // The canonicalized request headers.
  SignedHeaders + '\n' +        // The request headers that are used for signature calculation.
  HashedRequestPayload			
  • HTTPRequestMethod

    The HTTP request method in uppercase letters, such as GET or POST.

  • CanonicalURI

    The encoded resource path in the URL. The resource path is the part between the endpoint and the query string. The path includes the forward slash (/) that follows the endpoint but excludes the question mark (?) that precedes the query string. You must use a canonicalized URI for signature calculation. To construct a canonicalized URI, encode the strings separated by forward slashes (/) in UTF-8 based on RFC 3986. Encoding rules:

    • Letters, digits, hyphens (-), underscores (_), periods (.), and tildes (~) do not need to be encoded.

    • Other characters must be percent-encoded in the following format: % + ASCII code of the characters in hexadecimal notation. For example, double quotation marks (") are encoded as %22.

    • Spaces must be encoded as %20 instead of plus signs (+). Asterisks (*) are encoded as %2A. %7E is replaced with tildes (~).

      If you use java.net.URLEncoder in the Java standard library, encode the strings based on the standard library. In the encoded strings, replace plus signs (+) with %20, asterisks (*) with %2A, and %7E with tildes (~). This way, you can obtain encoded strings that match the preceding encoding rules.

    Important

    If the API style is RPC, use a forward slash (/) as the value of the CanonicalURI parameter.

    If the API style is ROA, encode the value of the path parameter in the metadata and use the encoded value as the value of the CanonicalURI parameter. Example: /api/v1/clusters.

  • CanonicalQueryString

    The canonicalized query string. You can perform the following steps to construct the canonicalized query string:

    1. Sort the parameters in the query string by parameter name in alphabetical order. For parameters with the same name, sort the parameters by parameter value in alphabetical order.

    2. Encode the parameter names and values in UTF-8 based on RFC 3986. The encoding rules are the same as those used to construct the canonicalized URI.

    3. Use an equal sign (=) to concatenate the encoded name and value of each parameter. For a parameter without a value, use an empty string as the parameter value.

    4. Use ampersands (&) to concatenate the encoded parameters in the order obtained in the previous step.

    Important

    If the query string is empty, use an empty string as the canonicalized query string.

  • CanonicalHeaders

    The canonicalized request headers. You must add non-standard HTTP request headers whose names are prefixed with x-acs-, host, or content-type as canonicalized request headers. You can perform the following steps to construct canonicalized request headers:

    1. Convert the names of all headers that are used for signature calculation to lowercase letters.

    2. Sort all headers by header name in alphabetical order.

    3. Remove the spaces before and after the value of each header. If a header has multiple values, remove the spaces before and after each value, sort the values in alphabetical order, and then use commas (,) to concatenate the values.

    4. Use a colon (:) to concatenate the name and value of each header obtained in the preceding steps and add a line break at the end to construct a canonicalized request header.

    5. If no headers are used for signature calculation, use an empty string as the canonicalized request headers.

    Important

    All common request headers except the Authorization header must be used for signature calculation.

    Pseudocode:

    CanonicalHeaderEntry = Lowercase(HeaderName) + ':' + Trim(HeaderValue) + '\n'
    
    CanonicalHeaders = 
        CanonicalHeaderEntry0 + CanonicalHeaderEntry1 + ... + CanonicalHeaderEntryN
  • SignedHeaders

    The request headers that are used for signature calculation. This parameter contains the same headers as those in the CanonicalHeaders parameter. You can perform the following steps to construct signed headers:

    • Convert the names of headers in the CanonicalHeaders parameter to lowercase letters.

    • Use semicolons (;) to concatenate all the headers by lowercase header name in alphabetical order. Example: content-type;host;x-acs-date.

    Pseudocode:

    SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ... + Lowercase(HeaderNameN) 
    
  • HashedRequestPayload

    The hash value of the request body. If the request body is empty, use an empty string as the value of the RequestPayload parameter. Otherwise, the value of the RequestPayload parameter is the JSON string that corresponds to the request body. Use a hash algorithm to calculate the hash value for the value of the RequestPayload parameter and obtain the value of the HashedRequestPayload parameter. The following pseudocode shows how to generate the hash value: HashedRequestPayload = HexEncode(Hash(RequestPayload)).

    • Hash() indicates the hash algorithm. Only the SHA-256 algorithm is supported. For example, if the encryption method of the signature string is ACS3-HMAC-SHA256, you must use SHA-256 as the hash algorithm.

    • HexEncode() encodes the hash value in Base16. This function returns an encoded hash value in the hexadecimal format in lowercase letters.

Table 1. Mappings among the encryption methods of the signature string, signature calculation algorithms, and hash algorithms

Encryption method of the signature string (SignatureAlgorithm)

Hash algorithm used to process RequestPayload and CanonicalRequest (Hash)

Algorithm for signature calculation

(SignatureMethod)

ACS3-HMAC-SHA256

SHA256

HMAC-SHA256

Step 2: Construct a string-to-sign

Construct a string-to-sign based on the following pseudocode:

StringToSign =
    SignatureAlgorithm + '\n' +
    HashedCanonicalRequest
  • SignatureAlgorithm

    The encryption method of the signature string. Only ACS3-HMAC-SHA256 is supported. The MD5- or SHA1-based algorithms are no longer supported.

  • HashedCanonicalRequest

    The hash value of the canonicalized request. The following pseudocode shows how to generate the hash value:

    HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
  1. Calculate the hash value of the canonicalized request that is obtained in Step 1. Select a hash algorithm based on the encryption method of the signature string, which is specified by the SignatureAlgorithm parameter. For more information, see Table 1 of this topic. Only ACS3-HMAC-SHA256 can be used as the encryption method. In this case, you must use SHA-256 as the hash algorithm.

  2. Encode the hash value obtained in the previous step in Base16.

Step 3: Calculate the signature string

Calculate the signature string based on the following pseudocode:

Signature = HexEncode(SignatureMethod(Secret, StringToSign))
  • StringToSign: the string-to-sign that is constructed in Step 2. The value is encoded in UTF-8.

  • SignatureMethod: the algorithm for signature calculation. Select an algorithm based on the encryption method of the signature string, which is specified by the SignatureAlgorithm parameter. For more information, see Table 1 of this topic.

  • Secret: the AccessKey secret provided by Alibaba Cloud. The value is in the binary format.

  • HexEncode: the function that is used to encode values in Base16.

Step 4: Add the signature string to the request

After you obtain the signature string, specify the Authorization header in the request in the following format: Authorization:<SignatureAlgorithm> Credential=<AccessKeyId>,SignedHeaders=<SignedHeaders>,Signature=<Signature>. Example:

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=6b595d672d79c15b18edb4ccfba6789a24a6f2b82c400e03162d9279b08555d7

Signature examples

Note
  1. To help you understand the preceding signature method, this topic provides the following sample code to completely implement the signature method in mainstream programming languages. The following sample code is provided only to help you understand the signature method and is not globally applicable. Alibaba Cloud provides SDKs for multiple programming languages and development frameworks. We recommend that you use Alibaba Cloud SDKs to initiate API requests and automatically generate signatures for the requests. You can develop applications on Alibaba Cloud without the need to manually calculate a signature.

  2. Before you sign an API request, you must view the API metadata to obtain information such as the request methods, request parameters, and parameter locations of the API operation that you want to call.

Java

To use the signature method in Java, you must add the following Maven dependencies to the 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.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.ContentType;

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.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * signature demo
 */
public class SignatureDemo {

    /**
     * The tool that is used to format dates. The tool can format a DATETIME string into a value in the yyyy-MM-dd'T'HH:mm:ss'Z format. 
     */
    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

    private static class Request {
        // HTTP Method
        private final String httpMethod;
        // The canonicalized URI. If the resource path is empty, use a forward slash (/) as the value of the CanonicalURI parameter.
        private final String canonicalUri;
        // endpoint
        private final String host;
        // API name
        private final String xAcsAction;
        // API version
        private final String xAcsVersion;
        // headers
        TreeMap<String, Object> headers = new TreeMap<>();
        // The request parameters whose parameter location is body. These parameters are the operation-specific parameters in the request body. The value must be a JSON string.
        String body;
        // The request parameters whose parameter location is query. These parameters are sorted by parameter name in alphabetical order and placed in the request URL.
        TreeMap<String, Object> queryParam = new TreeMap<>();

        public Request(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();
        }

        // init headers
        private void initHeader() {
            headers.put("host", host);
            headers.put("x-acs-action", xAcsAction);
            headers.put("x-acs-version", xAcsVersion);
            SDF.setTimeZone(new SimpleTimeZone(0, "GMT")); // Set the time zone for date formatting to GMT.
            headers.put("x-acs-date", SDF.format(new Date()));
            headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
        }
    }

    /**
     * In this example, the AccessKey ID and AccessKey secret are obtained from environment variables. 
     */
    private final static String ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
    private final static String ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");

    /**
     * The signature algorithm.
     */
    private static final String ALGORITHM = "ACS3-HMAC-SHA256";

    /**
     * Construct a request based on the specified parameters, perform signature authentication, and then initiate the request.
     */
    public static void main(String[] args) {
        // Construct a request for an RPC API operation.
        String httpMethod = "POST"; // The HTTP request method. In most cases, you can use the POST or GET method to call an RPC API operation. In this example, POST is used.
        String canonicalUri = "/"; // The resource path of an RPC API operation is empty. Therefore, a forward slash (/) is used as the value of the CanonicalURI parameter.
        String host = "ecs.cn-hangzhou.aliyuncs.com";  // endpoint
        String xAcsAction = "DescribeInstanceStatus";  // The operation that you want to perform.
        String xAcsVersion = "2014-05-26"; // The version number of the API.
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // The request parameters. Sort the parameters by parameter name in alphabetical order. For parameters with the same name, sort the parameters by parameter value in alphabetical order. 
        request.queryParam.put("RegionId", "cn-hangzhou"); // The RegionId parameter is of the String type.
        String[] instanceIds = {"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"};
        request.queryParam.put("InstanceId", Arrays.asList(instanceIds)); // The InstanceId parameter is of the Array type.

        /*// Construct a POST request for a ROA API operation.
        String httpMethod = "POST";
        String canonicalUri = "/clusters";
        String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
        String xAcsAction= "CreateCluster"; // The operation that you want to perform.
        String xAcsVersion=  "2015-12-15"; // The version number of the API.
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // The request body. Use Gson to convert the request body into a JSON string. The following parameters show an example.
        TreeMap<String, Object> body = new TreeMap<>();
        body.put("name", "testDemo");
        body.put("region_id", "cn-beijing");
        body.put("cluster_type", "ManagedKubernetes");
        body.put("vpcid", "vpc-2zeo42r27y4opXXXXXXXX");
        body.put("service_cidr", "172.16.1.0/20");
        body.put("security_group_id", "sg-2zec0dm6qi66XXXXXXXX");
        Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
        request.body = gson.toJson(body);
        request.headers.put("content-type", "application/json; charset=utf-8");*/

        /*// Construct a GET request for a ROA API operation.
        String httpMethod = "GET";
        // If the value of the canonicalUri parameter contains the value of the path parameter, encode the value of the path parameter by specifying percentCode({Value of the path parameter}).
        String canonicalUri = "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources";
        String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
        String xAcsAction = "DescribeClusterResources"; // The operation that you want to perform.
        String xAcsVersion = "2015-12-15"; // The version number of the API.
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        request.queryParam.put("with_addon_resources", true);*/

        /*// Construct a DELETE request for a ROA API operation.
        String httpMethod = "DELETE";
        String canonicalUri = "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX");
        String host = "cs.cn-beijing.aliyuncs.com";
        String xAcsAction = "DeleteCluster";
        String xAcsVersion = "2015-12-15";
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);*/

        // Sign the request.
        getAuthorization(request);
        // Call the API operation.
        callApi(request);
    }

    private static void callApi(Request request) {
        try {
            // Send the request by using HttpClient.
            String url = "https://" + request.host + request.canonicalUri;
            URIBuilder uriBuilder = new URIBuilder(url);
            // Specify request parameters.
            for (Map.Entry<String, Object> entry : request.queryParam.entrySet()) {
                uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
            }
            System.out.println(uriBuilder.build());
            HttpUriRequest httpRequest;
            switch (request.httpMethod) {
                case "GET":
                    httpRequest = new HttpGet(uriBuilder.build());
                    break;
                case "POST":
                    HttpPost httpPost = new HttpPost(uriBuilder.build());
                    if (request.body != null) {
                        httpPost.setEntity(new StringEntity(request.body, ContentType.APPLICATION_JSON));
                    }
                    httpRequest = httpPost;
                    break;
                case "DELETE":
                    httpRequest = new HttpDelete(uriBuilder.build());
                    break;
                default:
                    System.out.println("Unsupported HTTP method: " + request.body);
                    throw new IllegalArgumentException("Unsupported HTTP method");
            }

            // Specify HTTP request headers.
            for (Map.Entry<String, Object> entry : request.headers.entrySet()) {
                httpRequest.addHeader(entry.getKey(), String.valueOf(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(result);
            } catch (IOException e) {
                // Handle errors.
                System.out.println("Failed to send request");
                e.printStackTrace();
            }
        } catch (URISyntaxException e) {
            // Handle errors.
            System.out.println("Invalid URI syntax");
            e.printStackTrace();
        }
    }

    /**
     * This method is used to calculate and generate a signature string based on the specified HTTP request method, canonicalized URI, and query parameters. 
     */
    private static void getAuthorization(Request request) {
        try {
            // Flatten the query parameters of the List and Map types.
            TreeMap<String, Object> newQueryParam = new TreeMap<>();
            processObject(newQueryParam, "", request.queryParam);
            request.queryParam = newQueryParam;
            // Step 1: Construct a canonicalized request.
            // The request parameters. If the query string is empty, use an empty string as the canonicalized query string.
            StringBuilder canonicalQueryString = new StringBuilder();
            request.queryParam.entrySet().stream().map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
                // If the canonicalized query string is not empty, use ampersands (&) to concatenate more request parameters.
                if (canonicalQueryString.length() > 0) {
                    canonicalQueryString.append("&");
                }
                canonicalQueryString.append(queryPart);
            });
            // The request body. If the request body is empty, such as in a GET request, use an empty string as the value of the RequestPayload parameter.
            String requestPayload = "";
            if (request.body != null) {
                requestPayload = request.body;
            }

            // Calculate the hash value of the request body.
            String hashedRequestPayload = sha256Hex(requestPayload);
            request.headers.put("x-acs-content-sha256", hashedRequestPayload);
            // Construct request headers. Concatenate multiple canonicalized request headers by lowercase header name in alphabetical order.
            StringBuilder canonicalHeaders = new StringBuilder();
            // The request headers that are used for signature calculation. Use semicolons (;) to concatenate all the headers by lowercase header name in alphabetical order.
            StringBuilder signedHeadersSb = new StringBuilder();
            request.headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") || entry.getKey().equalsIgnoreCase("host") || entry.getKey().equalsIgnoreCase("content-type")).sorted(Map.Entry.comparingByKey()).forEach(entry -> {
                String lowerKey = entry.getKey().toLowerCase();
                String value = String.valueOf(entry.getValue()).trim();
                canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
                signedHeadersSb.append(lowerKey).append(";");
            });
            String signedHeaders = signedHeadersSb.substring(0, signedHeadersSb.length() - 1);
            String canonicalRequest = request.httpMethod + "\n" + request.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
            System.out.println("canonicalRequest=========>\n" + canonicalRequest);

            // Step 2: Construct a string-to-sign.
            String hashedCanonicalRequest = sha256Hex(canonicalRequest); // Calculate the hash value of the canonicalized request.
            String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
            System.out.println("stringToSign=========>\n" + stringToSign);

            // Step 3: Calculate the signature string.
            String signature = DatatypeConverter.printHexBinary(hmac256(ACCESS_KEY_SECRET.getBytes(StandardCharsets.UTF_8), stringToSign)).toLowerCase();
            System.out.println("signature=========>" + signature);

            // Step 4: Specify the Authorization header.
            String authorization = ALGORITHM + " " + "Credential=" + ACCESS_KEY_ID + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature;
            System.out.println("authorization=========>" + authorization);
            request.headers.put("Authorization", authorization);
        } catch (Exception e) {
            // Handle errors.
            System.out.println("Failed to get authorization");
            e.printStackTrace();
        }
    }

    /**
     * The objects on which recursion is performed. Complex objects such as maps and lists are recursively decomposed into key-value pairs.
     *
     * @param map The original map that stores key-value pairs. The map is recursively updated.
     * @param key The key that you want to process. As the recursion deepens, the keys accumulate the path leading to a specific value.
     * @param value The value of the key. The value can be of types such as Map or List.
     */
    private static void processObject(Map<String, Object> map, String key, Object value) {
        // No further processing is required for a null value.
        if (value == null) {
            return;
        }
        if (key == null) {
            key = "";
        }
        // If the value is of the List type, traverse the list and perform recursion on each element.
        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<?, ?>) {
            // If the value is of the Map type, traverse the map and perform recursion on each key-value pair.
            Map<?, ?> subMap = (Map<?, ?>) value;
            for (Map.Entry<?, ?> entry : subMap.entrySet()) {
                processObject(map, key + "." + entry.getKey().toString(), entry.getValue());
            }
        } else {
            // If a key starts with a period (.), remove the period (.) to maintain the continuity of keys.
            if (key.startsWith(".")) {
                key = key.substring(1);
            }
            // If a value is in the byte[] format, convert the value to a string encoded in UTF-8.
            if (value instanceof byte[]) {
                map.put(key, new String((byte[]) value, StandardCharsets.UTF_8));
            } else {
                // Convert the values of other types to strings.
                map.put(key, String.valueOf(value));
            }
        }
    }

    /**
     * Use the HMAC-SHA256 algorithm to generate a message authentication code (MAC). 
     *
     * @param secretKey The key that is used to generate the MAC. The key must be kept confidential. 
     * @param str The message to be authenticated. 
     * @return The MAC that is calculated by using the HMAC-SHA256 algorithm. 
     * @throws Exception The error that is reported when the MAC generator is initialized or the MAC is calculated. 
     */
    public static byte[] hmac256(byte[] secretKey, String str) throws Exception {
        // Obtain the object on which the HMAC-SHA256 algorithm is implemented.
        Mac mac = Mac.getInstance("HmacSHA256");
        // Create a key standard to initialize the MAC generator.
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, mac.getAlgorithm());
        // Initialize the MAC generator.
        mac.init(secretKeySpec);
        // Calculate and return the MAC.
        return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * Calculate the hash value of the signature string by using the SHA-256 algorithm and return the hash value as a string in the hexadecimal format. 
     *
     * @param stringToSign The string to be calculated by using the SHA-256 algorithm. 
     * @return The string in the hexadecimal format in lowercase letters. 
     * @throws Exception The error that is reported when you obtain the object on which the SHA-256 algorithm is implemented. 
     */
    public static String sha256Hex(String stringToSign) throws Exception {
        // Obtain the object on which the SHA-256 algorithm is implemented.
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        // Calculate the hash value of the string by using the SHA-256 algorithm.
        byte[] d = md.digest(stringToSign.getBytes(StandardCharsets.UTF_8));
        // Convert the hash value to a string in the hexadecimal format in lowercase letters and return the string.
        return DatatypeConverter.printHexBinary(d).toLowerCase();
    }

    /**
     * Perform URL encoding on the specified string. 
     * Encode the string in UTF-8 and replace specific characters to comply with URL encoding specifications. 
     *
     * @param str The string to be URL-encoded. 
     * @return The URL-encoded string. Plus signs (+) are replaced with %20, asterisks (*) are replaced with %2A, and %7E is replaced with tildes (~). 
     */
    public static String percentCode(String str) {
        if (str == null) {
            throw new IllegalArgumentException("The specified string cannot be null.");
        }
        try {
            return URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 encoding is not supported.", e);
        }
    }
}

Python

You must manually install the pytz and requests libraries. Run the following commands on the terminal based on your Python version:

Python 3

pip3 install pytz
pip3 install requests

Python 2

pip install pytz
pip install requests
import hashlib
import hmac
import json
import os
import uuid
from collections import OrderedDict
from urllib.parse import urlencode, quote_plus

import pytz
import requests
from datetime import datetime


class Request:
    def __init__(self, http_method, canonical_uri, host, x_acs_action, x_acs_version):
        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()
        self.body = None

    def _init_headers(self):
        headers = OrderedDict()
        headers['host'] = self.host
        headers['x-acs-action'] = self.x_acs_action
        headers['x-acs-version'] = self.x_acs_version
        current_time = datetime.now(pytz.timezone('Etc/GMT'))
        headers['x-acs-date'] = current_time.strftime('%Y-%m-%dT%H:%M:%SZ')
        headers['x-acs-signature-nonce'] = str(uuid.uuid4())
        return headers

    def sorted_query_params(self):
        # Sort query parameters by parameter name in alphabetical order and return the encoded string.
        sorted_query_params = sorted(self.query_param.items(), key=lambda item: item[0])
        self.query_param = {k: v for k, v in sorted_query_params}

    def sorted_headers(self):
        # Sort request headers by header name in alphabetical order and return the encoded string.
        sorted_headers = sorted(self.headers.items(), key=lambda item: item[0])
        self.headers = {k: v for k, v in sorted_headers}


# Obtain the AccessKey ID and AccessKey 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')

ALGORITHM = 'ACS3-HMAC-SHA256'


def get_authorization(request):
    try:
        new_query_param = OrderedDict()
        process_object(new_query_param, '', request.query_param)
        request.query_param = new_query_param
        # Step 1: Construct a canonicalized request.
        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 '')
        request.headers['x-acs-content-sha256'] = hashed_request_payload
        request.sorted_headers()

        # Construct canonicalized headers and request headers that are used for signature calculation.
        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(k for k in 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 a string-to-sign.
        hashed_canonical_request = sha256_hex(canonical_request)
        string_to_sign = f'{ALGORITHM}\n{hashed_canonical_request}'
        print(string_to_sign)

        # Step 3: Calculate the signature string.
        signature = hmac256(ACCESS_KEY_SECRET.encode('utf-8'), string_to_sign).hex().lower()
        print(signature)

        # Step 4: Specify the Authorization header.
        authorization = f'{ALGORITHM} Credential={ACCESS_KEY_ID},SignedHeaders={signed_headers},Signature={signature}'
        print(authorization)
        request.headers['Authorization'] = authorization
    except Exception as e:
        print("Failed to get authorization")
        print(e)


def process_object(result_map, key, value):
    # No further processing is required for a null value.
    if value is None:
        return

    if key is None:
        key = ""

    # If the value is of the List type, traverse the list and perform recursion on each element.
    if isinstance(value, list):
        for i, item in enumerate(value):
            process_object(result_map, f"{key}.{i + 1}", item)
    elif isinstance(value, dict):
        # If the value is of the Map type, traverse the map and perform recursion on each key-value pair.
        for sub_key, sub_value in value.items():
            process_object(result_map, f"{key}.{sub_key}", sub_value)
    else:
        # If a key starts with a period (.), remove the period (.) to maintain the continuity of keys.
        if key.startswith("."):
            key = key[1:]

        # If a value is in the byte[] format, convert the value to a string encoded in UTF-8.
        if isinstance(value, bytes):
            result_map[key] = value.decode('utf-8')
        else:
            # Convert the values of other types to strings.
            result_map[key] = str(value)


def hmac256(key, msg):
    return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()


def sha256_hex(s):
    return hashlib.sha256(s.encode('utf-8')).hexdigest()


def call_api(request):
    url = f'https://{request.host}{request.canonical_uri}'
    if request.query_param:
        url += '?' + urlencode(request.query_param, doseq=True, safe='*')
    headers = {k: v for k, v in request.headers.items()}
    if request.body:
        data = request.body
    else:
        data = None

    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):
    return encoded_str.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')


if __name__ == "__main__":
    # Construct a request for an RPC API operation.
    http_method = "POST"
    canonical_uri = "/"
    host = "ecs.cn-hangzhou.aliyuncs.com"
    x_acs_action = "DescribeInstanceStatus"
    x_acs_version = "2014-05-26"
    request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # The RegionId parameter is of the String type.
    request.query_param['RegionId'] = 'cn-hangzhou'
    # The InstanceId parameter is of the Array type.
    request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"]

    # Construct a POST request for a ROA API operation.
    # http_method = "POST"
    # canonical_uri = "/clusters"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "CreateCluster"
    # x_acs_version = "2015-12-15"
    # request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # body = OrderedDict()
    # body["name"] = "testDemo"
    # body["region_id"] = "cn-beijing"
    # body["cluster_type"] = "Kubernetes"
    # body["vpcid"] = "vpc-2zeo42r27y4opXXXXXXXX"
    # body["service_cidr"] = "172.16.1.0/20"
    # body["security_group_id"] = "sg-2zec0dm6qi66XXXXXXXX"
    # request.body = json.dumps(body, separators=(',', ':'))
    # request.headers["content-type"] = "application/json; charset=utf-8"

    # Construct a GET request for a ROA API operation.
    # http_method = "GET"
    # # If the value of the canonicalUri parameter contains the value of the path parameter, encode the value of the path parameter by specifying percent_code({Value of the path parameter}).
    # cluster_id_encode = percent_code("cb7cd6b9bde934f6193801878XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}/resources"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DescribeClusterResources"
    # x_acs_version = "2015-12-15"
    # request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # request.query_param['with_addon_resources'] = True

    request.sorted_query_params()
    get_authorization(request)
    call_api(request)

Go

You must run the following commands on 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         string
	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
}

var (
	AccessKeyId     = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
	AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
	ALGORITHM       = "ACS3-HMAC-SHA256"
)

func main() {
	// Construct a request for an RPC API operation.
	httpMethod := "POST"
	canonicalUri := "/"
	host := "ecs.cn-hangzhou.aliyuncs.com"
	xAcsAction := "DescribeInstanceStatus"
	xAcsVersion := "2014-05-26"
	req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	req.queryParam["RegionId"] = "cn-hangzhou"
	instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
	req.queryParam["InstanceId"] = instanceIds

	// Construct a POST request for a ROA API operation.
	// httpMethod := "POST"
	// canonicalUri := "/clusters"
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "CreateCluster"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// body := make(map[string]string)
	// body["name"] = "testDemo"
	// body["region_id"] = "cn-beijing"
	// body["cluster_type"] = "Kubernetes"
	// body["vpcid"] = "vpc-2zeo42r27y4opXXXXXXXX"
	// body["service_cidr"] = "172.16.1.0/20"
	// body["security_group_id"] = "sg-2zec0dm6qi66XXXXXXXX"
	// jsonBytes, err := json.Marshal(body)
	// if err != nil {
	// 	fmt.Println("Error marshaling to JSON:", err)
	// 	return
	// }
	// req.body = string(jsonBytes)
	// req.headers["content-type"] = "application/json; charset=utf-8"

	// Construct a GET request for a ROA API operation.
	// httpMethod := "GET"
	// // If the value of the canonicalUri parameter contains the value of the path parameter, encode the value of the path parameter by specifying percentCode({Value of the path parameter}).
	// canonicalUri := "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/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"

	// Sign the request.
	getAuthorization(req)
	// Call the API operation.
	err := callAPI(req)
	if err != nil {
		println(err.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(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) {
	// Flatten the query parameters of the List and Map types.
	newQueryParams := make(map[string]interface{})
	processObject(newQueryParams, "", req.queryParam)
	req.queryParam = newQueryParams
	// Step 1: Construct a canonicalized request.
	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)

	hashedRequestPayload := sha256Hex(req.body)
	req.headers["x-acs-content-sha256"] = hashedRequestPayload

	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 a string-to-sign.
	hashedCanonicalRequest := sha256Hex(canonicalRequest)
	stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
	fmt.Printf("stringToSign========>\n%s\n", stringToSign)

	// Step 3: Calculate the signature string.
	byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
	if err != nil {
		fmt.Println(err)
	}
	signature := strings.ToLower(hex.EncodeToString(byteData))

	// Step 4: Specify the Authorization header.
	authorization := ALGORITHM + " Credential=" + AccessKeyId + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
	fmt.Printf("authorization========>%s\n", authorization)
	req.headers["Authorization"] = authorization
}

func hmac256(key []byte, toSignString string) ([]byte, error) {
	// Obtain the object on which the HMAC-SHA256 algorithm is implemented.
	h := hmac.New(sha256.New, key)
	// Construct a string-to-sign.
	_, err := h.Write([]byte(toSignString))
	if err != nil {
		return nil, err
	}
	// Calculate the hash value of the signature string and return the hash value.
	return h.Sum(nil), nil
}

func sha256Hex(str string) string {
	// Obtain the object on which the SHA-256 algorithm is implemented.
	hash := sha256.New()
	// Write the signature string to the hash function.
	_, _ = hash.Write([]byte(str))
	// Calculate the hash value of the signature string by using the SHA-256 algorithm and return the hash value as a string in the hexadecimal format in lowercase letters.
	hexString := hex.EncodeToString(hash.Sum(nil))

	return hexString
}

func percentCode(str string) string {
	// Replace specific characters.
	str = strings.ReplaceAll(str, "+", "%20")
	str = strings.ReplaceAll(str, "*", "%2A")
	str = strings.ReplaceAll(str, "%7E", "~")
	return str
}

// processObject: the objects on which recursion is performed. Complex objects such as maps and lists are recursively decomposed into 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

In this example, JavaScript is used.

const crypto = require('crypto');

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;

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 a canonicalized request.
        const canonicalQueryString = Object.entries(signRequest.queryParam)
            .sort(([a], [b]) => a.localeCompare(b))
            .map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
            .join('&');

        // The request body. If the request body is empty, such as in a GET request, use an empty string as the value of the RequestPayload parameter.
        const requestPayload = signRequest.body || '';
        const hashedRequestPayload = sha256Hex(requestPayload);
        signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;

        // Convert all header names to lowercase letters.
        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();
        // The request headers that are used for signature calculation. Use semicolons (;) to concatenate all the headers by lowercase header name in alphabetical order.
        const signedHeaders = sortedKeys.join(";")
        // Construct request headers. Concatenate multiple canonicalized request headers by lowercase header name in alphabetical order.
        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 a string-to-sign.
        const hashedCanonicalRequest = sha256Hex(canonicalRequest);
        const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
        console.log('stringToSign=========>', stringToSign);

        // Step 3: Calculate the signature string.
        const signature = hmac256(accessKeySecret, stringToSign);
        console.log('signature=========>', signature);

        // Step 4: Specify 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}`;
        // Specify 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
        };

        // Process the 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(str) {
    if (str === null || str === undefined) {
        throw new Error('The input string is invalid. ');
    }
    const hash = crypto.createHash('sha256');
    const digest = hash.update(str, 'utf8').digest('hex');
    return digest.toLowerCase();
}

function processObject(map, key, value) {
    // No further processing is required for a null value.
    if (value === null) {
        return;
    }
    if (key === null) {
        key = "";
    }

    // If the value is of the Array type, traverse the array and perform recursion on each element.
    if (Array.isArray(value)) {
        value.forEach((item, index) => {
            processObject(map, `${key}.${index + 1}`, item);
        });
    } else if (typeof value === 'object' && value !== null) {
        // If the value is of the Object type, traverse the object and perform recursion on each key-value pair.
        Object.entries(value).forEach(([subKey, subValue]) => {
            processObject(map, `${key}.${subKey}`, subValue);
        });
    } else {
        // If a key starts with a period (.), remove the period (.) to maintain the continuity of keys.
        if (key.startsWith('.')) {
            key = key.slice(1);
        }
        map[key] = String(value);
    }
}


// Example 1: Construct a request for an RPC API operation.
const httpMethod = 'GET';
const canonicalUri = '/';
const host = 'ecs.cn-hangzhou.aliyuncs.com';
const xAcsAction = 'DescribeInstanceStatus';
const xAcsVersion = '2014-05-26';
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
signRequest.queryParam = {
    RegionId: 'cn-hangzhou',
    InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}

// Example 2: Construct a POST request for a ROA API operation.
// 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);
// const body = {
//     name: 'testDemo',
//     region_id: 'cn-beijing',
//     cluster_type: 'ExternalKubernetes',
//     vpcid: 'vpc-2zeo42r27y4opXXXXXXXX',
//     service_cidr: '172.16.3.0/20',
//     security_group_id: 'sg-2zeh5ta2iklXXXXXXXX',
//     vswitch_ids: [
//         'vsw-2ze3aagwn397gXXXXXXXX'
//       ],
// }
// signRequest.body = JSON.stringify(body)
// // Specify the Content-Type header based on your business requirements. In this example, JSON is used.
// signRequest.headers['content-type'] = 'application/json';

// Example 3: Construct a GET request for a ROA API operation.
// const httpMethod = 'GET';
// // If the value of the canonicalUri parameter contains the value of the path parameter, encode the value of the path parameter by specifying percentCode({Value of the path parameter}).
// const canonicalUri = '/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/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,
// }

getAuthorization(signRequest);
callApi(signRequest).then(r => {
    console.log(r);
}).catch(error => {
    console.error(error);
});

PHP

<?php

class SignatureDemo
{
    private $ALGORITHM;
    private $AccessKeyId;
    private $AccessKeySecret;

    public function __construct()
    {
        date_default_timezone_set('UTC'); // Set the time zone to GMT.
        $this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // Obtain the AccessKey ID of the RAM user from environment variables.
        $this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // Obtain the AccessKey secret of the RAM user from environment variables.
        $this->ALGORITHM = 'ACS3-HMAC-SHA256'; // Specify the encryption method.
    }

    public function main()
    {
        // Construct a request for an RPC API operation.
        $request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
        $request['queryParam'] = [
            'RegionId' => 'cn-hangzhou',
            'InstanceId' => ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"]
        ];

        // Construct a POST request for a ROA API operation.
        // $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
        // $this->addRequestBody($request, [
        //     'name' => 'testDemo',
        //     'region_id' => 'cn-beijing',
        //     'cluster_type' => 'ExternalKubernetes',
        //     'vpcid' => 'vpc-2zeo42r27y4opXXXXXXXX',
        //     'service_cidr' => '172.16.5.0/20',
        //     'security_group_id' => 'sg-2zeh5ta2ikljXXXXXXXX',
        //     "vswitch_ids" => [
        //         "vsw-2zeuntqtklsk0XXXXXXXX"
        //     ],
        // ]);

        // Construct a GET request for a ROA API operation.
        // If the value of the canonicalUri parameter contains the value of the path parameter, encode the value of the path parameter by specifying rawurlencode({Value of the path parameter}).
        // $cluster_id = 'cb7cd6b9bde934f6193801878XXXXXXXX';
        // $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,
        // ];

        $this->getAuthorization($request);
        $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 addRequestBody(&$request, $bodyData)
    {
        $request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
        $request['headers']['content-type'] = 'application/json; charset=utf-8';
    }

    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;

        $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 the request by using cURL.
            $url = "https://" . $request['host'] . $request['canonicalUri'];

            // Add the request parameters to the URL.
            if (!empty($request['queryParam'])) {
                $url .= '?' . http_build_query($request['queryParam']);
            }

            echo $url;
            // Initialize a cURL session.
            $ch = curl_init();

            // Set cURL options.
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL certificate verification. This increases security risks. We recommend that you do not disable SSL certificate verification in production environments.   
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the results but do not output the results.
            curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // Add request headers.

            // Set cURL options based on the 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 whether an error occurs.
            if (curl_errno($ch)) {
                echo "Failed to send request: " . curl_error($ch);
            } else {
                echo $result;
            }

        } catch (Exception $e) {
            echo "Error: " . $e->getMessage();
        } finally {
            // Close the cURL session.
            curl_close($ch);
        }
    }

    function processObject($value)
    {
        // No further processing is required for a null value.
        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

using System.Globalization;
using System.Net;
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 string 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 = "";
            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 environment variable ALIBABA_CLOUD_ACCESS_KEY_ID is not specified.");
        private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ??  throw new InvalidOperationException("The environment variable ALIBABA_CLOUD_ACCESS_KEY_SECRET is not specified.");
        private const string Algorithm = "ACS3-HMAC-SHA256";
        private const string ContentType = "content-type";

        public static void Main(string[] args)
        {
            // Construct a request for an RPC API operation.
            string httpMethod = "POST"; // In most cases, you can use the POST or GET method to call an RPC API operation.
            string canonicalUri = "/";
            string host = "ecs.cn-hangzhou.aliyuncs.com";
            string xAcsAction = "DescribeInstanceStatus";
            string xAcsVersion = "2014-05-26";
            var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            request.QueryParam["RegionId"] = "cn-hangzhou"; // The RegionId parameter is of the String type.
            List<string> instanceIds = ["i-bp10igfmnytt9zhth92h", "i-bp1incuofvzxww9rcz80", "i-bp1incuofvzxww9rcz7z"];
            request.QueryParam["InstanceId"] = instanceIds; // The InstanceId parameter is of the Array type.

            // // Construct a POST request for a ROA API operation.
            // String httpMethod = "POST";
            // String canonicalUri = "/clusters";
            // String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
            // String xAcsAction = "CreateCluster"; // The operation that you want to perform.
            // String xAcsVersion = "2015-12-15"; // The version number of the API.
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // The request body. Use JsonConvert to convert the request body into a JSON string.
            // var body = new SortedDictionary<string, object>
            // {
            //     { "name", "testDemo" },
            //     { "region_id", "cn-beijing" },
            //     { "cluster_type", "ManagedKubernetes" },
            //     { "vpcid", "vpc-2zeo42r27y4opXXXXXXXX" },
            //     { "service_cidr", "172.16.1.0/20" },
            //     { "security_group_id", "sg-2zec0dm6qi66XXXXXXXX" }
            // };
            // string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
            // request.Body = jsonBody;
            // request.Headers[ContentType] = "application/json; charset=utf-8";

            // // Construct a GET request for a ROA API operation.
            // String httpMethod = "GET";
            // // If the value of the canonicalUri parameter contains the value of the path parameter, encode the value of the path parameter by specifying percentCode({Value of the path parameter}).
            // String canonicalUri = "/clusters/" + PercentCode("c3c2987c8beae4b1b85bef006xxxxxxxx") + "/resources";
            // String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
            // String xAcsAction = "DescribeClusterResources"; // The operation that you want to perform.
            // String xAcsVersion = "2015-12-15"; // The version number of the API.
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // request.QueryParam["with_addon_resources"]=true;

            // // Construct a DELETE request for a ROA API operation.
            // String httpMethod = "DELETE";
            // // If the value of the canonicalUri parameter contains the value of the path parameter, encode the value of the path parameter by specifying percentCode({Value of the path parameter}).
            // String canonicalUri = "/clusters/" + PercentCode("c3c2987c8beae4b1b85bef006xxxxxxxx");
            // String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
            // String xAcsAction = "DeleteCluster"; // The operation that you want to perform.
            // String xAcsVersion = "2015-12-15"; // The version number of the API.
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

            GetAuthorization(request);
            var result = CallApiAsync(request);
            Console.WriteLine($"result:{result.Result}");
        }

        private static async Task<string?> CallApiAsync(Request request)
        {
            try
            {
                // Create an HttpClient instance.
                using var httpClient = new HttpClient();

                // Construct a URL.
                string url = $"https://{request.Host}{request.CanonicalUri}";
                var uriBuilder = new UriBuilder(url);
                var query = new List<string>();

                // Specify 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
                {
                    RequestUri = uriBuilder.Uri,
                };

                // Configure request headers.
                foreach (var entry in request.Headers)
                {
                    if (entry.Key == "Authorization")
                    {
                        requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ;
                    }
                    else if (entry.Key == ContentType) // The value of the ContentType key must be consistent with that defined in the main method.
                    {
                        continue;
                    }
                    else
                    {
                        requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
                    }
                }

                // Send the request.
                HttpResponseMessage response;
                switch (request.HttpMethod.ToUpper())
                {
                    case "GET":
                        response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
                        break;
                    case "POST":
                        requestMessage.Method = HttpMethod.Post;
                        requestMessage.Content = new StringContent(request.Body ??  "", Encoding.UTF8, "application/json");
                        response = await httpClient.SendAsync(requestMessage);
                        break;
                    case "DELETE":
                        requestMessage.Method = HttpMethod.Delete;
                        response = await httpClient.SendAsync(requestMessage);
                        break;
                    default:
                        Console.WriteLine("Unsupported HTTP method: " + request.HttpMethod);
                        throw new ArgumentException("Unsupported HTTP method");
                }

                // Read the content of the returned response.
                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
            {
                // Flatten the query parameters of the List and Map types.
                request.QueryParam = FlattenDictionary(request.QueryParam);

                // Step 1: Construct a canonicalized request.
                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() ??  "")}");
                }

                string requestPayload = string.IsNullOrEmpty(request.Body) ?  "" : request.Body;
                string hashedRequestPayload = Sha256Hash(requestPayload);
                request.Headers["x-acs-content-sha256"] = hashedRequestPayload;

                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 a string-to-sign.
                string hashedCanonicalRequest = Sha256Hash(canonicalRequest);
                string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
                Console.WriteLine($"stringToSign:{stringToSign}");

                // Step 3: Calculate the signature string.
                string signature = HmacSha256(AccessKeySecret, stringToSign);

                // Step 4: Specify 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 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(string input)
        {
            byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
            return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        }

        private static string PercentCode(string str)
        {
            if (string.IsNullOrEmpty(str))
            {
                throw new ArgumentException("The specified string cannot be null or empty");
            }
            return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
        }
    }
}

Shell

#!/bin/bash

accessKey_id="<YOUR-ACCESSKEY-ID>"
accessKey_secret="<YOUR-ACCESSKEY-SECRET>"
algorithm="ACS3-HMAC-SHA256"

# The request parameters.
httpMethod="POST"
host="ecs.cn-hangzhou.aliyuncs.com"
queryParam=("RegionId=cn-hangzhou" "InstanceId=(\"i-bp10igfmnyttXXXXXXXX\" \"i-bp1incuofvzxXXXXXXXX\" \"i-bp1incuofvzxXXXXXXXX\")")
action="DescribeInstanceStatus"
version="2014-05-26"
canonicalURI="/"
RequestPayload=""

# Specify the time in the ISO 8601 standard in UTC.
utc_timestamp=$(date +%s)
utc_date=$(date -u -d @${utc_timestamp} +"%Y-%m-%dT%H:%M:%SZ") 
# Set the x-acs-signature-nonce header to a random number.
random=$(uuidgen | sed 's/-//g') 

# Add the request headers that are used for signature calculation.
headers="host:${host}
x-acs-action:${action}
x-acs-version:${version}
x-acs-date:${utc_date}
x-acs-signature-nonce:${random}"

# Step 1: Construct a canonicalized request.
# Flatten all query parameters.
newQueryParam=()

# Traverse each raw query parameter.
for param in "${queryParam[@]}"; do
    # Check whether the value of a query parameter contains an equal sign (=). If the parameter value contains an equal sign (=), the parameter value is a key-value pair.
    if [[ "$param" == *"="* ]]; then
        # Split the parameter value into a key and value.
        IFS='=' read -r key value <<< "$param"

        # Check whether the value is a list. If the value is a list, the value is in parentheses ().
        if [[ "$value" =~ ^\(.+\)$ ]]; then
            # Remove the parentheses ().
            value="${value:1:-1}"

            # Use a specific internal field separator (IFS) to split values.
            IFS=' ' read -ra values <<< "$value"

            # Add an index for each value.
            index=1
            for val in "${values[@]}"; do
                # Remove double quotation marks (").
                val="${val%\"}"
                val="${val#\"}"

                # Add the query parameter to a new array.
                newQueryParam+=("$key.$index=$val")
                ((index++))
            done
        else
            # If the value is not a list, add the query parameter to the new array.
            newQueryParam+=("$param")
        fi
    else
        # If the value is not in parentheses, add the query parameter to the new array without modifications.
        newQueryParam+=("$param")
    fi
done

# Process and sort new query parameters.
sortedParams=()
declare -A paramsMap
for param in "${newQueryParam[@]}"; do
    IFS='=' read -r key value <<< "$param"
    paramsMap["$key"]="$value"
done
# Sort query parameters by key.
for key in $(echo ${!paramsMap[@]} | tr ' ' '\n' | sort); do
    sortedParams+=("$key=${paramsMap[$key]}")
done

#1.1 Construct a canonicalized query string.
canonicalQueryString=""
first=true
for item in "${sortedParams[@]}"; do
    [ "$first" = true ] && first=false || canonicalQueryString+="&"
    # Check whether 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 canonicalized 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 a canonicalized request.
canonicalRequest="${httpMethod}\n${canonicalURI}\n${canonicalQueryString}\n${canonicalHeaders}\n\n${signedHeaders}\n${hashedRequestPayload}"
echo -e "canonicalRequest=${canonicalRequest}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

hashedCanonicalRequest=$(printf "${canonicalRequest}" | openssl sha256 -hex | awk '{print $2}')
# Step 2: Construct a string-to-sign.
stringToSign="${algorithm}\n${hashedCanonicalRequest}"
echo -e "stringToSign=$stringToSign"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# Step 3: Calculate the signature string.
signature=$(printf "${stringToSign}" | openssl dgst -sha256 -hmac "${accessKey_secret}" | sed 's/^.* //')
echo -e "signature=${signature}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# Step 4: Specify the Authorization header.
authorization="${algorithm} Credential=${accessKey_id},SignedHeaders=${signedHeaders},Signature=${signature}"
echo -e "authorization=${authorization}"

# Construct a cURL command.
url="https://$host$canonicalURI"
curl_command="curl -X $httpMethod '$url?$canonicalQueryString'"

# Add the Authorization header.
IFS=$'\n'  # Use line feed (\n) as the new IFS.
for header in $headers; do
    curl_command="$curl_command -H '$header'"
done
curl_command+=" -H 'Authorization:$authorization'"

echo "$curl_command"
# Run the cURL command.
eval "$curl_command"

Fixed parameter values

In this example, fixed parameter values are used to help you verify whether the signature method is properly used. You can calculate the signature string based on the following fixed parameter values. If the signature string that you generate is the same as the signature string generated in this example, your signature calculation process is correct.

Parameter

Value

AccessKeyID

YourAccessKeyId

AccessKeySecret

YourAccessKeySecret

x-acs-signature-nonce

3156853299f313e23d1673dc12e1703d

x-acs-date

2023-10-26T10:22:32Z

You can perform the following steps to sign a request:

  1. Construct a canonicalized 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
  1. Construct a string-to-sign.

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
  1. Calculate the signature string.

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
  1. Add the signature string 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

FAQ

What do I do if a signature fails to be verified and the "Specified signature does not match our calculation." or "The request signature does not conform to Aliyun standards." error message is returned?

Cause: In most cases, a signature fails to be verified because required operations are not performed during the signature calculation process. For example, the parameters or the request headers that are used for signature calculation are not sorted by name in alphabetical order.

Solution: Check whether the implementation of the signature method follows the steps outlined in the signature calculation examples provided in this topic. Check whether the returned content at each step is consistent with that in the signature calculation examples. If inconsistencies occur, read the description of the corresponding steps in the signature calculation examples to ensure that your signature calculation process is correct.

In self-signed mode, if I use the GET method to call an API operation during debugging and the call is successful, can I use the POST method to call the API operation?

In most cases, you can use the POST or GET method to call an RPC API operation. However, you can use only one method to call a ROA API operation. For more information about the request methods that can be used to call API operations, see API metadata.

Contact us

If you have any questions during the signature calculation process, join the DingTalk group (ID: 78410016550) to contact for technical support.