V3版本请求体&签名机制

更新时间:

当您不希望通过SDK调用阿里云OpenAPI,或您的程序运行环境不支持使用SDK时,您可以采用自签名的方式来调用阿里云OpenAPI。本文将通过介绍V3版本的签名机制,帮助您实现直接使用HTTP请求调用阿里云OpenAPI。

使用说明

  • 如果您当前使用的是V2版本签名进行API调用,您可以直接替换为V3版本签名以进行调用。

  • OpenAPI门户上提供了阿里云SDK的相关产品,这些产品所提供的API支持使用V3版本签名。需要注意的是,部分云产品由于采用自建网关,其认证机制与本文内容存在差异,因此在通过HTTP发起请求时,需要参阅相应的签名机制介绍文档,例如SLSOSS。

HTTP 请求结构

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

名称

是否必选

描述

示例值

协议

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

https://

服务地址

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

ecs.cn-shanghai.aliyuncs.com

resource_URI_parameters

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

ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai

RequestHeader

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

Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

x-acs-action: RunInstances

host: ecs.cn-shanghai.aliyuncs.com

x-acs-date: 2023-10-26T09:01:01Z

x-acs-version: 2014-05-26

x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0

RequestBody

定义在body中的业务请求参数,可通过OpenAPI元数据获取。

HTTPMethod

请求方法,可通过OpenAPI元数据获取。

POST

RequestHeader

调用阿里云OpenAPI时,公共请求头需要包含如下信息。

名称

类型

是否必选

描述

示例值

host

String

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

ecs.cn-shanghai.aliyuncs.com

x-acs-action

String

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

RunInstances

x-acs-content-sha256

String

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

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-date

String

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

2023-10-26T10:22:32Z

x-acs-signature-nonce

String

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

3156853299f313e23d1673dc12e1703d

x-acs-version

String

API 版本。如何获取请参见API版本(x-acs-version)获取方法

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=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

x-acs-security-token

String

STS认证必传

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

签名机制

采用AK/SK方式进行签名与认证。对于每一次HTTPHTTPS协议请求,阿里云API网关将依据请求参数信息重新计算签名,通过对比该签名与请求中提供的签名是否一致,从而验证请求者的身份,以确保传输数据的完整性与安全性。

重要

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

步骤一:构造规范化请求

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

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

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

  • CanonicalURI(规范化URI)

    URL的资源路径部分经过编码之后的结果。资源路径部分指URLhost与查询字符串之间的部分,包含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该参数为OpenAPI元数据中path的值,例如/api/v1/clusters

  • CanonicalQueryString(规范化查询字符串)

    OpenAPI元数据中,如果API的请求参数信息包含了"in":"query"时,需要将这些请求参数按照如下构造方法拼接起来:

    1. 将请求参数按照参数名称的字符顺序升序排列。

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

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

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

    重要
    • 当请求参数类型是array、object时,需要将参数平铺为一个映射结构(map)。例如{"ImageId":"win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd","RegionId":"cn-shanghai","Tag":[{"tag1":"value1","tag2":"value2"}]}平铺后为{"ImageId":"win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd","RegionId":"cn-shanghai","Tag.1.tag1":"value1","Tag.1.tag2":"value2"}

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

    示例值:

    ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
  • HashedRequestPayload

    使用哈希函数对RequestPayload进行转换得到HashedRequestPayload,并将RequestHeaderx-acs-content-sha256的值修改为HashedRequestPayload的值。伪代码如下:

    HashedRequestPayload = HexEncode(Hash(RequestPayload))
    • RequestPayload的值有以下两种情况:

      • OpenAPI元数据中,如果API的请求参数信息包含了"in": "body""in": "formData"时,需通过请求body传递参数,RequestPayload的值为请求body对应的JSON字符串。

        重要
        • 当请求参数信息包含"in": "formData"时,需要将这类参数按照固定格式拼接为一个字符串,拼接格式为:key1=value1&key2=value2&key3=value3。若请求参数类型是array、object时,需要将参数平铺为一个映射结构(map)。例如{"key":["value1","value2"]}平铺后为{"key.1":"value1","key.2":"value2"}。同时需要在RequestHeader中添加content-type=application/x-www-form-urlencoded

        • 当请求参数信息包含"in": "body"时,需要在RequestHeader中添加content-type,content-type的值与请求内容类型有关。例如:

          • 请求内容类型为JSON数据时,content-type的值为application/json

          • 请求内容类型为二进制文件流时,content-type的值为application/octet-stream

      • 若没有请求body,RequestPayload的值固定为一个空字符串。

    • Hash表示消息摘要函数,目前仅支持SHA256算法。

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

    当请求body为空时的示例值:

    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  • CanonicalHeaders(规范化请求头)

    RequestHeader中的参数按照如下构造方法拼接起来:

    1. 过滤出RequestHeader中包含以x-acs-为前缀、hostcontent-type的参数。

    2. 将参数的名称转换为小写,并按照字符顺序升序排列。

    3. 将参数的值去除首尾空格。

    4. 将参数名称和参数值以英文冒号(:)连接,并在尾部添加换行符(\n),组成一个规范化消息头(CanonicalHeaderEntry)。

    5. 将多个规范化消息头(CanonicalHeaderEntry)拼接成一个字符串。

    说明

    Authorization外的所有RequestHeader,只要符合要求都必须被加入签名。

    伪代码如下:

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

    示例值:

    host:ecs.cn-shanghai.aliyuncs.com
    x-acs-action:RunInstances
    x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
    x-acs-date:2023-10-26T10:22:32Z
    x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
    x-acs-version:2014-05-26
  • SignedHeaders(已签名消息头列表)

    用于说明此次请求中参与签名的公共请求头信息,与CanonicalHeaders中的参数名称一一对应。其构造方法如下:

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

    • 按首字母升序排列并以英文分号(;)分隔。

    伪代码如下:

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

    示例值:

    host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version

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

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

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

    签名协议目前仅支持ACS3-HMAC-SHA256算法。

  • HashedCanonicalRequest

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

    HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
    • Hash表示消息摘要函数,目前仅支持SHA256算法。

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

示例值:

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259

步骤三:计算签名

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

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

  • SignatureMethod:使用HMAC-SHA256作为签名算法。

  • Secret:AccessKey Secret。

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

示例值:

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

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

计算完签名后,构造Authorization请求头,格式为:Authorization:<SignatureAlgorithm> Credential=<AccessKeyId>,SignedHeaders=<SignedHeaders>,Signature=<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=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

签名示例代码

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

重要

在签名之前,请务必要先查看OpenAPI元数据,获取API的请求方式、请求参数名称、请求参数类型以及参数如何传等信息!否则,签名极有可能会失败!

固定参数示例

本示例是以假设的参数值为例,展示了签名机制中每个步骤所产生的正确输出内容。您可以在代码中使用本示例提供的假设参数值进行计算,并通过对比您的输出结果与本示例的内容,以验证签名过程的正确性。

所需参数名称

假设的参数值

AccessKeyID

YourAccessKeyId

AccessKeySecret

YourAccessKeySecret

x-acs-signature-nonce

3156853299f313e23d1673dc12e1703d

x-acs-date

2023-10-26T10:22:32Z

x-acs-action

RunInstances

x-acs-version

2014-05-26

host

ecs.cn-shanghai.aliyuncs.com

API请求参数:

ImageId

win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd

RegionId

cn-shanghai

签名流程如下:

  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

Java示例

说明

示例代码的运行环境是JDK1.8,您可能需要根据具体情况对代码进行相应的调整。

运行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.ByteArrayEntity;
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.nio.file.Files;
import java.nio.file.Paths;
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 SignatureRequest {
        // 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, String> headers = new TreeMap<>();
        // body参数对应的字节数组,请求参数在元数据中显示"in":"body"或"in": "formData",表示参数放在body中
        byte[] body;
        // query参数,请求参数在元数据中显示"in":"query",表示参数拼接在请求URL上
        TreeMap<String, Object> queryParam = new TreeMap<>();

        public SignatureRequest(String httpMethod, String canonicalUri, String host, String xAcsAction, String xAcsVersion) {
            this.httpMethod = httpMethod;
            this.canonicalUri = canonicalUri;
            this.host = host;
            this.xAcsAction = xAcsAction;
            this.xAcsVersion = xAcsVersion;
            initHeader();
        }

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

    /**
     * System.getenv()表示通过环境变量获取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";

    /**
     * 签名示例,您需要根据实际情况替换main方法中的示例参数。
     * ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
     * <p>
     * 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
     * 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
     * 2. 请求参数在元数据中显示"in": "body",通过body传参。
     * 3. 请求参数在元数据中显示"in": "formData",通过body传参。
     */
    public static void main(String[] args) throws IOException {
        // RPC接口请求示例一:请求参数"in":"query"
        String httpMethod = "POST"; // 请求方式,从元数据中可以获取,建议使用POST。
        String canonicalUri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
        String host = "ecs.cn-hangzhou.aliyuncs.com";  // 云产品服务接入点
        String xAcsAction = "DescribeInstanceStatus";  // API名称
        String xAcsVersion = "2014-05-26"; // API版本号
        SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // DescribeInstanceStatus请求参数如下:
        // RegionId在元数据中显示的类型是String,"in":"query",必填
        signatureRequest.queryParam.put("RegionId", "cn-hangzhou");
        // InstanceId的在元数据中显示的类型是array,"in":"query",非必填
        String[] instanceIds = {"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"};
        signatureRequest.queryParam.put("InstanceId", Arrays.asList(instanceIds));

        /*// RPC接口请求示例二:请求参数"in":"body"
        String httpMethod = "POST";
        String canonicalUri = "/";
        String host = "ocr-api.cn-hangzhou.aliyuncs.com";
        String xAcsAction = "RecognizeGeneral";
        String xAcsVersion = "2021-07-07";
        SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // 请求参数在元数据中显示"in": "body",通过body传参。
        signatureRequest.body = Files.readAllBytes(Paths.get("D:\\test.png"));
        signatureRequest.headers.put("content-type", "application/octet-stream");*/

        /*// RPC接口请求示例三:请求参数"in": "formData"
        String httpMethod = "POST";
        String canonicalUri = "/";
        String host = "mt.aliyuncs.com";
        String xAcsAction = "TranslateGeneral";
        String xAcsVersion = "2018-10-12";
        SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // TranslateGeneral请求参数如下:
        // Context在元数据中显示的类型是String,"in":"query",非必填
        signatureRequest.queryParam.put("Context", "早上");
        // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
        Map<String, Object> body = new HashMap<>();
        body.put("FormatType", "text");
        body.put("SourceLanguage", "zh");
        body.put("TargetLanguage", "en");
        body.put("SourceText", "你好");
        body.put("Scene", "general");
        String formDataToString = formDataToString(body);
        signatureRequest.body = formDataToString.getBytes(StandardCharsets.UTF_8);
        signatureRequest.headers.put("content-type", "application/x-www-form-urlencoded");*/

        /*// ROA接口POST请求
        String httpMethod = "POST";
        String canonicalUri = "/clusters"; // 从元数据中获取:"path": "/clusters"
        String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
        String xAcsAction= "CreateCluster"; // API名称
        String xAcsVersion=  "2015-12-15"; // API版本号
        SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        // 调用API所需要的参数,请求参数在元数据中显示"in": "body",表示参数放在body中
        TreeMap<String, Object> body = new TreeMap<>();
        body.put("name", "测试");
        body.put("region_id", "cn-beijing");
        body.put("cluster_type", "ExternalKubernetes");
        body.put("vpcid", "vpc-2zeou1uod4ylaXXXXXXXX");
        body.put("container_cidr","10.0.0.0/8");
        body.put("service_cidr", "10.2.0.0/24");
        body.put("security_group_id", "sg-2ze1a0rlgeo7XXXXXXXX");
        body.put("vswitch_ids", Collections.singletonList(
                "vsw-2zei30dhfldu8XXXXXXXX"
        ));
        Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
        signatureRequest.body = gson.toJson(body).getBytes(StandardCharsets.UTF_8);
        signatureRequest.headers.put("content-type", "application/json");*/

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

        /*// ROA接口DELETE请求
        String httpMethod = "DELETE";
        String canonicalUri = "/clusters/" + percentCode("cdb14b4f85130407da748fd3fXXXXXXXX");
        String host = "cs.cn-beijing.aliyuncs.com";
        String xAcsAction = "DeleteCluster";
        String xAcsVersion = "2015-12-15";
        SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);*/

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

    private static void callApi(SignatureRequest signatureRequest) {
        try {
            // 通过HttpClient发送请求
            String url = "https://" + signatureRequest.host + signatureRequest.canonicalUri;
            URIBuilder uriBuilder = new URIBuilder(url);
            // 添加请求参数
            for (Map.Entry<String, Object> entry : signatureRequest.queryParam.entrySet()) {
                uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
            }
            System.out.println(uriBuilder.build());
            HttpUriRequest httpRequest;
            switch (signatureRequest.httpMethod) {
                case "GET":
                    httpRequest = new HttpGet(uriBuilder.build());
                    break;
                case "POST":
                    HttpPost httpPost = new HttpPost(uriBuilder.build());
                    if (signatureRequest.body != null) {
                        httpPost.setEntity(new ByteArrayEntity(signatureRequest.body, ContentType.create(signatureRequest.headers.get("content-type"))));
                    }
                    httpRequest = httpPost;
                    break;
                case "DELETE":
                    httpRequest = new HttpDelete(uriBuilder.build());
                    break;
                default:
                    System.out.println("Unsupported HTTP method: " + signatureRequest.httpMethod);
                    throw new IllegalArgumentException("Unsupported HTTP method");
            }

            // 添加http请求头
            for (Map.Entry<String, String> entry : signatureRequest.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(SignatureRequest signatureRequest) {
        try {
            // 处理queryParam中参数值为List、Map类型的参数,将参数平铺
            TreeMap<String, Object> newQueryParam = new TreeMap<>();
            processObject(newQueryParam, "", signatureRequest.queryParam);
            signatureRequest.queryParam = newQueryParam;
            // 步骤 1:拼接规范请求串
            // 请求参数,当请求的查询字符串为空时,使用空字符串作为规范化查询字符串
            StringBuilder canonicalQueryString = new StringBuilder();
            signatureRequest.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);
            });

            // 计算请求体的哈希值
            String requestPayload = ""; // 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
            String hashedRequestPayload = signatureRequest.body != null ? sha256Hex(signatureRequest.body) : sha256Hex(requestPayload.getBytes(StandardCharsets.UTF_8));
            signatureRequest.headers.put("x-acs-content-sha256", hashedRequestPayload);
            // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
            StringBuilder canonicalHeaders = new StringBuilder();
            // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
            StringBuilder signedHeadersSb = new StringBuilder();
            signatureRequest.headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") || "host".equalsIgnoreCase(entry.getKey()) || "content-type".equalsIgnoreCase(entry.getKey())).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 = signatureRequest.httpMethod + "\n" + signatureRequest.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
            System.out.println("canonicalRequest=========>\n" + canonicalRequest);

            // 步骤 2:拼接待签名字符串
            String hashedCanonicalRequest = sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8)); // 计算规范化请求的哈希值
            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);
            signatureRequest.headers.put("Authorization", authorization);
        } catch (Exception e) {
            // 异常处理
            System.out.println("Failed to get authorization");
            e.printStackTrace();
        }
    }

    /**
     * 处理请求参数类型为formData的参数。
     *
     * @param formData formData类型参数
     * @return String
     */
    private static String formDataToString(Map<String, Object> formData) {
        Map<String, Object> tileMap = new HashMap<>();
        processObject(tileMap, "", formData);
        StringBuilder result = new StringBuilder();
        boolean first = true;
        String symbol = "&";
        for (Map.Entry<String, Object> entry : tileMap.entrySet()) {
            String value = String.valueOf(entry.getValue());
            if (value != null && !value.isEmpty()) {
                if (first) {
                    first = false;
                } else {
                    result.append(symbol);
                }
                result.append(percentCode(entry.getKey()));
                result.append("=");
                result.append(percentCode(value));
            }
        }

        return result.toString();
    }

    /**
     * 递归处理对象,将复杂对象(如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 input 需要进行SHA-256哈希计算的字节数组。
     * @return 计算结果为小写十六进制字符串。
     * @throws Exception 如果在获取SHA-256消息摘要实例时发生错误。
     */
    public static String sha256Hex(byte[] input) throws Exception {
        // 获取SHA-256消息摘要实例
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        // 计算字符串s的SHA-256哈希值
        byte[] d = md.digest(input);
        // 将哈希值转换为小写十六进制字符串并返回
        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示例

说明

示例代码的运行环境是Python 3.12.3,您可能需要根据具体情况对代码进行相应的调整。

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

Python3

pip3 install pytz
pip3 install requests
import hashlib
import hmac
import json
import os
import uuid
from collections import OrderedDict
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from urllib.parse import quote_plus, urlencode

import pytz
import requests


class SignatureRequest:
    def __init__(
            self,
            http_method: str,
            canonical_uri: str,
            host: str,
            x_acs_action: str,
            x_acs_version: str
    ):
        self.http_method = http_method
        self.canonical_uri = canonical_uri
        self.host = host
        self.x_acs_action = x_acs_action
        self.x_acs_version = x_acs_version
        self.headers = self._init_headers()
        self.query_param = OrderedDict()  # type: Dict[str, Any]
        self.body = None  # type: Optional[bytes]

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

    def sorted_query_params(self) -> None:
        """对查询参数按名称排序并返回编码后的字符串"""
        self.query_param = dict(sorted(self.query_param.items()))

    def sorted_headers(self) -> None:
        """对请求头按名称排序并返回编码后的字符串"""
        self.headers = dict(sorted(self.headers.items()))


def get_authorization(request: SignatureRequest) -> None:
    try:
        new_query_param = OrderedDict()
        process_object(new_query_param, '', request.query_param)
        request.query_param = new_query_param
        request.sorted_query_params()

        # 步骤 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 b'')
        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(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.encode("utf-8"))
        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()

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


def form_data_to_string(form_data: Dict[str, Any]) -> str:
    tile_map = OrderedDict()
    process_object(tile_map, "", form_data)
    return urlencode(tile_map)


def process_object(result_map: Dict[str, str], key: str, value: Any) -> None:
    if value is None:
        return

    if isinstance(value, (list, tuple)):
        for i, item in enumerate(value):
            process_object(result_map, f"{key}.{i + 1}", item)
    elif isinstance(value, dict):
        for sub_key, sub_value in value.items():
            process_object(result_map, f"{key}.{sub_key}", sub_value)
    else:
        key = key.lstrip(".")
        result_map[key] = value.decode("utf-8") if isinstance(value, bytes) else str(value)


def hmac256(key: bytes, msg: str) -> bytes:
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()


def sha256_hex(s: bytes) -> str:
    return hashlib.sha256(s).hexdigest()


def call_api(request: SignatureRequest) -> None:
    url = f"https://{request.host}{request.canonical_uri}"
    if request.query_param:
        url += "?" + urlencode(request.query_param, doseq=True, safe="*")

    headers = dict(request.headers)
    data = request.body

    try:
        response = requests.request(
            method=request.http_method, url=url, headers=headers, data=data
        )
        response.raise_for_status()
        print(response.text)
    except requests.RequestException as e:
        print("Failed to send request")
        print(e)


def percent_code(encoded_str: str) -> str:
    return encoded_str.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")


# 环境变量中获取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"

"""
签名示例,您在测试时可根据实际情况选择main函数中的示例并修改示例值,例如调用SendSms选择示例一即可,然后修改http_method、host、x_acs_action、x_acs_version以及query_param。
ROA和RPC只有canonicalUri取值逻辑是不同的。

通过OpenAPI元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
1. 请求参数在元数据中显示"in":"query",通过queryParam传参,无需设置content-type。注:该类型参数也支持通过body传参,content-type为application/x-www-form-urlencoded,参见示例三。
2. 请求参数在元数据中显示"in": "body",通过body传参,根据实际情况设置content-type。
3. 请求参数在元数据中显示"in": "formData",通过body传参,content-type为application/x-www-form-urlencoded。
"""
if __name__ == "__main__":
    # RPC接口请求示例一:请求参数"in":"query"
    http_method = "POST"  # 请求方式,从元数据中可以获取,建议使用POST。
    canonical_uri = "/"  # RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
    host = "ecs.cn-hangzhou.aliyuncs.com"  # 云产品服务接入点
    x_acs_action = "DescribeInstanceStatus"  # API名称
    x_acs_version = "2014-05-26"  # API版本号
    signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # DescribeInstanceStatus请求参数如下:
    # RegionId在元数据中显示的类型是String,"in":"query",必填
    signature_request.query_param['RegionId'] = 'cn-hangzhou'
    # InstanceId的在元数据中显示的类型是array,"in":"query",非必填
    signature_request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX",
                                                   "i-bp1incuofvzxXXXXXXXX"]

    # # RPC接口请求示例二:请求参数"in":"body"
    # http_method = "POST"
    # canonical_uri = "/"
    # host = "ocr-api.cn-hangzhou.aliyuncs.com"
    # x_acs_action = "RecognizeGeneral"
    # x_acs_version = "2021-07-07"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # # 请求参数在元数据中显示"in": "body",通过body传参。
    # file_path = "D:\\test.png"
    # with open(file_path, 'rb') as file:
    #     # 读取图片内容为字节数组
    #     signature_request.body = file.read()
    #     signature_request.headers["content-type"] = "application/octet-stream"

    # # RPC接口请求示例三:请求参数"in": "formData"
    # http_method = "POST"
    # canonical_uri = "/"
    # host = "mt.aliyuncs.com"
    # x_acs_action = "TranslateGeneral"
    # x_acs_version = "2018-10-12"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # # TranslateGeneral请求参数如下:
    # # Context在元数据中显示的类型是String,"in":"query",非必填
    # signature_request.query_param['Context'] = '早上'
    # # FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
    # form_data = OrderedDict()
    # form_data["FormatType"] = "text"
    # form_data["SourceLanguage"] = "zh"
    # form_data["TargetLanguage"] = "en"
    # form_data["SourceText"] = "你好"
    # form_data["Scene"] = "general"
    # signature_request.body = bytes(form_data_to_string(form_data), 'utf-8')
    # signature_request.headers["content-type"] = "application/x-www-form-urlencoded"

    # # 示例四:ROA接口POST请求
    # http_method = "POST"
    # canonical_uri = "/clusters"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "CreateCluster"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # 请求参数在元数据中显示"in":"body",通过body传参。
    # body = OrderedDict()
    # body["name"] = "testDemo"
    # body["region_id"] = "cn-beijing"
    # body["cluster_type"] = "ExternalKubernetes"
    # body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
    # body["container_cidr"] = "172.16.1.0/20"
    # body["service_cidr"] = "10.2.0.0/24"
    # body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
    # body["vswitch_ids"] = ["vsw-2zei30dhfldu8XXXXXXXX"]
    # signature_request.body = bytes(json.dumps(body, separators=(',', ':')), 'utf-8')
    # signature_request.headers["content-type"] = "application/json; charset=utf-8"

    # # 示例五:ROA接口GET请求
    # http_method = "GET"
    # # canonicalUri如果存在path参数,需要对path参数encode,percent_code({path参数})
    # cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}/resources"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DescribeClusterResources"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # signature_request.query_param['with_addon_resources'] = True

    # # 示例六:ROA接口DELETE请求
    # http_method = "DELETE"
    # # canonicalUri如果存在path参数,需要对path参数encode,percent_code({path参数})
    # cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DeleteCluster"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)

    get_authorization(signature_request)
    call_api(signature_request)

Go示例

说明

示例代码的运行环境是go1.22.2,您可能需要根据具体情况对代码进行相应的调整。

需要您在终端(Terminal)执行以下命令:

go get github.com/google/uuid
go get golang.org/x/exp/maps
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"os"
	"sort"

	"golang.org/x/exp/maps"

	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/google/uuid"
)

type Request struct {
	httpMethod   string
	canonicalUri string
	host         string
	xAcsAction   string
	xAcsVersion  string
	headers      map[string]string
	body         []byte
	queryParam   map[string]interface{}
}

func NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion string) *Request {
	req := &Request{
		httpMethod:   httpMethod,
		canonicalUri: canonicalUri,
		host:         host,
		xAcsAction:   xAcsAction,
		xAcsVersion:  xAcsVersion,
		headers:      make(map[string]string),
		queryParam:   make(map[string]interface{}),
	}
	req.headers["host"] = host
	req.headers["x-acs-action"] = xAcsAction
	req.headers["x-acs-version"] = xAcsVersion
	req.headers["x-acs-date"] = time.Now().UTC().Format(time.RFC3339)
	req.headers["x-acs-signature-nonce"] = uuid.New().String()
	return req
}

// os.Getenv()表示从环境变量中获取AccessKey ID和AccessKey Secret。
var (
	AccessKeyId     = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
	AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
	ALGORITHM       = "ACS3-HMAC-SHA256"
)

// 签名示例,您需要根据实际情况替换main方法中的示例参数。
// ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
// 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
// 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
// 2. 请求参数在元数据中显示"in": "body",通过body传参。
// 3. 请求参数在元数据中显示"in": "formData",通过body传参。
func main() {
	// RPC接口请求示例一:请求参数"in":"query"
	httpMethod := "POST"                   // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
	canonicalUri := "/"                    // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
	host := "ecs.cn-hangzhou.aliyuncs.com" // 云产品服务接入点
	xAcsAction := "DescribeInstanceStatus" // API名称
	xAcsVersion := "2014-05-26"            // API版本号
	req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// DescribeInstanceStatus请求参数如下:
	// RegionId在元数据中显示的类型是String,"in":"query",必填
	req.queryParam["RegionId"] = "cn-hangzhou"
	// InstanceId的在元数据中显示的类型是array,"in":"query",非必填
	instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
	req.queryParam["InstanceId"] = instanceIds

	// // RPC接口请求示例二:请求参数"in":"body"
	// httpMethod := "POST"
	// canonicalUri := "/"
	// host := "ocr-api.cn-hangzhou.aliyuncs.com"
	// xAcsAction := "RecognizeGeneral"
	// xAcsVersion := "2021-07-07"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // 读取文件内容
	// filePath := "D:\\test.png"
	// bytes, err := os.ReadFile(filePath)
	// if err != nil {
	// 	fmt.Println("Error reading file:", err)
	// 	return
	// }
	// req.body = bytes
	// req.headers["content-type"] = "application/octet-stream"

	// // RPC接口请求示例三:请求参数"in": "formData"
	// httpMethod := "POST"
	// canonicalUri := "/"
	// host := "mt.aliyuncs.com"
	// xAcsAction := "TranslateGeneral"
	// xAcsVersion := "2018-10-12"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // TranslateGeneral请求参数如下:
	// // Context在元数据中显示的类型是String,"in":"query",非必填
	// req.queryParam["Context"] = "早上"
	// // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
	// body := make(map[string]interface{})
	// body["FormatType"] = "text"
	// body["SourceLanguage"] = "zh"
	// body["TargetLanguage"] = "en"
	// body["SourceText"] = "你好"
	// body["Scene"] = "general"
	// str := formDataToString(body)
	// req.body = []byte(*str)
	// req.headers["content-type"] = "application/x-www-form-urlencoded"

	// // 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)
	// // 封装请求参数,请求参数在元数据中显示"in": "body",表示参数放在body中
	// body := make(map[string]interface{})
	// body["name"] = "testDemo"
	// body["region_id"] = "cn-beijing"
	// body["cluster_type"] = "ExternalKubernetes"
	// body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
	// body["container_cidr"] = "10.0.0.0/8"
	// body["service_cidr"] = "172.16.1.0/20"
	// body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
	// vswitch_ids := []interface{}{"vsw-2zei30dhfldu8XXXXXXXX"}
	// body["vswitch_ids"] = vswitch_ids
	// jsonBytes, err := json.Marshal(body)
	// if err != nil {
	// 	fmt.Println("Error marshaling to JSON:", err)
	// 	return
	// }
	// req.body = []byte(jsonBytes)
	// req.headers["content-type"] = "application/json; charset=utf-8"

	// // ROA接口GET请求
	// httpMethod := "GET"
	// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
	// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66") + "/resources"
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "DescribeClusterResources"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// req.queryParam["with_addon_resources"] = "true"

	// // ROA接口DELETE请求
	// httpMethod := "DELETE"
	// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
	// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66")
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "DeleteCluster"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)

	// 签名过程
	getAuthorization(req)
	// 调用API
	error := callAPI(req)
	if error != nil {
		println(error.Error())
	}
}

func callAPI(req *Request) error {
	urlStr := "https://" + req.host + req.canonicalUri
	q := url.Values{}
	keys := maps.Keys(req.queryParam)
	sort.Strings(keys)
	for _, k := range keys {
		v := req.queryParam[k]
		q.Set(k, fmt.Sprintf("%v", v))
	}
	urlStr += "?" + q.Encode()
	fmt.Println(urlStr)

	httpReq, err := http.NewRequest(req.httpMethod, urlStr, strings.NewReader(string(req.body)))
	if err != nil {
		return err
	}

	for key, value := range req.headers {
		httpReq.Header.Set(key, value)
	}

	client := &http.Client{}
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			return
		}
	}(resp.Body)
	var respBuffer bytes.Buffer
	_, err = io.Copy(&respBuffer, resp.Body)
	if err != nil {
		return err
	}
	respBytes := respBuffer.Bytes()
	fmt.Println(string(respBytes))
	return nil
}

func getAuthorization(req *Request) {
	// 处理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)

	var bodyContent []byte
	if req.body == nil {
		bodyContent = []byte("")
	} else {
		bodyContent = req.body
	}
	hashedRequestPayload := sha256Hex(bodyContent)
	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([]byte(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(byteArray []byte) string {
	// 实例化SHA-256哈希函数
	hash := sha256.New()
	// 将字符串写入哈希函数
	_, _ = hash.Write(byteArray)
	// 计算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
}

func formDataToString(formData map[string]interface{}) *string {
	tmp := make(map[string]interface{})
	processObject(tmp, "", formData)
	res := ""
	urlEncoder := url.Values{}
	for key, value := range tmp {
		v := fmt.Sprintf("%v", value)
		urlEncoder.Add(key, v)
	}
	res = urlEncoder.Encode()
	return &res
}

// processObject 递归处理对象,将复杂对象(如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示例

说明

示例代码的运行环境是Node.js v20.13.1,您可能需要根据具体情况对代码进行相应的调整。

本示例所用语言是javaScript。

const crypto = require('crypto');
const fs = require('fs');

class Request {
    constructor(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion) {
        this.httpMethod = httpMethod;
        this.canonicalUri = canonicalUri || '/';
        this.host = host;
        this.xAcsAction = xAcsAction;
        this.xAcsVersion = xAcsVersion;
        this.headers = {};
        this.body = null;
        this.queryParam = {};
        this.initHeader();
    }

    initHeader() {
        const date = new Date();
        this.headers = {
            'host': this.host,
            'x-acs-action': this.xAcsAction,
            'x-acs-version': this.xAcsVersion,
            'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
            'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
        }
    }
}

const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
const encoder = new TextEncoder()

if (!accessKeyId || !accessKeySecret) {
    console.error('ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables must be set.');
    process.exit(1);
}

function getAuthorization(signRequest) {
    try {
        newQueryParam = {};
        processObject(newQueryParam, "", signRequest.queryParam);
        signRequest.queryParam = newQueryParam;
        // 步骤 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 || encoder.encode('');
        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(encoder.encode(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(bytes) {
    const hash = crypto.createHash('sha256');
    const digest = hash.update(bytes).digest('hex');
    return digest.toLowerCase();
}

function formDataToString(formData) {
    const tmp = {};
    processObject(tmp, "", formData);
    let queryString = '';
    for (let [key, value] of Object.entries(tmp)) {
        if (queryString !== '') {
            queryString += '&';
        }
        queryString += encodeURIComponent(key) + '=' + encodeURIComponent(value);
    }
    return queryString;
}

function processObject(map, key, value) {
    // 如果值为空,则无需进一步处理
    if (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);
    }
}

/**
 * 签名示例,您需要根据实际情况替换main方法中的示例参数。
 * ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
 *
 * 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
 * 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
 * 2. 请求参数在元数据中显示"in": "body",通过body传参。
 * 3. 请求参数在元数据中显示"in": "formData",通过body传参。
 */

// RPC接口请求示例一:请求参数"in":"query"
const httpMethod = 'POST'; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
const canonicalUri = '/'; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
const host = 'ecs.cn-hangzhou.aliyuncs.com'; // endpoint
const xAcsAction = 'DescribeInstanceStatus'; // API名称
const xAcsVersion = '2014-05-26'; // API版本号
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// DescribeInstanceStatus请求参数如下:
signRequest.queryParam = {
    // RegionId在元数据中显示的类型是String,"in":"query",必填
    RegionId: 'cn-hangzhou',
    // InstanceId的在元数据中显示的类型是array,"in":"query",非必填
    InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}


// // RPC接口请求示例二:请求参数"in":"body"
// const httpMethod = 'POST';
// const canonicalUri = '/';
// const host = 'ocr-api.cn-hangzhou.aliyuncs.com';
// const xAcsAction = 'RecognizeGeneral';
// const xAcsVersion = '2021-07-07';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// const filePath = 'D:\\test.png';
// const bytes = fs.readFileSync(filePath);
// // 请求参数在元数据中显示"in": "body",表示参数放在body中
// signRequest.body = bytes;
// signRequest.headers['content-type'] = 'application/octet-stream';


// // RPC接口请求示例三:请求参数"in": "formData"
// const httpMethod = 'POST'; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
// const canonicalUri = '/'; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
// const host = 'mt.aliyuncs.com'; // endpoint
// const xAcsAction = 'TranslateGeneral'; // API名称
// const xAcsVersion = '2018-10-12'; // API版本号
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// // TranslateGeneral请求参数如下:
// // Context在元数据中显示的类型是String,"in":"query",非必填
// signRequest.queryParam["Context"] = "早上";
// // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
// const formData = {
//     SourceLanguage: "zh",
//     TargetLanguage: "en",
//     FormatType: "text",
//     Scene: "general",
//     SourceText: '你好'
// }
// const str = formDataToString(formData)
// signRequest.body = encoder.encode(str);
// signRequest.headers['content-type'] = 'application/x-www-form-urlencoded';


// // 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);
// // 请求参数在元数据中显示"in": "body",表示参数放在body中
// const body = {
//     name: 'testDemo',
//     region_id: 'cn-beijing',
//     cluster_type: 'ExternalKubernetes',
//     vpcid: 'vpc-2zeou1uod4ylaf35teei9',
//     container_cidr: '10.0.0.0/8',
//     service_cidr: '172.16.3.0/20',
//     security_group_id: 'sg-2ze1a0rlgeo7dj37dd1q',
//     vswitch_ids: [
//         'vsw-2zei30dhfldu8ytmtarro'
//       ],
// }
// signRequest.body = encoder.encode(JSON.stringify(body));
// signRequest.headers['content-type'] = 'application/json';


// // ROA接口GET请求
// const httpMethod = 'GET';
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96") + '/resources';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DescribeClusterResources';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// signRequest.queryParam = {
//     with_addon_resources: true,
// }


// // ROA接口DELETE请求
// const httpMethod = 'DELETE';
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96");
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DeleteCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);

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

PHP示例

说明

示例代码的运行环境是PHP 7.4.33,您可能需要根据具体情况对代码进行相应的调整。

<?php

class SignatureDemo
{
    // 加密算法
    private $ALGORITHM;
    // Access Key ID
    private $AccessKeyId;
    // Access Key Secret
    private $AccessKeySecret;

    public function __construct()
    {
        date_default_timezone_set('UTC'); // 设置时区为GMT
        $this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // getenv()表示从环境变量中获取RAM用户Access Key ID
        $this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // getenv()表示从环境变量中获取RAM用户Access Key Secret
        $this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 设置加密算法
    }

    /**
     * 签名示例,您需要根据实际情况替换main方法中的示例参数。
     * ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
     *
     * 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
     * 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
     * 2. 请求参数在元数据中显示"in": "body",通过body传参。
     * 3. 请求参数在元数据中显示"in": "formData",通过body传参。
     */
    public function main()
    {
        // RPC接口请求示例一:请求参数"in":"query"
        $request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
        // DescribeInstanceStatus请求参数如下:
        $request['queryParam'] = [
            // RegionId在元数据中显示的类型是String,"in":"query",必填
            'RegionId' => 'cn-hangzhou',
            // InstanceId的在元数据中显示的类型是array,"in":"query",非必填
            'InstanceId' => ["i-bp11ht4h2kdXXXXXXXX", "i-bp16maz3h3xgXXXXXXXX", "i-bp10r67hmslXXXXXXXX"]
        ];

        // // RPC接口请求示例二:请求参数"in":"body"
        // $request = $this->createRequest('POST', '/', 'ocr-api.cn-hangzhou.aliyuncs.com', 'RecognizeGeneral', '2021-07-07');
        // // 请求参数在元数据中显示"in": "body",通过body传参。
        // $filePath = 'D:\\test.png';
        // // 使用文件资源传入二进制格式文件
        // $fileResource = fopen($filePath, 'rb');
        // $request['body'] = stream_get_contents($fileResource); 
        // $request['headers']['content-type'] = 'application/octet-stream'; // 设置 Content-Type 为 application/octet-stream
        // // 关闭文件资源
        // fclose($fileResource);


        // // RPC接口请求示例三:请求参数"in": "formData"
        // $request = $this->createRequest('POST', '/', 'mt.aliyuncs.com', 'TranslateGeneral', '2018-10-12');
        // // TranslateGeneral请求参数如下:
        // $request['queryParam'] = [
        //     // Context在元数据中显示的类型是String,"in":"query",非必填
        //     'Context' => '早上',
        // ];
        // $formData = [
        //     'FormatType' => 'text',
        //     'SourceLanguage' => 'zh',
        //     'TargetLanguage' => 'en',
        //     'SourceText' => '你好',
        //     'Scene' => 'general',
        // ];
        // $str = self::formDataToString($formData);
        // $request['body'] = $str;
        // $request['headers']['content-type'] = 'application/x-www-form-urlencoded';


        // // ROA接口POST请求
        // $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
        // $bodyData = [
        //     'name' => '测试集群',
        //     'region_id' => 'cn-beijing',
        //     'cluster_type' => 'ExternalKubernetes',
        //     'vpcid' => 'vpc-2zeou1uod4ylaXXXXXXXX',
        //     'service_cidr' => '10.2.0.0/24',
        //     'security_group_id' => 'sg-2ze1a0rlgeo7XXXXXXXX',
        //     "vswitch_ids" => [
        //         "vsw-2zei30dhfldu8XXXXXXXX"
        //     ]
        // ];
        // $request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
        // $request['headers']['content-type'] = 'application/json; charset=utf-8'; 


        // // ROA接口GET请求
        // // canonicalUri如果存在path参数,需要对path参数encode,rawurlencode({path参数})
        // $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
        // $canonicalUri = sprintf("/clusters/%s/resources", rawurlencode($cluster_id));
        // $request = $this->createRequest('GET', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DescribeClusterResources', '2015-12-15');
        // $request['queryParam'] = [
        //     'with_addon_resources' => true,
        // ];


        // // ROA接口DELETE请求
        // $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
        // $canonicalUri = sprintf("/clusters/%s", rawurlencode($cluster_id));
        // $request = $this->createRequest('DELETE', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DeleteCluster', '2015-12-15');

        $this->getAuthorization($request);
        // 调用API
        $this->callApi($request);
    }

    private function createRequest($httpMethod, $canonicalUri, $host, $xAcsAction, $xAcsVersion)
    {
        $headers = [
            'host' => $host,
            'x-acs-action' => $xAcsAction,
            'x-acs-version' => $xAcsVersion,
            'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'),
            'x-acs-signature-nonce' => bin2hex(random_bytes(16)),
        ];
        return [
            'httpMethod' => $httpMethod,
            'canonicalUri' => $canonicalUri,
            'host' => $host,
            'headers' => $headers,
            'queryParam' => [],
            'body' => null,
        ];
    }

    private function getAuthorization(&$request)
    {
        $request['queryParam'] = $this->processObject($request['queryParam']);
        $canonicalQueryString = $this->buildCanonicalQueryString($request['queryParam']);
        $hashedRequestPayload = hash('sha256', $request['body'] ?? '');
        $request['headers']['x-acs-content-sha256'] = $hashedRequestPayload;

        $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 formDataToString($formData)
    {
        $res = self::processObject($formData);
        return http_build_query($res);
    }

    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示例

说明

示例代码的运行环境是.NET 8.0.302,您可能需要根据具体情况对代码进行相应的调整。

using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using Newtonsoft.Json;

namespace SignatureDemo
{
    public class Request
    {
        public string HttpMethod { get; private set; }
        public string CanonicalUri { get; private set; }
        public string Host { get; private set; }
        public string XAcsAction { get; private set; }
        public string XAcsVersion { get; private set; }
        public SortedDictionary<string, object> Headers { get; private set; }
        public byte[] Body { get; set; }
        public Dictionary<string, object> QueryParam { get; set; }

        public Request(string httpMethod, string canonicalUri, string host, string xAcsAction, string xAcsVersion)
        {
            HttpMethod = httpMethod;
            CanonicalUri = canonicalUri;
            Host = host;
            XAcsAction = xAcsAction;
            XAcsVersion = xAcsVersion;
            Headers = [];
            QueryParam = [];
            Body = null;
            InitHeader();
        }

        private void InitHeader()
        {
            Headers["host"] = Host;
            Headers["x-acs-action"] = XAcsAction;
            Headers["x-acs-version"] = XAcsVersion;
            DateTime utcNow = DateTime.UtcNow;
            Headers["x-acs-date"] = utcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
            Headers["x-acs-signature-nonce"] = Guid.NewGuid().ToString();
        }
    }

    public class Program
    {
        private static readonly string AccessKeyId = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID") ?? throw new InvalidOperationException("环境变量 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";

        /**
        * 签名示例,您需要根据实际情况替换main方法中的示例参数。
        * ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
        *
        * 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
        * 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
        * 2. 请求参数在元数据中显示"in": "body",通过body传参。
        * 3. 请求参数在元数据中显示"in": "formData",通过body传参。
        */
        public static void Main(string[] args)
        {
            // RPC接口请求示例一:请求参数"in":"query"
            string httpMethod = "POST"; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
            string canonicalUri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
            string host = "ecs.cn-hangzhou.aliyuncs.com"; // 云产品服务接入点
            string xAcsAction = "DescribeInstanceStatus"; // API名称
            string xAcsVersion = "2014-05-26"; // API版本号
            var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // DescribeInstanceStatus请求参数如下:
            // RegionId在元数据中显示的类型是String,"in":"query",必填
            request.QueryParam["RegionId"] = "cn-hangzhou"; 
            // InstanceId的在元数据中显示的类型是array,"in":"query",非必填
            List<string> instanceIds = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"];
            request.QueryParam["InstanceId"] = instanceIds; 

            // // RPC接口请求示例二:请求参数"in":"body"
            // string httpMethod = "POST"; 
            // string canonicalUri = "/"; 
            // string host = "ocr-api.cn-hangzhou.aliyuncs.com"; 
            // string xAcsAction = "RecognizeGeneral"; 
            // string xAcsVersion = "2021-07-07"; 
            // var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // 请求参数在元数据中显示"in": "body",通过body传参。
            // request.Body = File.ReadAllBytes(@"D:\test.png");
            // request.Headers["content-type"] = "application/octet-stream";


            // // RPC接口请求示例三:请求参数"in": "formData"
            // string httpMethod = "POST"; 
            // string canonicalUri = "/"; 
            // string host = "mt.aliyuncs.com"; 
            // string xAcsAction = "TranslateGeneral"; 
            // string xAcsVersion = "2018-10-12"; 
            // var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // TranslateGeneral请求参数如下:
            // // Context在元数据中显示的类型是String,"in":"query",非必填
            // request.QueryParam["Context"] = "早上"; 
            // // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
            // var body = new Dictionary<string, object>
            // {
            //     { "FormatType", "text" },
            //     { "SourceLanguage", "zh" },
            //     { "TargetLanguage", "en" },
            //     { "SourceText", "你好" },
            //     { "Scene", "general" },
            // };
            // var str = FormDataToString(body);
            // request.Body = Encoding.UTF8.GetBytes(str);
            // request.Headers[ContentType] = "application/x-www-form-urlencoded";


            // // ROA接口POST请求
            // String httpMethod = "POST";
            // String canonicalUri = "/clusters";
            // String host = "cs.cn-beijing.aliyuncs.com";
            // String xAcsAction = "CreateCluster"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // 请求body,通过JsonConvert将body转成JSON字符串
            // var body = new SortedDictionary<string, object>
            // {
            //     { "name", "testDemo" },
            //     { "region_id", "cn-beijing" },
            //     { "cluster_type", "ExternalKubernetes" },
            //     { "vpcid", "vpc-2zeou1uod4ylaXXXXXXXX" },
            //     { "container_cidr", "10.0.0.0/8" },
            //     { "service_cidr", "172.16.1.0/20" },
            //     { "security_group_id", "sg-2ze1a0rlgeo7XXXXXXXX" },
            //     { "vswitch_ids", new List<string>{"vsw-2zei30dhfldu8XXXXXXXX"} },
            // };
            // string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
            // request.Body = Encoding.UTF8.GetBytes(jsonBody);
            // request.Headers[ContentType] = "application/json; charset=utf-8";

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

            // // ROA接口DELETE请求
            // String httpMethod = "DELETE";
            // // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
            // String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX");
            // String host = "cs.cn-beijing.aliyuncs.com"; 
            // String xAcsAction = "DeleteCluster"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

            GetAuthorization(request);
            // 调用API
            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
                {
                    Method = new HttpMethod(request.HttpMethod),
                    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());
                    }
                }

                if (request.Body != null)
                {
                    HttpContent content = new ByteArrayContent(request.Body);
                    string contentType = request.Headers["content-type"].ToString();
                    content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
                    requestMessage.Content = content;
                }
                
                // 发送请求
                HttpResponseMessage response = await httpClient.SendAsync(requestMessage);
                // 读取响应内容
                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() ?? "")}");
                }

                byte[] requestPayload = request.Body==null ? Encoding.UTF8.GetBytes("") : 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(Encoding.UTF8.GetBytes(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 string FormDataToString(Dictionary<string, object> formData)
        {
            Dictionary<string, object> tileMap = FlattenDictionary( formData);
            
            StringBuilder result = new StringBuilder();
            bool first = true;
            string symbol = "&";

            foreach (var entry in tileMap)
            {
                string value = entry.Value.ToString();
                if (!string.IsNullOrEmpty(value))
                {
                    if (!first)
                    {
                        result.Append(symbol);
                    }
                    first = false;
                    result.Append(PercentCode(entry.Key));
                    result.Append("=");
                    result.Append(PercentCode(value));
                }
            }
            return result.ToString();
        }

        private static Dictionary<string, object> FlattenDictionary(Dictionary<string, object> dictionary, string prefix = "")
        {
            var result = new Dictionary<string, object>();
            foreach (var kvp in dictionary)
            {
                string key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";

                if (kvp.Value is Dictionary<string, object> nestedDict)
                {
                    var nestedResult = FlattenDictionary(nestedDict, key);
                    foreach (var nestedKvp in nestedResult)
                    {
                        result[nestedKvp.Key] = nestedKvp.Value;
                    }
                }
                else if (kvp.Value is List<string> list)
                {
                    for (int i = 0; i < list.Count; i++)
                    {
                        result[$"{key}.{i + 1}"] = list[i];
                    }
                }
                else
                {
                    result[key] = kvp.Value;
                }
            }
            return result;
        }

        private static string HmacSha256(string key, string message)
        {
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
            {
                byte[] hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
                return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
            }
        }

        private static string Sha256Hash(byte[] input)
        {
            byte[] hashBytes = SHA256.HashData(input);
            return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        }

        private static string PercentCode(string str)
        {
            if (string.IsNullOrEmpty(str))
            {
                throw new ArgumentException("输入字符串不可为null或空");
            }
            return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
        }
    }
}

Rust示例

说明

示例代码的运行环境是rustc 1.82.0,您可能需要根据具体情况对代码进行相应的调整。

运行Rust示例,需要您在Cargo.toml中添加以下依赖。

[dependencies]
serde = { version = "1.0" }
serde_json = "1.0"
rand = "0.8"
base64 = "0.21"
sha2 = "0.10"
chrono = "0.4"
hmac = "0.12"
hex = "0.4"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
percent-encoding = "2.1"
use core::str;
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::time::{SystemTime, SystemTimeError};
use chrono::DateTime;
use hmac::{Hmac, Mac};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use rand::Rng;
use serde_json::{json, Value}; 
use std::borrow::Cow;    
use reqwest::{
    Client,
    header::{HeaderMap, HeaderValue}, Method, Response, StatusCode,
};
use sha2::{Digest, Sha256};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;


// 生成 x-acs-date
pub fn current_timestamp() -> Result<u64, SystemTimeError> {
    Ok(SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs())
}
// URL编码处理
pub fn percent_code(encode_str: &str) -> Cow<'_, str> {
    let encoded = utf8_percent_encode(encode_str, NON_ALPHANUMERIC)
        .to_string()
        .replace("+", "20%")
        .replace("%5F", "_")
        .replace("%2D", "-")
        .replace("%2E", ".")
        .replace("%7E", "~");
        
    Cow::Owned(encoded) // 返回一个 Cow<str> 可以持有 String 或 &str
}

fn flatten_target_ops(
    targets: Vec<HashMap<&str, &str>>,
    base_key: &str,
) -> Vec<(&'static str, &'static str)> {
    let mut result = Vec::new();

    for (idx, item) in targets.iter().enumerate() {
        let prefix = format!("{}.{}", base_key, idx + 1);

        for (&k, &v) in item {
            let key = format!("{}.{}", prefix, k);
            let key_static: &'static str = Box::leak(key.into_boxed_str());
            let value_static: &'static str = Box::leak(v.to_string().into_boxed_str());

            result.push((key_static, value_static));
        }
    }

    result
}

/// 计算SHA256哈希
pub fn sha256_hex(message: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(message);
    format!("{:x}", hasher.finalize()).to_lowercase()
}
// HMAC SHA256
pub fn hmac256(key: &[u8], message: &str) -> Result<Vec<u8>, String> {
    let mut mac = Hmac::<Sha256>::new_from_slice(key)
        .map_err(|e| format!("use data key on sha256 fail:{}", e))?;
    mac.update(message.as_bytes());
    let signature = mac.finalize();
    Ok(signature.into_bytes().to_vec())
}
// 生成签名唯一随机数
pub fn generate_random_string(length: usize) -> String {
    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
    let mut rng = rand::thread_rng();
    (0..length)
        .map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char)
        .collect()
}
pub fn generate_nonce() -> String {
    generate_random_string(32)
}
// 构建规范化查询参数(编码后)
pub fn build_sored_encoded_query_string(query_params: &[(&str, &str)]) -> String {
    let sorted_query_params: BTreeMap<_, _> = query_params.iter().copied().collect();
    let encoded_params: Vec<String> = sorted_query_params
        .into_iter()
        .map(|(k, v)| {
            let encoded_key = percent_code(k);
            let encoded_value = percent_code(v);
            format!("{}={}", encoded_key, encoded_value)
        })
        .collect();
    encoded_params.join("&")
}
// 读取响应
pub async fn read_response(result: Response) -> Result<(StatusCode, String), String> {
    let status = result.status();
    let data = result.bytes().await.map_err(|e| format!("Read response body failed: {}", e))?;
    let res = match str::from_utf8(&data) {
        Ok(s) => s.to_string(),
        Err(_) => return Err("Body contains non UTF-8 characters".to_string()),
    };
    Ok((status, res))
}
// 定义 FormData 类型数据的value类型
#[derive(Debug, Clone)]
pub enum FormValue {
    String(String),
    Vec(Vec<String>),
    HashMap(HashMap<String, String>),
}
// 定义一个body请求体枚举,用于统一处理请求体类型,包含Json/Binary/FormData类型 
pub enum RequestBody {
    Json(HashMap<String, Value>), // Json
    Binary(Vec<u8>), // Binary
    FormData(HashMap<String, FormValue>), //  FormData 
    None,
}
// 规范化请求
pub async fn call_api(
    client: Client,
    method: Method,
    host: &str,
    canonical_uri: &str,
    query_params: &[(&str, &str)], 
    action: &str,
    version: &str,
    body: RequestBody,   
    access_key_id: &str,
    access_key_secret: &str,
) -> Result<String, String> {

    // 根据 body 类型处理请求体内容,将处理后的存储在 body_content 变量中。
    let body_content = match &body { 
        RequestBody::Json(body_map) => json!(body_map).to_string(),  
        RequestBody::Binary(binary_data) => {
            STANDARD.encode(binary_data)
        },
        RequestBody::FormData(form_data) => {
            let params: Vec<String> = form_data
            .iter()
            .flat_map(|(k, v)| {
                match v {
                    FormValue::String(s) => {
                        vec![format!("{}={}", percent_code(k), percent_code(&s))]
                    },
                    FormValue::Vec(vec) => {
                        vec.iter()
                            .map(|s| format!("{}={}", percent_code(k), percent_code(s)))
                            .collect::<Vec<_>>()
                    },
                    FormValue::HashMap(map) => {
                        map.iter()
                            .map(|(sk, sv)| format!("{}={}", percent_code(sk), percent_code(sv)))
                            .collect::<Vec<_>>()
                    },
                }
            })
            .collect();
            params.join("&") 
        },
        RequestBody::None => String::new(),
    };
    
    // 计算 请求体body的x-acs-content-sha256 ;准备x-acs-date; x-acs-signature-nonce;待签名请求头
    let hashed_request_payload = if body_content.is_empty() {
        sha256_hex("") 
    } else {
        sha256_hex(&body_content) 
    };
    // x-acs-date
    let now_time = current_timestamp().map_err(|e| format!("Get current timestamp failed: {}", e))?;
    let datetime = DateTime::from_timestamp(now_time as i64, 0).ok_or_else(|| format!("Get datetime from timestamp failed: {}", now_time))?;
    let datetime_str = datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string();
    // x-acs-signature-nonce
    let signature_nonce = generate_nonce();
    println!("Signature Nonce: {}", signature_nonce);
    // 待签名请求头
    let sign_header_arr = &[
        "host",
        "x-acs-action",
        "x-acs-content-sha256",
        "x-acs-date",
        "x-acs-signature-nonce",
        "x-acs-version",
    ];
    let sign_headers = sign_header_arr.join(";");
    // 1.构造规范化请求头
    let mut headers = HeaderMap::new();
    headers.insert("Host", HeaderValue::from_str(host).unwrap());
    headers.insert("x-acs-action", HeaderValue::from_str(action).unwrap());
    headers.insert("x-acs-version", HeaderValue::from_str(version).unwrap());
    headers.insert("x-acs-date", HeaderValue::from_str(&datetime_str).unwrap());
    headers.insert("x-acs-signature-nonce", HeaderValue::from_str(&signature_nonce).unwrap());
    headers.insert("x-acs-content-sha256", HeaderValue::from_str(&hashed_request_payload).unwrap());
    // 2.构造待签名请求头
    let canonical_query_string = build_sored_encoded_query_string(query_params); // 参数编码拼接处理
    println!("CanonicalQueryString: {}", canonical_query_string);
    let canonical_request = format!(
        "{}\n{}\n{}\n{}\n\n{}\n{}",
        method.as_str(),
        canonical_uri,
        canonical_query_string,
        sign_header_arr.iter().map(|&header| format!("{}:{}", header, headers[header].to_str().unwrap())).collect::<Vec<_>>().join("\n"),
        sign_headers,
        hashed_request_payload
    );
    println!("Canonical Request: {}", canonical_request);
    // 3.计算待签名请求头的 SHA-256 哈希;
    let result = sha256_hex(&canonical_request);
    // 4.构建待签名字符串
    let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", result);
    // 5.计算签名
    let signature = hmac256(access_key_secret.as_bytes(), &string_to_sign)?;
    let data_sign = hex::encode(&signature);
    let auth_data = format!(
        "ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}",
        access_key_id, sign_headers, data_sign
    );
    // 6.构建 Authorization
    headers.insert("Authorization", HeaderValue::from_str(&auth_data).unwrap());
    // 构造 url 拼接请求参数
    let url: String;
    if !query_params.is_empty() {
        url = format!("https://{}{}?{}", host, canonical_uri,canonical_query_string);
    } else {
        url = format!("https://{}{}", host, canonical_uri);
    }        
    // 调用发送请求
    let response = send_request(
        &client,
        method,
        &url,
        headers,
        query_params,                
        &body,                      
        &body_content,                
    ) 
    .await?;
    
    // 读取响应
    let (_, res) = read_response(response).await?;
    Ok(res)
}

/// 发送请求
async fn send_request(
    client: &Client,
    method: Method,
    url: &str,
    headers: HeaderMap,
    query_params: &[(&str, &str)],     // 接收 query 参数
    body: &RequestBody,                // 用此判断 body 数据类型
    body_content: &str,                // body 不为空时 接收 body 请求参数 FormData/Json/Binary
) -> Result<Response, String> {
    let mut request_builder = client.request(method.clone(), url);
    // 添加请求头 headers
    for (k, v) in headers.iter() {
        request_builder = request_builder.header(k, v.clone());
    }
     // 添加请求体 body
     match body {
        RequestBody::Binary(_) => {
            request_builder = request_builder.header("Content-Type", "application/octet-stream");
            request_builder = request_builder.body(body_content.to_string()); // 移动这里的值
        }
        RequestBody::Json(_) => {
            // 如果body为map,且不为空,转化为Json后存储在 body_content 变量中,设置  application/json; charset=utf-8
            if !body_content.is_empty() { 
                request_builder = request_builder.body(body_content.to_string());
                request_builder = request_builder.header("Content-Type", "application/json; charset=utf-8");
            }
        }
        RequestBody::FormData(_) => {
            // 处理 form-data 类型,设置 content-type
            if !body_content.is_empty() { 
            request_builder = request_builder.header("Content-Type", "application/x-www-form-urlencoded");
            request_builder = request_builder.body(body_content.to_string());
            }
        }
        RequestBody::None => {
            request_builder = request_builder.body(String::new());
        }
    }
    // 构建请求
    let request = request_builder
        .build()
        .map_err(|e| format!("build request fail: {}", e))?;
    // 发起请求
    let response = client
        .execute(request)
        .await
        .map_err(|e| format!("execute request fail: {}", e))?;
    // 返回结果
    Ok(response)
}


 /**
     * 
     * 签名示例,您需要根据实际情况替换main方法中的示例参数。
     * <p>
     * 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in)。
     * 1. 请求参数在元数据中显示"in":"query",通过query_params传参。
     * 2. 请求参数在元数据中显示"in": "body",通过body传参。
     * 2. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
#[tokio::main]
async fn main() {
    // 创建 HTTP 客户端
    let client = Client::new();
    // env::var()表示通过环境变量获取Access Key ID和Access Key Secret
    let access_key_id = env::var("ALIBABA_CLOUD_ACCESS_KEY_ID").expect("Cannot get access key id.");
    let access_key_secret = env::var("ALIBABA_CLOUD_ACCESS_KEY_SECRET").expect("Cannot get access key id.");
    let access_key_id: &str = &access_key_id;
    let access_key_secret: &str = &access_key_secret;
    
    // RPC接口请求示例一:请求参数"in":"query"   POST
    let method = Method::POST; // 请求方法
    let host = "ecs.cn-hangzhou.aliyuncs.com"; // endpoint
    let canonical_uri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
    let action = "DescribeInstanceStatus"; // API名称
    let version = "2014-05-26"; // API版本号
    let region_id = "cn-hangzhou";
    let instance_ids = vec![
        "i-bp11ht4XXXXXXXX",
        "i-bp16mazXXXXXXXX",
    ];
    let mut query: Vec<(&str, &str)> = Vec::new();
    query.push(("RegionId", region_id));
    for (index, instance_id) in instance_ids.iter().enumerate() {
        let key = format!("InstanceId.{}", index + 1); 
        query.push((Box::leak(key.into_boxed_str()), instance_id)); 
    }
    // query 参数
    let query_params: &[(&str, &str)] = &query;
    // 请求体 body 为空时
    let body = RequestBody:: None;
    
    // RPC接口 "in":"query" query为复杂类型参数  POST
    // let method = Method::POST; // 请求方法
    // let host = "tds.cn-shanghai.aliyuncs.com"; // endpoint
    // let canonical_uri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
    // let action = "AddAssetSelectionCriteria"; // API名称
    // let version = "2018-12-03"; // API版本号
    // 定义参数
    // let mut target_op = HashMap::new();
    // target_op.insert("Operation", "add");
    // target_op.insert("Target", "i-2ze1j7ocdXXXXXXXX");
    // 定义参数 TargetOperationList ,集合中传入map类型
    // let target_operation_list = vec![target_op];
    // 平铺参数
    // let mut query = flatten_target_ops(target_operation_list, "TargetOperationList");
    // 普通参数
    // query.push(("SelectionKey", "85a561b7-27d5-47ad-a0ec-XXXXXXXX"));
    // let query_params: &[(&str, &str)] = &query;
    // let body = RequestBody:: None;
    
    // RPC接口请求示例二:请求参数"in":"body"  POST
    // let method = Method::POST; // 请求方法
    // let host = "ocr-api.cn-hangzhou.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "RecognizeGeneral";
    // let version = "2021-07-07";
    // 请求参数"in":"body" 二进制文件类型 
    // let binary_data = std::fs::read("<FILE_PATH>").expect("读文件失败"); // <FILE_PATH>需替换为实际文件路径
    // 当 body 为 二进制类型时
    // let body = RequestBody::Binary(binary_data);
    // query 参数为空
    // let query_params = &[];
 
    // RPC接口请求示例三:请求参数"in": "formData"  POST
    // let method = Method::POST; // 请求方法
    // let host = "mt.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "TranslateGeneral";
    // let version = "2018-10-12";
    // // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
    // let mut form_data = HashMap::new();  // body 类型为 FormData(HashMap<String, FormValue>)   FormValue  可支持Vec<String>, HashSet<String> 或者 HashMap<String, String> ...,更多类型可在FormValue枚举中添加
    // form_data.insert(String::from("FormatType"),FormValue::String(String::from("text")));
    // form_data.insert(String::from("SourceLanguage"),FormValue::String(String::from("zh")));
    // form_data.insert(String::from("TargetLanguage"),FormValue::String(String::from("en")));
    // form_data.insert(String::from("SourceText"),FormValue::String(String::from("你好")));
    // form_data.insert(String::from("Scene"),FormValue::String(String::from("general")));
    // query 参数
    // let query_params = &[("Context", "早上")];
    // body 为 FormData 类型时 "in":"formdata"
    // let body = RequestBody::FormData(form_data);

    // ROA接口POST请求  API:CreateCluster创建集群  
    // 定义API请求常量
    // let method = Method::POST; // 请求方法
    // let host = "cs.cn-hangzhou.aliyuncs.com";
    // let canonical_uri = "/clusters";
    // let action = "CreateCluster";
    // let version = "2015-12-15";
    // 设置请求体参数
    // let mut body_json = HashMap::new();  //  body 类型为 Json(HashMap<String, Value>)  Value支持类型:Value::String("test".to_string()) // String  Value::Number(serde_json::Number::from(42)) // Number  Value::Bool(true) // Boolean  Value::Null // Null  Value::Array(vec![Value::from(1), Value::from(2), Value::from(3)]) //Array json!({"nested_key": "nested_value"})
    // body_json.insert(String::from("name"),json!("测试集群"));
    // body_json.insert(String::from("region_id"),json!("cn-hangzhou"));
    // body_json.insert(String::from("cluster_type"),json!("ExternalKubernetes"));
    // body_json.insert(String::from("vpcid"),json!("vpc-2zeou1uodXXXXXXXX"));
    // body_json.insert(String::from("container_cidr"),json!("10.X.X.X/X"));
    // body_json.insert(String::from("service_cidr"),json!("10.X.X.X/X"));
    // body_json.insert(String::from("security_group_id"),json!("sg-2ze1a0rlgXXXXXXXX"));
    // body_json.insert(
    //     String::from("vswitch_ids"),
    //     Value::Array(vec![
    //         Value::from("vsw-2zei30dhflXXXXXXXX"),
    //         Value::from("vsw-2zei30dhflXXXXXXXX"),
    //         Value::from("vsw-2zei30dhflXXXXXXXX"),
    //     ]),
    // );
    // query 参数为空
    // let query_params = &[];
    // body 为 Json 类型时 
    // let body = RequestBody::Json(body_json);

    // ROA接口GET请求   API:DeleteCluster  查询指定集群的关联资源
    // let method = Method::GET; // 请求方法
    // let host = "cs.cn-hangzhou.aliyuncs.com"; // endpoint
    // // 拼接资源路径
    // let uri = format!("/clusters/{}/resources", percent_code("ce196d21571a64be9XXXXXXXX").as_ref()); 
    // let canonical_uri = uri.as_str(); // 资源路径  转化为&Str类型
    // let action = "DescribeClusterResources";   // API名称
    // let version = "2015-12-15"; // API版本号
    // // 设置 query 参数
    // let query_params = &[("with_addon_resources", if true { "true" } else { "false" })];  // "true" or "false"
    // // 设置 body 参数为空
    // let body = RequestBody:: None;

    // ROA接口DELETE请求   API:DeleteCluster  DELETE请求删除一个按量付费的集群
    // let method = Method::DELETE;
    // let host = "cs.cn-hangzhou.aliyuncs.com";
    // let uri = format!("/clusters/{}", percent_code("ce0138ff31ad044f8XXXXXXXX").as_ref()); 
    // let canonical_uri = uri.as_str(); // 资源路径转化为&Str类型
    // let action = "DeleteCluster";
    // let version = "2015-12-15";
    // // query 参数
    // let query_params = &[];
    // // body 参数为空时
    // let body = RequestBody:: None;
    
    // 发送短信 API
    // let method = Method::POST; // 请求方法
    // let host = "dysmsapi.aliyuncs.com"; // endpoint
    // let canonical_uri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
    // let action = "SendSms"; // API名称
    // let version = "2017-05-25"; // API版本号
    // let mut query: Vec<(&str, &str)> = Vec::new();
    // query.push(("PhoneNumbers", "<YOUR_PHONENUMBERS>"));
    // query.push(("TemplateCode", "<YOUR_TEMPLATECODE>"));
    // query.push(("SignName", "<YOUR_SIGNNAME>"));
    // query.push(("TemplateParam", "<YOUR_TEMPLATEPARAM>"));
    // // query 参数
    // let query_params: &[(&str, &str)] = &query;
    // // 请求体 body 为空时
    // let body = RequestBody:: None;

    // 发起请求
    match call_api(
        client.clone(),
        method,                                                  // API请求方式 POST/GET/DELETE                                
        host,                                                    // API服务地址
        canonical_uri,                                           // API资源路径
        query_params,                                            // "in":"query" 查询参数
        action,                                                  // API名称
        version,                                                 // API版本号
        body,                                                    // "in":"body" 请求体参数 支持Json/FormData/Binary类型
        access_key_id,                                           
        access_key_secret,
    )
    .await {
        Ok(response) => println!("响应信息: {}", response),
        Err(error) => eprintln!("异常: {}", error),
    }
}

Shell脚本示例

#!/bin/bash

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

# 请求参数 --这部分内容需要根据实际情况修改
httpMethod="POST"
host="dns.aliyuncs.com"
queryParam=("DomainName=example.com" "RRKeyWord=@")
action="DescribeDomainRecords"
version="2015-01-09"
canonicalURI="/"
# body类型参数或者formdata类型参数通过body传参
# body类型参数:body的值为json字符串: "{'key1':'value1','key2':'value2'}",且需在签名header中添加content-type:application/json; charset=utf-8
# body类型参数是二进制文件时:body无需修改,只需在签名header中添加content-type:application/octet-stream,并在curl_command中添加--data-binary参数
# formdata类型参数:body参数格式:"key1=value1&key2=value2",且需在签名header中添加content-type:application/x-www-form-urlencoded
body=""

# 按照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}"

# URL编码函数
urlencode() {
    local string="${1}"
    local strlen=${#string}
    local encoded=""
    local pos c o

    for (( pos=0 ; pos<strlen ; pos++ )); do
        c=${string:$pos:1}
        case "$c" in
            [-_.~a-zA-Z0-9] ) o="${c}" ;;
            * )               printf -v o '%%%02X' "'$c"
        esac
        encoded+="${o}"
    done
    echo "${encoded}"
}

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

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

        # 对值进行URL编码
        value=$(urlencode "$value")

        # 检查值是否为一个列表(通过查找括号)
        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+=("$key=$value")
        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 "+++++++++++++++++++++++++++++++++++++++++++++++++++"

str=$(echo "$canonicalRequest" | sed 's/%/%%/g')
hashedCanonicalRequest=$(printf "${str}" | 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'"
# body类型参数是二进制文件时,需要注释掉下面这行代码
curl_command+=" -d '$body'"
# body类型参数是二进制文件时,需要放开下面这行代码的注释
#curl_command+=" --data-binary @"/root/001.png" "

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

C语言示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>
#include <stdint.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/rand.h>
#include <curl/curl.h>

// getenv()表示通过环境变量获取Access Key ID和Access Key Secret
#define ACCESS_KEY_ID getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
#define ACCESS_KEY_SECRET getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
#define ALGORITHM "ACS3-HMAC-SHA256"
#define BUFFER_SIZE 4096

// 用于排序的结构体
typedef struct {
    char key[256];
    char value[256];
} KeyValuePair;

// 比较函数:按 key 字典序排序
int compare_pairs(const void *a, const void *b) {
    return strcmp(((const KeyValuePair *)a)->key, ((const KeyValuePair *)b)->key);
}

// URL编码
char* percentEncode(const char* str) {
    if (str == NULL) {
        fprintf(stderr, "输入字符串不可为null\n");
        return NULL;
    }
    size_t len = strlen(str);
    char* encoded = (char*)malloc(len * 3 + 1);
    if (encoded == NULL) {
        fprintf(stderr, "内存分配失败\n");
        free(encoded); 
        return NULL;
    }
    char* ptr = encoded;
    for (size_t i = 0; i < len; i++) {
        unsigned char c = (unsigned char)str[i];
        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
            *ptr++ = c;
        } else {
            ptr += sprintf(ptr, "%%%02X", c);
        }
    }
    *ptr = '\0'; 
    char* finalEncoded = malloc(strlen(encoded) + 1);
    if (finalEncoded) {
        char* fptr = finalEncoded;
        for (size_t j = 0; j < strlen(encoded); j++) {
            if (encoded[j] == '+') {
                strcpy(fptr, "%20");
                fptr += 3; 
            } else if (encoded[j] == '*') {
                strcpy(fptr, "%2A");
                fptr += 3;
            } else if (encoded[j] == '~') {
                *fptr++ = '~';
            } else {
                *fptr++ = encoded[j];
            }
        }
        *fptr = '\0'; 
    }

    free(encoded); 
    return finalEncoded;
}

/**
 * @brief 对查询参数进行 URL 编码、按字典序排序,并生成规范化的查询字符串
 * @param query_params 原始查询参数字符串(格式如 "key1=value1&key2=value2")
 * @return char* 已排序并编码后的规范化查询字符串(需要调用者释放内存)
 */
char* generate_sorted_encoded_query(const char* query_params) {
    if (query_params == NULL || strlen(query_params) == 0) {
        return strdup(""); // 空参数返回空字符串
    }

    KeyValuePair pairs[100]; // 最多支持 100 个键值对
    int pair_count = 0;

    char* copy = strdup(query_params);
    if (!copy) {
        fprintf(stderr, "内存分配失败\n");
        return NULL;
    }

    char* token = NULL;
    char* saveptr = NULL;
    token = strtok_r(copy, "&", &saveptr);

    while (token != NULL && pair_count < 100) {
        char* eq = strchr(token, '=');
        if (eq) {
            size_t key_len = eq - token;
            char key[256], value[256];

            strncpy(key, token, key_len);
            key[key_len] = '\0';

            const char* val = eq + 1;
            strncpy(value, val, sizeof(value) - 1);
            value[sizeof(value) - 1] = '\0';

            char* encoded_key = percentEncode(key);
            char* encoded_value = percentEncode(value);

            strncpy(pairs[pair_count].key, encoded_key, sizeof(pairs[pair_count].key));
            strncpy(pairs[pair_count].value, encoded_value, sizeof(pairs[pair_count].value));
            pair_count++;

            free(encoded_key);
            free(encoded_value);
        }
        token = strtok_r(NULL, "&", &saveptr);
    }

    free(copy);

    // 按 key 排序
    qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);

    // 拼接排序后的查询字符串
    char* query_sorted = malloc(BUFFER_SIZE);
    if (!query_sorted) {
        fprintf(stderr, "内存分配失败\n");
        return NULL;
    }
    query_sorted[0] = '\0';

    for (int i = 0; i < pair_count; ++i) {
        if (i == 0) {
            snprintf(query_sorted, BUFFER_SIZE, "%s=%s", pairs[i].key, pairs[i].value);
        } else {
            char temp[512];
            snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
            strncat(query_sorted, temp, BUFFER_SIZE - strlen(query_sorted) - 1);
        }
    }

    return query_sorted;
}

// HMAC-SHA256计算
void hmac256(const char *key, const char *message, char *output) {
    unsigned char hmac[SHA256_DIGEST_LENGTH];
    unsigned int result_len;
    HMAC(EVP_sha256(), key, strlen(key), (unsigned char *)message, strlen(message), hmac, &result_len);
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        sprintf(output + (i * 2), "%02x", hmac[i]);
    }
    output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// 计算 SHA-256 哈希
void sha256_hex(const char *input, char *output) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((unsigned char *)input, strlen(input), hash);
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        sprintf(output + (i * 2), "%02x", hash[i]);
    }
    output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// 用于生成 x-acs-signature-nonce
void generate_uuid(char *uuid, size_t size) {
    if (size < 37) { 
        fprintf(stderr, "Buffer size too small for UUID\n");
        return;
    }
    unsigned char random_bytes[16];
    RAND_bytes(random_bytes, sizeof(random_bytes));
    random_bytes[6] &= 0x0f; // 保留高4位
    random_bytes[6] |= 0x40; // 将版本设置为4
    random_bytes[8] &= 0x3f; // 保留高2位
    random_bytes[8] |= 0x80; // 将变体设置为10xx
    snprintf(uuid, size,
             "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
             random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
             random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
             random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
             random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
}
// 上传文件
size_t read_file(const char *file_path, char **buffer) {
    FILE *file = fopen(file_path, "rb");
    if (!file) {
        fprintf(stderr, "Cannot open file %s\n", file_path);
        return 0; // 读取失败
    }
    fseek(file, 0, SEEK_END);
    size_t file_size = ftell(file);
    fseek(file, 0, SEEK_SET);

    *buffer = (char *)malloc(file_size);
    if (!*buffer) {
        fprintf(stderr, "Failed to allocate memory for file buffer\n");
        fclose(file);
        return 0; // 读取失败
    }
    fread(*buffer, 1, file_size, file);
    fclose(file);
    return file_size; // 返回读取的字节数
}
// 计算 Authorization
char* get_authorization(const char *http_method, const char *canonical_uri, const char *host,
                       const char *x_acs_action, const char *x_acs_version, const char *query_params,
                       const char *body, char *authorization_header,
                        char *hashed_payload, char *x_acs_date, char *uuid) {
    // 准备 x-acs-signature-nonce;x-acs-date;x-acs-content-sha256;待签名字符串
    generate_uuid(uuid, 37);
    // x-acs-date 格式为yyyy-MM-ddTHH:mm:ssZ,例如2025-04-17T07:19:10Z
    time_t now = time(NULL);
    struct tm *utc_time = gmtime(&now);
    strftime(x_acs_date, 64, "%Y-%m-%dT%H:%M:%SZ", utc_time);
    // 待签名字符串
    char signed_headers[] = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
    // x-acs-content-sha256 
    sha256_hex(body ? body : "", hashed_payload);
    printf("Generated x-acs-content-sha256: %s\n", hashed_payload);
    // 1.构造规范化请求头
    char canonical_headers[BUFFER_SIZE];
    snprintf(canonical_headers, sizeof(canonical_headers),
             "host:%s\nx-acs-action:%s\nx-acs-content-sha256:%s\nx-acs-date:%s\nx-acs-signature-nonce:%s\nx-acs-version:%s",
              host, x_acs_action, hashed_payload, x_acs_date, uuid, x_acs_version);
    printf("Canonical Headers:\n%s\n", canonical_headers);

    // 2.构造待签名的请求头
    // 对query参数进行排序编码处理
    char* sorted_query_params = generate_sorted_encoded_query(query_params);
    if (!sorted_query_params) {
      fprintf(stderr, "生成排序后的查询字符串失败\n");
      return NULL;
    }
    char canonical_request[BUFFER_SIZE];
    snprintf(canonical_request, sizeof(canonical_request),
         "%s\n%s\n%s\n%s\n\n%s\n%s",
         http_method,
         canonical_uri,
         sorted_query_params ? sorted_query_params : "",
         canonical_headers,
         signed_headers,
         hashed_payload);
    printf("Canonical Request:\n%s\n", canonical_request);

    // 3.计算规范化请求的SHA-256哈希
    char hashed_canonical_request[SHA256_DIGEST_LENGTH * 2 + 1];
    sha256_hex(canonical_request, hashed_canonical_request);
    printf("hashedCanonicalRequest: %s\n", hashed_canonical_request);
    // 4.构建待签名字符串 
    char string_to_sign[BUFFER_SIZE];
    snprintf(string_to_sign, sizeof(string_to_sign), "%s\n%s", ALGORITHM, hashed_canonical_request);
    printf("stringToSign:\n%s\n", string_to_sign);
    // 5.计算签名 
    char signature[SHA256_DIGEST_LENGTH * 2 + 1];
    hmac256(ACCESS_KEY_SECRET, string_to_sign, signature);
    printf("Signature: %s\n", signature);
    // 6.构建 Authorization
    snprintf(authorization_header, BUFFER_SIZE,
             "%s Credential=%s,SignedHeaders=%s,Signature=%s",
             ALGORITHM, ACCESS_KEY_ID, signed_headers, signature);
    printf("Authorization: %s\n", authorization_header);

    return sorted_query_params;
}
// 发送请求
void call_api(const char *http_method, const char *canonical_uri, const char *host,
              const char *x_acs_action, const char *x_acs_version, const char *query_params,
              const char *body,const char *content_type, size_t body_length) {
    // 获取计算签名所需的参数值
    char authorization_header[BUFFER_SIZE];
    char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
    char x_acs_date[64];
    char uuid[37];
    // 1.初始化curl
    CURL *curl = curl_easy_init();
    if (!curl) {
        fprintf(stderr, "curl_easy_init() failed\n");
        goto cleanup;
    }
    // 2.计算签名 (返回已经排序编码处理后的query参数)
    char *signed_query_params = get_authorization(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, authorization_header, hashed_payload, x_acs_date, uuid);
    // 3.添加请求参数 
    char url[BUFFER_SIZE];
    if (signed_query_params && strlen(signed_query_params) > 0) {
        snprintf(url, sizeof(url), "https://%s%s?%s", host, canonical_uri, signed_query_params);
    } else {
        snprintf(url, sizeof(url), "https://%s%s", host, canonical_uri);
    }
    printf("Request URL: %s\n", url);
    // 释放内存
    if (signed_query_params) {
        free(signed_query_params); // 释放内存
    }

    // 4.添加headers请求头
    struct curl_slist *headers = NULL;
    char header_value[BUFFER_SIZE];
    snprintf(header_value, sizeof(header_value), "Content-Type: %s", content_type);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "Authorization: %s", authorization_header);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "host: %s", host);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-action: %s", x_acs_action);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-content-sha256: %s", hashed_payload);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-date: %s", x_acs_date);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-signature-nonce: %s", uuid);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-version: %s", x_acs_version);
    headers = curl_slist_append(headers, header_value);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method);
    curl_easy_setopt(curl, CURLOPT_URL, url);
    // CURL其他设置禁用 SSL 验证,添加调试信息
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    // 5.添加 body 请求体
    if (body) {
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_length); 
        if (strcmp(content_type, "application/octet-stream") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        } else if (strcmp(content_type, "application/x-www-form-urlencoded") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        } else if (strcmp(content_type, "application/json; charset=utf-8") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        }
    }
    printf("RequestBody:%s\n",body);
    // 6.发起请求
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        goto cleanup;
    }
cleanup:
    if (headers) curl_slist_free_all(headers);
    if (curl) curl_easy_cleanup(curl);
}
/**
*
     * 签名示例,您需要根据实际情况替换main方法中的示例参数。
     * <p>
     * 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in)
     * 1. 请求参数在元数据中显示"in":"query",通过query_params传参。
     * 2. 请求参数在元数据中显示"in": "body",通过body传参。
     * 2. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
int main() {
    // 设置响应格式为UTF-8
    SetConsoleOutputCP(CP_UTF8);
    srand((unsigned int)time(NULL));

    /**
      * RPC接口请求示例:请求参数"in":"query"  query类型为复杂类型
    */   
    const char *http_method = "POST";
    const char *canonical_uri = "/";
    const char *host = "tds.cn-shanghai.aliyuncs.com";
    const char *x_acs_action = "AddAssetSelectionCriteria";
    const char *x_acs_version = "2018-12-03";

    // 定义参数 SelectionKey 字符串类型
    const char *selection_key = "85a561b7-27d5-47ad-a0ec-XXXXXXXX";
    // 定义参数 TargetOperationList 集合 目标对象列表(可以扩展多个)
    struct {
        const char *operation;
        const char *target;
    } targetOperation_list[] = {
        {"add", "i-2ze1j7ocdg9XXXXXXXX"},
        // 可以添加更多条目
        // {"add", "i-abc123xyzXXXXX"},
    };

    int count = sizeof(targetOperation_list) / sizeof(targetOperation_list[0]);
    KeyValuePair pairs[100]; // 存储原始 key 和 value
    int pair_count = 0;

    for (int i = 0; i < count; ++i) {
      char op_key[128], target_key[128];
      snprintf(op_key, sizeof(op_key), "TargetOperationList.%d.Operation", i + 1);
      snprintf(target_key, sizeof(target_key), "TargetOperationList.%d.Target", i + 1);

      strncpy(pairs[pair_count].key, op_key, sizeof(pairs[pair_count].key));
      strncpy(pairs[pair_count].value, targetOperation_list[i].operation, sizeof(pairs[pair_count].value));
      pair_count++;

      strncpy(pairs[pair_count].key, target_key, sizeof(pairs[pair_count].key));
      strncpy(pairs[pair_count].value, targetOperation_list[i].target, sizeof(pairs[pair_count].value));
      pair_count++;
}
    // 添加 SelectionKey 参数
    snprintf(pairs[pair_count].key, sizeof(pairs[pair_count].key), "SelectionKey");
    snprintf(pairs[pair_count].value, sizeof(pairs[pair_count].value), "%s", selection_key);
    pair_count++;

    // 排序和编码都在 get_authorization() 中完成
    qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);

    // 构造原始 query string(未编码)
    char query_params[BUFFER_SIZE] = {0};
    for (int i = 0; i < pair_count; ++i) {
      if (i == 0) {
        snprintf(query_params, sizeof(query_params), "%s=%s", pairs[i].key, pairs[i].value);
     } else {
        char temp[512];
        snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
        strncat(query_params, temp, sizeof(query_params) - strlen(query_params) - 1);
    }
}
    const char *body = ""; // 请求体为空
    const char *content_type = "application/json; charset=utf-8";
    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

    /**
      * RPC接口请求示例:请求参数"in":"query"
    */
    // 定义API请求参数
    // const char *http_method = "POST";
    // const char *canonical_uri = "/";
    // const char *host = "ecs.cn-hangzhou.aliyuncs.com";
    // const char *x_acs_action = "DescribeInstanceStatus";
    // const char *x_acs_version = "2014-05-26";
    // // 定义参数 InstanceId 数组 InstanceId为 可选参数
    // const char *instance_ids[] = {
    //     "i-bp11ht4hXXXXXXXX",
    //     "i-bp16maz3XXXXXXXX"
    // };
    // // 拼接InstanceId数组
    // char InstanceId[BUFFER_SIZE];
    // snprintf(InstanceId, sizeof(InstanceId),
    //          "InstanceId.1=%s&InstanceId.2=%s",
    //         instance_ids[0],
    //         instance_ids[1]);
    // // 定义查询参数 必填参数 :RegionId=cn-hangzhou    const char *query_params = "RegionId=cn-hangzhou";
    // char query_params[BUFFER_SIZE];
    // snprintf(query_params, sizeof(query_params),
    //          "%s&RegionId=cn-hangzhou", InstanceId);
    // const char *body = "";
    // const char *content_type = "application/json; charset=utf-8";
    // call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * RPC接口请求示例:请求参数"in":"body"
      */
    // 声明存储读取的文件内容的指针
    // char *body = NULL;
    // size_t body_length = read_file("<YOUR_FILE_PATH>", &body);
    // if (body_length > 0) {
    //   const char *http_method = "POST";
    //   const char *canonical_uri = "/";
    //   const char *host = "ocr-api.cn-hangzhou.aliyuncs.com";
    //   const char *x_acs_action = "RecognizeGeneral";
    //   const char *x_acs_version = "2021-07-07";
    //   const char *query_params = "";
    //   const char *content_type = "application/octet-stream";
    //   call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, body_length);
    //   free(body);
    // } else {
    //   fprintf(stderr, "File read error\n");
    // }

      /**
       * RPC接口请求示例:请求参数"in": "formData"
       */
    // const char *http_method = "POST";
    // const char *canonical_uri = "/";
    // const char *host = "mt.aliyuncs.com";
    // const char *x_acs_action = "TranslateGeneral";
    // const char *x_acs_version = "2018-10-12";
    // char query_params[BUFFER_SIZE];
    // snprintf(query_params, sizeof(query_params), "Context=%s", "早上");
    // const char *format_type = "text";
    // const char *source_language = "zh";
    // const char *target_language = "en";
    // const char *source_text = "你好";
    // const char *scene = "general";
    // char body[BUFFER_SIZE];
    // snprintf(body, sizeof(body),
    // "FormatType=%s&SourceLanguage=%s&TargetLanguage=%s&SourceText=%s&Scene=%s",
    // percentEncode(format_type), percentEncode(source_language), percentEncode(target_language),
    // percentEncode(source_text), percentEncode(scene));
    // const char *content_type = "application/x-www-form-urlencoded";
    // printf("formdate_body: %s\n", body);
    // call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

   // RPC接口请求示例三:请求参数"in": "formData"
//    const char *http_method = "POST";
//    const char *canonical_uri = "/";
//    const char *host = "sasti.aliyuncs.com";
//    const char *x_acs_action = "AskTextToTextMsg";
//    const char *x_acs_version = "2020-05-12";
//    // query
//    const char *query_params = "";
//    // body
//    const char *Memory = "false";
//    const char *Stream = "true";
//    const char *ProductCode = "sddp_pre";
//    const char *Feature = "{}";
//    const char *Model = "yunsec-llm-latest";
//    const char *Type = "Chat";
//    const char *TopP = "0.9";
//    const char *Temperature = "0.01";
//    const char *Prompt = "你是谁";
//    const char *Application = "sddp_pre";
//    char body[BUFFER_SIZE];
//    snprintf(body, sizeof(body),
//            "Memory=%s&Stream=%s&ProductCode=%s&Feature=%s&Model=%s&Type=%s&TopP=%s&Temperature=%s&Prompt=%s&Application=%s",
//            Memory, Stream, ProductCode, Feature, Model, Type, TopP, Temperature, Prompt, Application);
//    const char *content_type = "application/x-www-form-urlencoded";
//    printf("formdate_body: %s\n", body);
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * ROA接口POST请求"in" "body"
      */
//    const char *http_method = "POST";
//    const char *canonical_uri = "/clusters";
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "CreateCluster";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "";
//    char body[BUFFER_SIZE];
//    snprintf(body, sizeof(body),
//             "{\"name\":\"%s\",\"region_id\":\"%s\",\"cluster_type\":\"%s\","
//             "\"vpcid\":\"%s\",\"container_cidr\":\"%s\","
//             "\"service_cidr\":\"%s\",\"security_group_id\":\"%s\","
//             "\"vswitch_ids\":[\"%s\"]}",
//             "测试集群", "cn-beijing", "ExternalKubernetes",
//             "vpc-2zeou1uod4yXXXXXXXX", "10.X.X.X/XX",
//             "10.X.X.X/XX", "sg-2ze1a0rlgeXXXXXXXX",
//             "vsw-2zei30dhflXXXXXXXX");
//    const char *content_type = "application/json; charset=utf-8";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * ROA接口GET请求
      */
//    const char *http_method = "GET";
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s/resources", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "DescribeClusterResources";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "with_addon_resources=true";
//    const char *body = "";
//    const char *content_type = "";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        *  ROA接口DELETE请求
      */
//    const char *http_method = "DELETE";
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "DeleteCluster";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "";
//    const char *body = "";
//    const char *content_type = "";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));



    // 变量存储生成的值
    char authorization_header[BUFFER_SIZE];
    char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
    char x_acs_date[64];
    char uuid[37];
    return 0;
}

常见问题

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

问题原因:出现该问题通常是由于参数传递错误或者在签名机制的某个步骤中遗漏了必要的操作。常见的问题原因:

  • 参数可能是要拼接在URL上,但却使用了body传参。

  • CanonicalQueryString中参数未按字符顺序升序排列。

  • SignedHeaders未按升序排列。

  • 未将空格编码为%20

  • Accesskey配置错误。

解决方案:

说明

建议您先根据固定参数示例检查本地代码计算结果是否正确。

  1. 从报错信息中获取CanonicalRequestStringToSign的值。

  2. 首先,请对比报错信息中的CanonicalRequest与您计算的CanonicalRequest内容是否一致。若存在不一致,请查看步骤一:构造规范化请求,并检查您的代码中与文档介绍内容不一致的部分。常见的错误之一是参数传递位置不当,例如参数应当拼接在URL中,但却错误地通过请求body进行传递,这将导致参数CanonicalQueryStringx-acs-content-sha256的结果均出现错误。

  3. 当报错信息中的CanonicalRequest与您计算的CanonicalRequest内容一致时,请对比StringToSign与您计算的StringToSign的值是否一致。需要注意的是,StringToSignHashedCanonicalRequest的值应使用与x-acs-content-sha256相同的函数生成。

  4. 若仍然无法解决,请联系我们

如何使用Postman测试

无法直接通过Postman调用OpenAPI。若您期望使用Postman测试,可以根据以下步骤完成:

  1. 必须先通过代码计算出Authorization,并打印出计算过程中代码生成的x-acs-content-sha256x-acs-datex-acs-signature-nonce

    说明

    计算出Authorization后,请勿通过代码调用OpenAPI,否则当您将参数复制到Postman进行测试时,将会提示“Specified time stamp or date value is expired.”。

  2. PostmanHeaders中依次补充以下内容:

    Key

    Value

    host

    示例:dysmsapi.aliyuncs.com

    x-acs-action

    示例:SendSms

    x-acs-content-sha256

    示例:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

    x-acs-date

    示例:2025-04-16T07:45:55Z

    x-acs-signature-nonce

    示例:315484d3-b129-4966-974a-699b7ee56647

    x-acs-version

    示例:2017-05-25

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

  3. 根据参数类型设置参数:

    说明

    参数顺序需与计算签名时的顺序保持一致。

    • 如果是query参数,在Params中填入相关参数。

    • 如果是body参数,在Body中填入相关参数。

请求参数该如何传

OpenAPI元数据中定义了每个API的请求参数该如何传。例如:

  • "in":"query"表示参数需拼接在URL上,无需添加content-type。

  • "in": "body"表示通过body传参,需要在RequestHeader中添加content-type,content-type的值与请求内容类型有关。例如:

    • 请求内容类型为JSON数据时,content-type的值为application/json

    • 请求内容类型为二进制文件流时,content-type的值为application/octet-stream

  • "in": "formData"表示通过body传参。在传参时,需要将参数按照固定格式拼接为一个字符串,拼接格式为:key1=value1&key2=value2&key3=value3。同时需要在RequestHeader中添加content-type,content-type的值为application/x-www-form-urlencoded

API元数据中style的值不是RPCROA

RPCROA仅影响CanonicalURI的值。当style的值是其他类型时,可在API元数据中检查将调用的API概要信息中是否包含path参数,若存在,则CanonicalURI的值为该path的值,若不存在,则CanonicalURI的值为正斜杠(/)。例如在查询ACK集群列表的API元数据中path的值为/api/v1/clusters,那么CanonicalURI的值为/api/v1/clustersimage

当参数类型是array、object时,该如何传参

当参数类型是复杂数据结构时,需要将复杂数据结构平铺为一个映射结构(map)。例如DescribeInstanceStatus接口中InstanceId的参数类型是array,当参数值有多个时,需要将参数转换为如下结构:

{
	"InstanceId.1": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.10": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.11": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.12": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.2": "i-bp1incuofvzxXXXXXXXX",
	"InstanceId.3": "i-bp1incuofvzxXXXXXXXX",
	"InstanceId.4": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.5": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.6": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.7": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.8": "i-bp10igfmnyttXXXXXXXX",
	"InstanceId.9": "i-bp10igfmnyttXXXXXXXX"
}

API版本(x-acs-version)如何获取

  1. 访问阿里云 OpenAPI 开发者门户,选择您将要调用的API对应的云产品,这里以ECS为例。image

  2. 在云产品主页中可以查看推荐的API版本,例如ECS推荐的API版本为2014-05-26。

    image

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

  • 对于RPC接口,通常既支持GET请求也支持POST请求。

  • 对于ROA接口,则仅支持单一的请求方式。

如何获取API支持的请求方式,请参见OpenAPI元数据

调用API时提示“You are not authorized to do this operation. ”

问题原因:您所使用的AccessKey对应的RAM用户没有权限调用该API。

解决方法:请参见code 403,You are not authorized to do this operation. Action: xxxx。

如何获取AccessKey

AccessKey是阿里云为用户提供的永久访问凭据,由AccessKey IDAccessKey Secret组成的一对密钥。在通过调用API访问阿里云资源时,系统将对请求中携带的AccessKey ID以及通过AccessKey Secret加密生成的签名进行身份验证和请求合法性校验。如何获取AccessKey,请参见创建RAM用户的AccessKey

联系我们

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

    OSZAR »