V3版本请求体&签名机制

更新时间:

V3版本通过公共请求头设置接口所需的参数信息,在签名机制的实现上消除了接口风格的差异,从而实现了更标准、更简化的设计。本文提供了详细的指南,以帮助您了解和实施阿里云SDK V3版的请求结构及签名过程。您将学习如何构造标准的HTTP请求,以及如何使用正确的签名算法来验证请求的身份,以确保传输数据的完整性和安全性。如您希望自行研发阿里云OpenAPI的请求签名,本文将为您提供相关参考。

说明

在OpenAPI门户上提供了阿里云SDK的相关产品,这些产品所提供的API支持使用V3签名。

HTTP 请求结构

一个完整的阿里云 OpenAPI 请求,包含以下部分。

名称

是否必选

描述

示例值

协议

您可以查阅不同云产品的 API 参考文档进行配置。支持通过HTTPHTTPS协议进行请求通信。为了获得更高的安全性,推荐您使用HTTPS协议发送请求。取值范围为https://或者 http://

https://

服务地址

即 Endpoint。您可以查阅不同云产品的服务接入地址文档,查阅不同服务区域下的服务地址。

cs.aliyuncs.com

resource_URI_parameters(接口URL)

接口URL,包括接口路径和位置在 path、 query的接口请求参数。

/clusters/{cluster_id}/triggers

RequestHeader(请求头信息)

请求头信息,通常包含API的版本、Host、Authorization等信息。后文将详细说明。

x-acs-action

RequestBody

定义在 body 中的业务请求参数,建议您在阿里云 OpenAPI 开发者门户进行试用。

cluster_id

HTTPMethod

请求使用的方法,ROA接口请求方法包括PUT、POST、GET、DELETE。

POST

RequestHeader(公共请求头)

一个完整的阿里云 OpenAPI 请求,包含以下部分。

名称

类型

是否必选

描述

示例值

x-acs-action

String

API的名称。您可以访问阿里云 OpenAPI 开发者门户,搜索您想调用的 OpenAPI。

RunInstances

x-acs-version

String

API 版本。您可以访问阿里云 OpenAPI 开发者门户,查看您调用 OpenAPI 对应的 API 版本。

2014-05-26

Authorization

String

非匿名请求必须

用于验证请求合法性的认证信息,格式为Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature

其中SignatureAlgorithm为签名加密方式,为ACS3-HMAC-SHA256。

Credential 为用户的访问密钥ID。您可以在RAM 控制台查看您的 AccessKeyId。如需创建 AccessKey,请参见创建AccessKey

SignedHeaders为请求头中包含的参与签名字段键名,【说明】:除了Authorization之外,建议对所有公共请求头添加签名,以提高安全性。

Signature为请求签名,取值参见签名机制。

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

签名唯一随机数。该随机数用于防止网络重放攻击,每一次请求都必须使用不同的随机数。

d410180a5abf7fe235dd9b74aca91fc0

x-acs-date

String

按照ISO 8601标准表示的UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ,例如2018-01-01T12:00:00Z。值为请求发出前15分钟内的时间。

2023-10-26T09:01:01Z

host

String

即服务地址,参见HTTP 请求结构

ecs.cn-shanghai.aliyuncs.com

x-acs-content-sha256

String

请求正文Hash摘要后再base-16编码的结果,与HashedRequestPayload一致。

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-security-token

String

STS认证必传

为调用AssumeRole接口返回值中SecurityToken的值。

签名机制

为保证API的安全调用,在调用API时阿里云会对每个API请求通过签名(Signature)进行身份验证。无论使用HTTP还是HTTPS协议提交请求,都需要在请求中包含签名信息。

重要

请求及返回结果都使用UTF-8字符集进行编码。

对于每一次HTTP或者HTTPS协议请求,阿里云会根据访问中的签名信息验证访问请求者身份。您在访问时签名信息时,请按照以下方法对请求进行签名处理:

步骤一:构造规范化请求

使用AK/SK方式进行签名与认证,首先需要规范请求内容,然后再进行签名。客户端与云服务API网关使用相同的请求规范,可以确保同一个HTTP请求的前后端得到相同的签名结果,从而完成身份校验。

构造规范化请求(CanonicalRequest)的伪代码如下:

CanonicalRequest =
  HTTPRequestMethod + '\n' +    //http请求方法,全大写
  CanonicalURI + '\n' +         //规范化URI
  CanonicalQueryString + '\n' + //规范化查询字符串
  CanonicalHeaders + '\n' +     //规范化消息头
  SignedHeaders + '\n' +        //已签名消息头
  HashedRequestPayload			
  • HTTPRequestMethod

    请求方法,即大写的HTTP方法名,如GET、POST。

  • CanonicalURI

    规范化URI,是指URL的资源路径部分经过编码之后的结果。资源路径部分指URL中host与查询字符串之间的部分,包含host之后的/但不包含查询字符串前的?。用户发起请求时的URI应使用规范化URI,编码方式使用UTF-8字符集按照RFC3986的规则对URI中的每一部分(即被/分割开的字符串)进行编码:

    • 字符A~Z、a~z、0~9以及字符-_.~不编码。

    • 其他字符编码成%加字符对应ASCII码的16进制。示例:半角双引号(")对应%22

    • 空格( )编码成%20,而不是加号(+)、星号(*)替换为%2A%7E替换为波浪号(~)。

      如果您使用的是Java标准库中的java.net.URLEncoder,可以先用标准库中encode编码,随后将编码后的字符中加号(+)替换为%20、星号(*)替换为%2A%7E替换为波浪号(~),即可得到上述规则描述的编码字符串。

    重要

    RPC风格API使用正斜杠(/)作为CanonicalURI,

    ROA风格API该参数为元数据文件中path的值,例如/api/v1/clusters。

  • CanonicalQueryString

    规范化查询字符串,构造方法如下:

    1. 将查询字符串中的参数按照参数名的字符代码升序排列,具有重复名称的参数应按值进行排序。

    2. 使用UTF-8字符集按照RFC3986的规则对每个参数的参数名和参数值分别进行URI编码,具体规则与上一节中的CanonicalURI编码规则相同。

    3. 使用等号(=)连接编码后的请求参数名和参数值,对于没有值的参数使用空字符串。

    4. 多个请求参数之间使用与号(&)连接。

    重要

    当请求的查询字符串为空时,使用空字符串作为规范化查询字符串。

  • CanonicalHeaders

    规范化请求头,是一个非标准HTTP头部信息。需要将请求中包含以x-acs-为前缀、hostcontent-type的参数信息,添加到规范化请求头中,构造方法如下:

    1. 将所有需要签名的参数的名称转换为小写。

    2. 将所有参数按照参数名称的字符顺序以升序排列。

    3. 将参数的值去除首尾空格。对于有多个值的参数,将多个值分别去除首尾空格后按值升序排列,然后用逗号(,)连接。

    4. 将步骤2、3的结果以英文冒号(:)连接,并在尾部添加换行符,组成一个规范化消息头(CanonicalHeaderEntry)。

    5. 如果没有需要签名的消息头,使用空字符串作为规范化消息头列表。

    重要

    除Authorization外的所有公共请求头,只要符合要求的参数都必须被加入签名。

    伪代码如下:

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

    已签名消息头列表,用于说明此次请求中参与签名的消息头,与CanonicalHeaders中包含的消息头一一对应。其构造方法如下:

    • 将CanonicalHeaders中包含的请求头的名称转为小写。

    • 多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔,例如 content-type;host;x-acs-date

    伪代码如下:

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

    当请求体(body)为空时,RequestPayload固定为空字符串,否则RequestPayload的值为请求体(body)对应的JSON字符串。再使用哈希函数对RequestPayload进行转换得到HashedRequestPayload,转换规则用伪代码可表示为 HashedRequestPayload = HexEncode(Hash(RequestPayload))

    • Hash表示消息摘要函数,目前支持SHA256算法,例如,当签名协议使用ACS3-HMAC-SHA256时,应使用SHA256作为Hash函数。

    • HexEncode表示以小写的十六进制的形式返回摘要的编码函数(即Base16编码)。

表1:签名协议与签名算法、摘要函数的对应关系

签名协议(SignatureAlgorithm)

处理RequestPayload以及CanonicalRequest时使用的摘要函数(Hash)

计算签名时实际使用的签名算法

(SignatureMethod)

ACS3-HMAC-SHA256

SHA256

HMAC-SHA256

步骤二:构造待签名字符串

按照以下伪代码构造待签名字符串(stringToSign):

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

    签名协议目前支持ACS3-HMAC-SHA256算法,已不再支持基于MD5或SHA1的算法。

  • HashedCanonicalRequest

    规范化请求摘要串,计算方法伪代码如下:

    HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
  1. 使用哈希函数(Hash)对步骤一中得到的规范化请求(CanonicalRequest)进行摘要处理,具体使用的Hash函数取决于签名协议(SignatureAlgorithm),参见表1,例如,当签名协议为ACS3-HMAC-SHA256时,应使用SHA256作为Hash函数。

  2. 将上一步得到的摘要结果以小写的十六进制形式编码。

步骤三:计算签名

按照以下伪代码计算签名值(Signature)。

Signature = HexEncode(SignatureMethod(Secret, StringToSign))
  • StringToSign:步骤二中构造的待签名字符串,UTF-8编码。

  • SignatureMethod:签名算法,具体使用的算法取决于签名协议(SignatureAlgorithm),其对应关系如表1。

  • Secret:用户的签名密钥,为二进制数据。

  • HexEncode:以小写的十六进制的形式返回摘要的编码函数(即Base16编码)。

步骤四:将签名添加到请求中

计算完签名后,构造Authorization请求头,格式为:Authorization:<SignatureAlgorithm> Credential=<AccessKeyId>,SignedHeaders=<SignedHeaders>,Signature=<Signature>示例如下:

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

签名示例

说明
  1. 为了让您更清晰地理解上述签名机制,下面以主流编程语言为例,将签名机制完整实现。示例代码只是让您更好地理解签名机制,存在不通用性,阿里云OpenAPI提供多种编程语言和开发框架的SDK,使用这些SDK可以免去签名过程,便于您快速构建与阿里云相关的应用程序,建议您使用SDK。

  2. 在签名之前,请先查看OpenAPI元数据,获取API的请求方式、请求参数以及参数位置等信息。

Java示例

运行Java示例,需要您在pom.xml中添加以下Maven依赖。

<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 {

    /**
     * 日期格式化工具,用于将日期时间字符串格式化为"yyyy-MM-dd'T'HH:mm:ss'Z'"的格式。
     */
    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;
        // 请求路径,当资源路径为空时,使用正斜杠(/)作为CanonicalURI
        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<>();
        // 调用API所需要的参数,参数位置在body。Json字符串
        String body;
        // 调用API所需要的参数,参数位置在query,参数按照参数名的字符代码升序排列
        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")); // 设置日期格式化时区为GMT
            headers.put("x-acs-date", SDF.format(new Date()));
            headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
        }
    }

    /**
     * 这里通过环境变量获取Access Key ID和Access Key Secret,
     */
    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");

    /**
     * 签名协议
     */
    private static final String ALGORITHM = "ACS3-HMAC-SHA256";

    /**
     * 通过给定的参数构建请求,并进行签名认证,最终发起请求
     */
    public static void main(String[] args) {
        // RPC接口请求
        String httpMethod = "POST"; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
        String canonicalUri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
        String host = "ecs.cn-hangzhou.aliyuncs.com";  // endpoint
        String xAcsAction = "DescribeInstanceStatus";  // API名称
        String xAcsVersion = "2014-05-26"; // API版本号
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // 调用API所需要的参数,参数按照参数名的字符代码升序排列,具有重复名称的参数应按值进行排序。
        request.queryParam.put("RegionId", "cn-hangzhou"); // RegionId的参数类型是String
        String[] instanceIds = {"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"};
        request.queryParam.put("InstanceId", Arrays.asList(instanceIds)); // InstanceId的参数类型是array

        /*// ROA接口POST请求
        String httpMethod = "POST";
        String canonicalUri = "/clusters";
        String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
        String xAcsAction= "CreateCluster"; // API名称
        String xAcsVersion=  "2015-12-15"; // API版本号
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // 请求body,通过Gson将body转成JSON字符串,以下参数仅做示例
        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");*/

        /*// ROA接口GET请求
        String httpMethod = "GET";
        // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
        String canonicalUri = "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources";
        String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
        String xAcsAction = "DescribeClusterResources"; // API名称
        String xAcsVersion = "2015-12-15"; // API版本号
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        request.queryParam.put("with_addon_resources", true);*/

        /*// ROA接口DELETE请求
        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);*/

        // 签名过程
        getAuthorization(request);
        // 调用API
        callApi(request);
    }

    private static void callApi(Request request) {
        try {
            // 通过HttpClient发送请求
            String url = "https://" + request.host + request.canonicalUri;
            URIBuilder uriBuilder = new URIBuilder(url);
            // 添加请求参数
            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");
            }

            // 添加http请求头
            for (Map.Entry<String, Object> entry : request.headers.entrySet()) {
                httpRequest.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
            }
            // 发送请求
            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) {
                // 异常处理
                System.out.println("Failed to send request");
                e.printStackTrace();
            }
        } catch (URISyntaxException e) {
            // 异常处理
            System.out.println("Invalid URI syntax");
            e.printStackTrace();
        }
    }

    /**
     * 该方法用于根据传入的HTTP请求方法、规范化的URI、查询参数等,计算并生成授权信息。
     */
    private static void getAuthorization(Request request) {
        try {
            // 处理queryParam中参数值为List、Map类型的参数,将参数平铺
            TreeMap<String, Object> newQueryParam = new TreeMap<>();
            processObject(newQueryParam, "", request.queryParam);
            request.queryParam = newQueryParam;
            // 步骤 1:拼接规范请求串
            // 请求参数,当请求的查询字符串为空时,使用空字符串作为规范化查询字符串
            StringBuilder canonicalQueryString = new StringBuilder();
            request.queryParam.entrySet().stream().map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
                // 如果canonicalQueryString已经不是空的,则在查询参数前添加"&"
                if (canonicalQueryString.length() > 0) {
                    canonicalQueryString.append("&");
                }
                canonicalQueryString.append(queryPart);
            });
            // 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
            String requestPayload = "";
            if (request.body != null) {
                requestPayload = request.body;
            }

            // 计算请求体的哈希值
            String hashedRequestPayload = sha256Hex(requestPayload);
            request.headers.put("x-acs-content-sha256", hashedRequestPayload);
            // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
            StringBuilder canonicalHeaders = new StringBuilder();
            // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
            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);

            // 步骤 2:拼接待签名字符串
            String hashedCanonicalRequest = sha256Hex(canonicalRequest); // 计算规范化请求的哈希值
            String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
            System.out.println("stringToSign=========>\n" + stringToSign);

            // 步骤 3:计算签名
            String signature = DatatypeConverter.printHexBinary(hmac256(ACCESS_KEY_SECRET.getBytes(StandardCharsets.UTF_8), stringToSign)).toLowerCase();
            System.out.println("signature=========>" + signature);

            // 步骤 4:拼接 Authorization
            String authorization = ALGORITHM + " " + "Credential=" + ACCESS_KEY_ID + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature;
            System.out.println("authorization=========>" + authorization);
            request.headers.put("Authorization", authorization);
        } catch (Exception e) {
            // 异常处理
            System.out.println("Failed to get authorization");
            e.printStackTrace();
        }
    }

    /**
     * 递归处理对象,将复杂对象(如Map和List)展开为平面的键值对
     *
     * @param map   原始的键值对集合,将被递归地更新
     * @param key   当前处理的键,随着递归的深入,键会带有嵌套路径信息
     * @param value 对应于键的值,可以是嵌套的Map、List或其他类型
     */
    private static void processObject(Map<String, Object> map, String key, Object value) {
        // 如果值为空,则无需进一步处理
        if (value == null) {
            return;
        }
        if (key == null) {
            key = "";
        }
        // 当值为List类型时,遍历List中的每个元素,并递归处理
        if (value instanceof List<?>) {
            List<?> list = (List<?>) value;
            for (int i = 0; i < list.size(); ++i) {
                processObject(map, key + "." + (i + 1), list.get(i));
            }
        } else if (value instanceof Map<?, ?>) {
            // 当值为Map类型时,遍历Map中的每个键值对,并递归处理
            Map<?, ?> subMap = (Map<?, ?>) value;
            for (Map.Entry<?, ?> entry : subMap.entrySet()) {
                processObject(map, key + "." + entry.getKey().toString(), entry.getValue());
            }
        } else {
            // 对于以"."开头的键,移除开头的"."以保持键的连续性
            if (key.startsWith(".")) {
                key = key.substring(1);
            }
            // 对于byte[]类型的值,将其转换为UTF-8编码的字符串
            if (value instanceof byte[]) {
                map.put(key, new String((byte[]) value, StandardCharsets.UTF_8));
            } else {
                // 对于其他类型的值,直接转换为字符串
                map.put(key, String.valueOf(value));
            }
        }
    }

    /**
     * 使用HmacSHA256算法生成消息认证码(MAC)。
     *
     * @param secretKey 密钥,用于生成MAC的密钥,必须保密。
     * @param str       需要进行MAC认证的消息。
     * @return 返回使用HmacSHA256算法计算出的消息认证码。
     * @throws Exception 如果初始化MAC或计算MAC过程中遇到错误,则抛出异常。
     */
    public static byte[] hmac256(byte[] secretKey, String str) throws Exception {
        // 实例化HmacSHA256消息认证码生成器
        Mac mac = Mac.getInstance("HmacSHA256");
        // 创建密钥规范,用于初始化MAC生成器
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, mac.getAlgorithm());
        // 初始化MAC生成器
        mac.init(secretKeySpec);
        // 计算消息认证码并返回
        return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 使用SHA-256算法计算字符串的哈希值并以十六进制字符串形式返回。
     *
     * @param stringToSign 需要进行SHA-256哈希计算的字符串。
     * @return 计算结果为小写十六进制字符串。
     * @throws Exception 如果在获取SHA-256消息摘要实例时发生错误。
     */
    public static String sha256Hex(String stringToSign) throws Exception {
        // 获取SHA-256消息摘要实例
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        // 计算字符串s的SHA-256哈希值
        byte[] d = md.digest(stringToSign.getBytes(StandardCharsets.UTF_8));
        // 将哈希值转换为小写十六进制字符串并返回
        return DatatypeConverter.printHexBinary(d).toLowerCase();
    }

    /**
     * 对指定的字符串进行URL编码。
     * 使用UTF-8编码字符集对字符串进行编码,并对特定的字符进行替换,以符合URL编码规范。
     *
     * @param str 需要进行URL编码的字符串。
     * @return 编码后的字符串。其中,加号"+"被替换为"%20",星号"*"被替换为"%2A",波浪号"%7E"被替换为"~"。
     */
    public static String percentCode(String str) {
        if (str == null) {
            throw new IllegalArgumentException("输入字符串不可为null");
        }
        try {
            return URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8编码不被支持", e);
        }
    }
}

Python示例

需要您手动安装pytz和requests,请根据您所使用的Python版本在终端(Terminal)执行以下命令。

Python3

pip3 install pytz
pip3 install requests

Python2

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):
        # 对查询参数按名称排序并返回编码后的字符串
        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):
        # 对请求头按名称排序并返回编码后的字符串
        sorted_headers = sorted(self.headers.items(), key=lambda item: item[0])
        self.headers = {k: v for k, v in sorted_headers}


# 环境变量中获取Access Key ID和Access Key Secret
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
        # 步骤 1:拼接规范请求串
        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()

        # 构建规范化请求头和已签名消息头列表
        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)

        # 步骤 2:拼接待签名字符串
        hashed_canonical_request = sha256_hex(canonical_request)
        string_to_sign = f'{ALGORITHM}\n{hashed_canonical_request}'
        print(string_to_sign)

        # 步骤 3:计算签名
        signature = hmac256(ACCESS_KEY_SECRET.encode('utf-8'), string_to_sign).hex().lower()
        print(signature)

        # 步骤 4:拼接Authorization
        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):
    # 如果值为空,则无需进一步处理
    if value is None:
        return

    if key is None:
        key = ""

    # 当值为列表类型时,遍历列表中的每个元素,并递归处理
    if isinstance(value, list):
        for i, item in enumerate(value):
            process_object(result_map, f"{key}.{i + 1}", item)
    elif isinstance(value, dict):
        # 当值为字典类型时,遍历字典中的每个键值对,并递归处理
        for sub_key, sub_value in value.items():
            process_object(result_map, f"{key}.{sub_key}", sub_value)
    else:
        # 对于以"."开头的键,移除开头的"."以保持键的连续性
        if key.startswith("."):
            key = key[1:]

        # 对于字节类型的值,将其转换为UTF-8编码的字符串
        if isinstance(value, bytes):
            result_map[key] = value.decode('utf-8')
        else:
            # 对于其他类型的值,直接转换为字符串
            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__":
    # RPC接口请求
    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)
    # RegionId的参数类型是String
    request.query_param['RegionId'] = 'cn-hangzhou'
    # InstanceId的参数类型是array
    request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"]

    # ROA接口POST请求
    # 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"

    # ROA接口GET请求
    # http_method = "GET"
    # # canonicalUri如果存在path参数,需要对path参数encode,percent_code({path参数})
    # 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示例

需要您在终端(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() {
	// RPC接口请求
	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

	// ROA接口POST请求
	// 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"

	// ROA接口GET请求
	// httpMethod := "GET"
	// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
	// 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"

	// 签名过程
	getAuthorization(req)
	// 调用API
	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) {
	// 处理queryParam中参数值为List、Map类型的参数,将参数平铺
	newQueryParams := make(map[string]interface{})
	processObject(newQueryParams, "", req.queryParam)
	req.queryParam = newQueryParams
	// 步骤 1:拼接规范请求串
	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)

	// 步骤 2:拼接待签名字符串
	hashedCanonicalRequest := sha256Hex(canonicalRequest)
	stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
	fmt.Printf("stringToSign========>\n%s\n", stringToSign)

	// 步骤 3:计算签名
	byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
	if err != nil {
		fmt.Println(err)
	}
	signature := strings.ToLower(hex.EncodeToString(byteData))

	// 步骤 4:拼接Authorization
	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) {
	// 实例化HMAC-SHA256哈希
	h := hmac.New(sha256.New, key)
	// 写入待签名的字符串
	_, err := h.Write([]byte(toSignString))
	if err != nil {
		return nil, err
	}
	// 计算签名并返回
	return h.Sum(nil), nil
}

func sha256Hex(str string) string {
	// 实例化SHA-256哈希函数
	hash := sha256.New()
	// 将字符串写入哈希函数
	_, _ = hash.Write([]byte(str))
	// 计算SHA-256哈希值并转换为小写的十六进制字符串
	hexString := hex.EncodeToString(hash.Sum(nil))

	return hexString
}

func percentCode(str string) string {
	// 替换特定的编码字符
	str = strings.ReplaceAll(str, "+", "%20")
	str = strings.ReplaceAll(str, "*", "%2A")
	str = strings.ReplaceAll(str, "%7E", "~")
	return str
}

// processObject 递归处理对象,将复杂对象(如Map和List)展开为平面的键值对
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示例

本示例所用语言是javaScript。

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;
        // 步骤 1:拼接规范请求串
        const canonicalQueryString = Object.entries(signRequest.queryParam)
            .sort(([a], [b]) => a.localeCompare(b))
            .map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
            .join('&');

        // 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
        const requestPayload = signRequest.body || '';
        const hashedRequestPayload = sha256Hex(requestPayload);
        signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;

        // 将所有key都转换为小写
        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();
        // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
        const signedHeaders = sortedKeys.join(";")
        // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
        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);

        // 步骤 2:拼接待签名字符串
        const hashedCanonicalRequest = sha256Hex(canonicalRequest);
        const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
        console.log('stringToSign=========>', stringToSign);

        // 步骤 3:计算签名
        const signature = hmac256(accessKeySecret, stringToSign);
        console.log('signature=========>', signature);

        // 步骤 4:拼接 Authorization
        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}`;
        // 添加请求参数
        if (signRequest.queryParam) {
            const query = new URLSearchParams(signRequest.queryParam);
            url += '?' + query.toString();
        }
        console.log('url=========>', url);

        // 配置请求选项
        let options = {
            method: signRequest.httpMethod.toUpperCase(),
            headers: signRequest.headers
        };

        // 处理请求体
        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('输入的字符串有误!');
    }
    const hash = crypto.createHash('sha256');
    const digest = hash.update(str, 'utf8').digest('hex');
    return digest.toLowerCase();
}

function processObject(map, key, value) {
    // 如果值为空,则无需进一步处理
    if (value === null) {
        return;
    }
    if (key === null) {
        key = "";
    }

    // 当值为Array类型时,遍历Array中的每个元素,并递归处理
    if (Array.isArray(value)) {
        value.forEach((item, index) => {
            processObject(map, `${key}.${index + 1}`, item);
        });
    } else if (typeof value === 'object' && value !== null) {
        // 当值为Object类型时,遍历Object中的每个键值对,并递归处理
        Object.entries(value).forEach(([subKey, subValue]) => {
            processObject(map, `${key}.${subKey}`, subValue);
        });
    } else {
        // 对于以"."开头的键,移除开头的"."以保持键的连续性
        if (key.startsWith('.')) {
            key = key.slice(1);
        }
        map[key] = String(value);
    }
}


// 示例一:RPC接口请求
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"],
}

// 示例二:ROA接口POST请求
// 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)
// // 根据需要设置正确的Content-Type,这里假设是JSON
// signRequest.headers['content-type'] = 'application/json';

// 示例三:ROA接口GET请求
// const httpMethod = 'GET';
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// 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'); // 设置时区为GMT
        $this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // 从环境变量中获取RAM用户Access Key ID
        $this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // 从环境变量中获取RAM用户Access Key Secret
        $this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 设置加密算法
    }

    public function main()
    {
        // RPC接口请求
        $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"]
        ];

        // ROA接口POST请求
        // $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"
        //     ],
        // ]);

        // ROA接口GET请求
        // canonicalUri如果存在path参数,需要对path参数encode,rawurlencode({path参数})
        // $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 {
            // 通过cURL发送请求
            $url = "https://" . $request['host'] . $request['canonicalUri'];

            // 添加请求参数到URL
            if (!empty($request['queryParam'])) {
                $url .= '?' . http_build_query($request['queryParam']);
            }

            echo $url;
            // 初始化cURL会话
            $ch = curl_init();

            // 设置cURL选项
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 禁用SSL证书验证,请注意,这会降低安全性,不应在生产环境中使用(不推荐!!!)
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回而不是输出内容
            curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // 添加请求头

            // 根据请求类型设置cURL选项
            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");
            }

            // 发送请求
            $result = curl_exec($ch);

            // 检查是否有错误发生
            if (curl_errno($ch)) {
                echo "Failed to send request: " . curl_error($ch);
            } else {
                echo $result;
            }

        } catch (Exception $e) {
            echo "Error: " . $e->getMessage();
        } finally {
            // 关闭cURL会话
            curl_close($ch);
        }
    }

    function processObject($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("环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 未设置");
        private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ?? throw new InvalidOperationException("环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET 未设置");
        private const string Algorithm = "ACS3-HMAC-SHA256";
        private const string ContentType = "content-type";

        public static void Main(string[] args)
        {
            // RPC接口请求
            string httpMethod = "POST"; // RPC接口大部分是同时支持POST和GET的
            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"; // RegionId的类型是string
            List<string> instanceIds = ["i-bp10igfmnytt9zhth92h", "i-bp1incuofvzxww9rcz80", "i-bp1incuofvzxww9rcz7z"];
            request.QueryParam["InstanceId"] = instanceIds; // InstanceId的类型是array

            // // ROA接口POST请求
            // String httpMethod = "POST";
            // String canonicalUri = "/clusters";
            // String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
            // String xAcsAction = "CreateCluster"; // API名称
            // String xAcsVersion = "2015-12-15"; // API版本号
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // 请求body,通过JsonConvert将body转成JSON字符串
            // 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";

            // // ROA接口GET请求
            // String httpMethod = "GET";
            // // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
            // String canonicalUri = "/clusters/" + PercentCode("c3c2987c8beae4b1b85bef006xxxxxxxx") + "/resources";
            // String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
            // String xAcsAction = "DescribeClusterResources"; // API名称
            // String xAcsVersion = "2015-12-15"; // API版本号
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // request.QueryParam["with_addon_resources"]=true;

            // // ROA接口DELETE请求
            // String httpMethod = "DELETE";
            // // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
            // String canonicalUri = "/clusters/" + PercentCode("c3c2987c8beae4b1b85bef006xxxxxxxx");
            // String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
            // String xAcsAction = "DeleteCluster"; // API名称
            // String xAcsVersion = "2015-12-15"; // 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
            {
                // 声明 httpClient
                using var httpClient = new HttpClient();

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

                // 添加请求参数
                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,
                };

                // 设置请求头
                foreach (var entry in request.Headers)
                {
                    if (entry.Key == "Authorization")
                    {
                        requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ;
                    }
                    else if (entry.Key == ContentType) // 与main中定义的要一致
                    {
                        continue;
                    }
                    else
                    {
                        requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
                    }
                }

                // 发送请求
                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");
                }

                // 读取响应内容
                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
            {
                // 处理queryParam中参数值为List、Map类型的参数,将参数平铺
                request.QueryParam = FlattenDictionary(request.QueryParam);

                // 步骤 1:拼接规范请求串
                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}");

                // 步骤 2:拼接待签名字符串
                string hashedCanonicalRequest = Sha256Hash(canonicalRequest);
                string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
                Console.WriteLine($"stringToSign:{stringToSign}");

                // 步骤 3:计算签名
                string signature = HmacSha256(AccessKeySecret, stringToSign);

                // 步骤 4:拼接 Authorization
                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("输入字符串不可为null或空");
            }
            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"

# 请求参数
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=""

# 按照ISO 8601标准表示的UTC时间
utc_timestamp=$(date +%s)
utc_date=$(date -u -d @${utc_timestamp} +"%Y-%m-%dT%H:%M:%SZ") 
# x-acs-signature-nonce 随机数
random=$(uuidgen | sed 's/-//g') 

# 签名header
headers="host:${host}
x-acs-action:${action}
x-acs-version:${version}
x-acs-date:${utc_date}
x-acs-signature-nonce:${random}"

# 步骤 1:拼接规范请求串
# 将queryParam中的参数全部平铺
newQueryParam=()

# 遍历每一个原始参数
for param in "${queryParam[@]}"; do
    # 检查是否包含等号,以确定是键值对
    if [[ "$param" == *"="* ]]; then
        # 分割键和值
        IFS='=' read -r key value <<< "$param"

        # 检查值是否为一个列表(通过查找括号)
        if [[ "$value" =~ ^\(.+\)$ ]]; then
            # 去掉两边的括号
            value="${value:1:-1}"

            # 使用IFS分割值列表
            IFS=' ' read -ra values <<< "$value"

            # 对于每个值添加索引
            index=1
            for val in "${values[@]}"; do
                # 去除双引号
                val="${val%\"}"
                val="${val#\"}"

                # 添加到新数组
                newQueryParam+=("$key.$index=$val")
                ((index++))
            done
        else
            # 如果不是列表,则直接添加
            newQueryParam+=("$param")
        fi
    else
        # 如果没有等号,直接保留原样
        newQueryParam+=("$param")
    fi
done

# 处理并排序新的查询参数
sortedParams=()
declare -A paramsMap
for param in "${newQueryParam[@]}"; do
    IFS='=' read -r key value <<< "$param"
    paramsMap["$key"]="$value"
done
# 根据键排序
for key in $(echo ${!paramsMap[@]} | tr ' ' '\n' | sort); do
    sortedParams+=("$key=${paramsMap[$key]}")
done

# 1.1 拼接规范化查询字符串
canonicalQueryString=""
first=true
for item in "${sortedParams[@]}"; do
    [ "$first" = true ] && first=false || canonicalQueryString+="&"
    # 检查是否存在等号
    if [[ "$item" == *=* ]]; then
        canonicalQueryString+="$item"
    else
        canonicalQueryString+="$item="
    fi
done

# 1.2 处理请求体
hashedRequestPayload=$(echo -n "$body" | openssl dgst -sha256 | awk '{print $2}')
headers="${headers}
x-acs-content-sha256:$hashedRequestPayload"

# 1.3 构造规范化请求头
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 构造规范请求
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}')
# 步骤 2:构造签名字符串
stringToSign="${algorithm}\n${hashedCanonicalRequest}"
echo -e "stringToSign=$stringToSign"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# 步骤 3:计算签名
signature=$(printf "${stringToSign}" | openssl dgst -sha256 -hmac "${accessKey_secret}" | sed 's/^.* //')
echo -e "signature=${signature}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# 步骤 4:构造Authorization
authorization="${algorithm} Credential=${accessKey_id},SignedHeaders=${signedHeaders},Signature=${signature}"
echo -e "authorization=${authorization}"

# 构造 curl 命令
url="https://$host$canonicalURI"
curl_command="curl -X $httpMethod '$url?$canonicalQueryString'"

# 添加请求头
IFS=$'\n'  # 设置换行符为新的IFS
for header in $headers; do
    curl_command="$curl_command -H '$header'"
done
curl_command+=" -H 'Authorization:$authorization'"

echo "$curl_command"
# 执行 curl 命令
eval "$curl_command"

固定参数示例

本示例是以固定的参数值为例,帮助您验证签名是否正确。根据下面假设的固定值计算完签名之后,得到与本示例一样的签名,表明您的签名过程是正确的。

所需参数名称

假设的参数值

AccessKeyID

YourAccessKeyId

AccessKeySecret

YourAccessKeySecret

x-acs-signature-nonce

3156853299f313e23d1673dc12e1703d

x-acs-date

2023-10-26T10:22:32Z

签名流程如下:

  1. 构造规范化请求。

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. 构造待签名字符串。

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
  1. 计算签名。

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
  1. 将签名添加到请求中。

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

常见问题

签名失败,提示“Specified signature does not match our calculation.”或者“The request signature does not conform to Aliyun standards.”。

问题原因:出现该问题通常是由于在签名机制的某个步骤中遗漏了必要的操作,例如参数未按升序排列或SignedHeaders未按升序排列等。

解决方案:请首先根据固定签名示例验证您的签名过程是否正确,逐步对比每个步骤的返回内容与固定签名示例的一致性。如发现不一致之处,请仔细阅读签名机制中相应步骤的说明。

自签名时,如果使用GET调试成功了,可以使用POST吗?

对于RPC接口,通常既支持GET请求也支持POST请求;而对于ROA接口,则仅支持单一的请求方式。如何获取API支持的请求方式,请参见OpenAPI元数据

联系我们

当您在计算签名时遇到无法解决的问题时,可以加入钉钉群:78410016550,联系值班同学进行咨询。