全部產品
Search
文件中心

Alibaba Cloud SDK:V3版本請求體&簽名機制

更新時間:Dec 21, 2024

本文將通過介紹V3版本的簽名機制,協助您實現直接使用HTTP請求調用阿里雲OpenAPI。

說明

在OpenAPI門戶上提供了阿里雲SDK的相關產品,這些產品所提供的API支援使用V3版本簽名。如果您當前使用的是V2版本簽名進行API調用,也可以直接替換為V3版本簽名以進行調用。

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摘要後再base-16編碼的結果,與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 版本。您可以訪問阿里雲 OpenAPI 開發人員門戶,查看您調用OpenAPI對應的 API 版本。

2014-05-26

Authorization

String

非匿名請求必須

用於驗證請求合法性的認證資訊,格式為Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature

其中SignatureAlgorithm為簽名加密方式,為ACS3-HMAC-SHA256。

Credential 為使用者的存取金鑰ID。您可以在RAM 控制台查看您的 AccessKeyId。如需建立 AccessKey,請參見建立AccessKey

SignedHeaders為要求標頭中包含的參與簽名欄位鍵名,【說明】:除了Authorization之外,建議對所有公用要求標頭添加簽名,以提高安全性。

Signature為請求籤名,取值參見簽名機制。

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

x-acs-security-token

String

STS認證必傳

為調用AssumeRole介面傳回值中SecurityToken的值。

簽名機制

採用AK/SK方式進行簽名與認證。對於每一次HTTP或HTTPS協議請求,阿里雲API Gateway將依據請求參數資訊重新計算簽名,通過對比該簽名與請求中提供的簽名是否一致,從而驗證要求者的身份,以確保傳輸資料的完整性與安全性。

重要

請求及返回結果都使用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的資源路徑部分經過編碼之後的結果。資源路徑部分指URL中host與查詢字串之間的部分,包含host之後的/但不包含查詢字串前的?。使用者發起請求時的URI應使用正常化URI,編碼方式使用UTF-8字元集按照RFC3986的規則對URI中的每一部分(即被/分割開的字串)進行編碼:

    • 字元A~Z、a~z、0~9以及字元-_.~不編碼。

    • 其他字元編碼成%加字元對應ASCII碼的16進位。樣本:半形雙引號(")對應%22。需要注意的是,部分特殊字元需要特殊處理,具體如下:

      編碼前

      編碼後

      空格( )

      %20

      星號(*

      %2A

      %7E

      波浪號(~

      如果您使用的是Java標準庫中的java.net.URLEncoder,可以先用標準庫中encode編碼,隨後將編碼後的字元中加號(+)替換為%20、星號(*)替換為%2A%7E替換為波浪號(~),即可得到上述規則描述的編碼字串。

    重要

    RPC風格API使用正斜杠(/)作為CanonicalURI,

    ROA風格API該參數為OpenAPI中繼資料中path的值,例如/api/v1/clusters

  • CanonicalQueryString(正常化查詢字串)

    OpenAPI中繼資料中,如果API的請求參數資訊包含了"in":"query"時,需要將這請求參數按照如下構造方法拼接起來:

    1. 將查詢字串中的參數按照參數名的字元順序升序排列。

    2. 使用UTF-8字元集按照RFC3986的規則對每個參數的參數名和參數值分別進行URI編碼,具體規則與上一節中的CanonicalURI編碼規則相同。

    3. 使用等號(=)串連編碼後的請求參數名和參數值,對於沒有值的參數使用Null 字元串。

    4. 多個請求參數之間使用與號(&)串連。

    重要

    當請求的查詢字串為空白時,使用Null 字元串作為正常化查詢字串。

    樣本值:

    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的值為請求體(body)對應的JSON字串。在OpenAPI中繼資料中,如果API的請求參數資訊包含了"in": "body""in": "formData"時,需通過請求體(body)傳遞參數。若沒有請求體(body)時,RequestPayload的值固定為一個Null 字元串。

      重要
      • 當請求參數類型是集合(Array)、映射(map)時,需要將參數平鋪。例如{"key":["value1","value2"]}平鋪後為{"key.2":"value2","key.1":"value1"}

      • 當請求參數資訊包含"in": "formData"時,需要將這類參數按照固定格式拼接為一個字串,拼接格式為:key1=value1&key2=value2&key3=value3。同時需要在RequestHeader中添加content-type,content-type的值為application/x-www-form-urlencoded

      • 當請求參數資訊包含"in": "body"時,需要在RequestHeader中添加content-type,content-type的值與請求內容類型有關。例如:

        • 請求內容類型為JSON資料時,content-type的值為application/json

        • 請求內容類型為二進位檔案流時,content-type的值為application/octet-stream

    • 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:拼接規範請求串
            // 請求參數,當請求的查詢字串為空白時,使用Null 字元串作為正常化查詢字串
            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,您可能需要根據具體情況對代碼進行相應的調整。

需要您手動安裝pytz和requests,請根據您所使用的Python版本在終端(Terminal)執行以下命令。

Python3

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

import pytz
import requests
from datetime import datetime


class SignatureRequest:
    def __init__(self, http_method, canonical_uri, host, x_acs_action, x_acs_version):
        self.http_method = http_method
        self.canonical_uri = canonical_uri
        self.host = host
        self.x_acs_action = x_acs_action
        self.x_acs_version = x_acs_version
        self.headers = self._init_headers()
        self.query_param = OrderedDict()
        self.body = None

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

    def sorted_query_params(self):
        # 對查詢參數按名稱排序並返回編碼後的字串
        sorted_query_params = sorted(self.query_param.items(), key=lambda item: item[0])
        self.query_param = {k: v for k, v in sorted_query_params}

    def sorted_headers(self):
        # 對要求標頭按名稱排序並返回編碼後的字串
        sorted_headers = sorted(self.headers.items(), key=lambda item: item[0])
        self.headers = {k: v for k, v in sorted_headers}


def get_authorization(request):
    try:
        new_query_param = OrderedDict()
        process_object(new_query_param, '', request.query_param)
        request.query_param = new_query_param
        # 步驟 1:拼接規範請求串
        canonical_query_string = '&'.join(
            f'{percent_code(quote_plus(k))}={percent_code(quote_plus(str(v)))}' for k, v in
            request.query_param.items())
        hashed_request_payload = sha256_hex(request.body or ''.encode('utf-8'))
        request.headers['x-acs-content-sha256'] = hashed_request_payload
        request.sorted_headers()

        # 構建正常化要求標頭和已簽名訊息頭列表
        filtered_headers = OrderedDict()
        for k, v in request.headers.items():
            if k.lower().startswith('x-acs-') or k.lower() in ['host', 'content-type']:
                filtered_headers[k.lower()] = v

        canonical_headers = '\n'.join(f'{k}:{v}' for k, v in filtered_headers.items()) + '\n'
        signed_headers = ';'.join(k for k in filtered_headers.keys())

        canonical_request = (
            f'{request.http_method}\n{request.canonical_uri}\n{canonical_query_string}\n'
            f'{canonical_headers}\n{signed_headers}\n{hashed_request_payload}')
        print(canonical_request)

        # 步驟 2:拼接待簽名字串
        hashed_canonical_request = sha256_hex(canonical_request.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()
        print(signature)

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


def form_data_to_string(form_data):
    tile_map = OrderedDict()
    process_object(tile_map, '', form_data)
    return urlencode(tile_map)


def process_object(result_map, key, value):
    # 如果值為空白,則無需進一步處理
    if value is None:
        return

    if key is None:
        key = ""

    # 當值為清單類型時,遍曆列表中的每個元素,並遞迴處理
    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:
        # 對於以"."開頭的鍵,移除開頭的"."以保持鍵的連續性
        if key.startswith("."):
            key = key[1:]

        # 對於位元組類型的值,將其轉換為UTF-8編碼的字串
        if isinstance(value, bytes):
            result_map[key] = value.decode('utf-8')
        else:
            # 對於其他類型的值,直接轉換為字串
            result_map[key] = str(value)


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


def sha256_hex(s):
    return hashlib.sha256(s).hexdigest()


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

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


def percent_code(encoded_str):
    return encoded_str.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')


# 環境變數中擷取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方法中的樣本參數。
ROA介面和RPC介面只有canonicalUri取值邏輯是完全不同,其餘內容都是相似的。

通過API中繼資料擷取要求方法(methods)、請求參數名稱(name)、請求參數類型(type)、請求參數位置(in),並將參數封裝到SignatureRequest中。
1. 請求參數在中繼資料中顯示"in":"query",通過queryParam傳參。
2. 請求參數在中繼資料中顯示"in": "body",通過body傳參。
3. 請求參數在中繼資料中顯示"in": "formData",通過body傳參。
"""
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介面GET請求
    # 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)

    signature_request.sorted_query_params()
    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);
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);
        $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);
            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; // 載入base64 crate

/// 擷取目前時間戳(秒)
pub fn current_timestamp() -> Result<u64, SystemTimeError> {
    Ok(SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs())
}

// 對指定的字串進行URL編碼。傳回值類型為&Str,URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
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
}

/// 計算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"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let mut rng = rand::thread_rng();
    (0..length)
        .map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char)
        .collect()
}

/// 產生隨機字串作為nonce
pub fn generate_nonce() -> String {
    generate_random_string(32)
}

// 定義 FormData 資料類型
#[derive(Debug, Clone)]
pub enum FormValue {
    String(String),
    // 添加類型:Vec<String>, HashSet<String> 或者 HashMap<String, String> 等
    Vec(Vec<String>),
    HashMap(HashMap<String, String>),
}

// 定義一個body請求體枚舉,用於統一處理請求體類型,包含Json,Map,二進位類型 
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)], // 添加$query_params 查詢參數
    action: &str,
    version: &str,
    body: RequestBody,   // 定義接收請求體body參數 (類型為:Json,map,二進位)
    access_key_id: &str,
    access_key_secret: &str,
) -> Result<String, String> {

    // CanonicalQueryString 構建正常化查詢字串
    let canonical_query_string = build_sored_encoded_query_string(query_params);

    // 處理 body 請求體內容,判斷 boby 類型        
    let body_content = match &body { // 使用引用來避免移動
        RequestBody::Json(body_map) => json!(body_map).to_string(),  // 若 body 為map,轉化為 &str 類型,儲存 body_content 變數中
        RequestBody::Binary(_) => String::new(), // 若 body 為二進位類型這裡可以保留Null 字元串,body_content 變數為空白
        RequestBody::FormData(form_data) => {
            let params: Vec<String> = form_data
            .iter()
            .flat_map(|(k, v)| {
                match v {
                    FormValue::String(s) => {
                        // 當 FormValue 為 String 時
                        vec![format!("{}={}", percent_code(k), percent_code(&s))]
                    },
                    FormValue::Vec(vec) => {
                        // 當 FormValue 為 Vec 時
                        vec.iter()
                            .map(|s| format!("{}={}", percent_code(k), percent_code(s)))
                            .collect::<Vec<_>>()
                    },
                    FormValue::HashMap(map) => {
                        // 當 FormValue 為 HashMap 時
                        map.iter()
                            .map(|(sk, sv)| format!("{}={}", percent_code(sk), percent_code(sv)))
                            .collect::<Vec<_>>()
                    },
                }
            })
            .collect();
            params.join("&") //  組成 key=value&key=value 的形式
        },
        RequestBody::None => String::new(),
    };
    // 列印 body 和 query
    println!("Request Body: {}", body_content);
    println!("Query Params: {:?}", query_params);

    let hashed_request_payload = 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();
    
    // 構造要求標頭
    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());

    // 簽名的訊息頭
    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(";");
    // 正常化要求標頭
    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
    );
    
    // 計算待簽名字串
    let result = sha256_hex(&canonical_request);
    let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", result);
    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
    );
    
    headers.insert("Authorization", HeaderValue::from_str(&auth_data).unwrap());
    
    // 發送請求
    let url = format!("https://{}{}", host, canonical_uri);
    
    let response = send_request(
        &client,
        method,
        &url,
        headers,
        query_params,                // 接收 query 參數
        &body,                       // 接收body請求體參數(包含Json,Map,二進位) 
        &body_content,               // 接收body請求體參數。當body為map 或者 formDate 的時候,該變數有值;若body參數為二進位,則該變數為空白   
    ) 
    .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請求體參數(包含Json,Map,二進位)
    body_content: &str,                // 接收body請求體參數。當body為 map 或者 formDate 的時候,該變數有值;若body參數為二進位,則該變數為空白   
) -> Result<Response, String> {
    
    let mut request_builder = client.request(method, url);

     // 添加 query 參數  
     if !query_params.is_empty() {
        // 這裡使用 query_params 直接作為 query 的參數
        request_builder = request_builder.query(query_params);
    }

    // 添加要求標頭
    for (k, v) in headers.iter() {
        request_builder = request_builder.header(k, v.clone());
    }
    
     // 根據 RequestBody 類型佈建要求體
     match body {
        // 如果body是二進位,設定 application/octet-stream 
        RequestBody::Binary(binary_data) => {
            request_builder = request_builder.header("Content-Type", "application/octet-stream");
            request_builder = request_builder.body(binary_data.clone()); // 移動這裡的值
        }
        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)
}

/// 讀取響應
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))
}

/// 構建正常化查詢字串
pub fn build_sored_encoded_query_string(query_params: &[(&str, &str)]) -> String {
    // 按參數名升序排序並使用 BTreeMap 處理重複
    let sorted_query_params: BTreeMap<_, _> = query_params.iter().copied().collect();
    
    // URI 編碼
    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("&")
}
 /**
     * 簽名樣本,您需要根據實際情況替換main方法中的樣本參數。
     * ROA介面和RPC介面只有canonicalUri取值邏輯是完全不同,其餘內容都是相似的。
     * 通過API中繼資料擷取要求方法(methods)、請求參數名稱(name)、請求參數類型(type)、請求參數位置(in),並將參數封裝到SignatureRequest中。
     * 1. 請求參數在中繼資料中顯示"in":"query",通過queryParam傳參。
     * 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.");

    // RPC介面請求樣本一:請求參數"in":"query"   POST
    let host = "ecs.cn-hangzhou.aliyuncs.com"; // endpoint
    let canonical_uri = "/"; // RPC介面無資源路徑,故使用正斜杠(/)作為CanonicalURI
    let action = "DescribeInstanceStatus"; // API名稱
    let version = "2014-05-26"; // API版本號碼
    // RegionId在中繼資料中顯示的類型是String,"in":"query",必填
    let query_params = &[("RegionId", "cn-hangzhou")];
    // 構建查詢參數  InstanceId的在中繼資料中顯示的類型是array,"in":"query",非必填
    // let region_id = "cn-hangzhou";
    // let instance_ids = vec![
    //     "i-bp11ht4h2kd1XXXXXXXX",
    //     "i-bp16maz3h3xgXXXXXXXX",
    //     "i-bp10r67hmsllXXXXXXXX",
    // ];
    // // // 將 instance_ids 轉換為逗號分隔的字串
    // let instance_id_str = instance_ids.join(",");
    // // 建立查詢參數時,開始的時候添加 RegionId  
    // let mut query: Vec<(&str, &str)> = vec![("RegionId", region_id), ("InstanceId", &instance_id_str)];
    // let query_params = &query[..];


    // RPC介面請求樣本二:請求參數"in":"body"  POST
    // let host = "ocr-api.cn-hangzhou.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "RecognizeGeneral";
    // let version = "2021-07-07";
    // // // 上傳檔案
    // let binary_data = std::fs::read("C:\\Users\\issuser\\Desktop\\img\\001.png").expect("讀檔案失敗"); // 參數必須要直接傳檔案二進位
   
    // RPC介面請求樣本三:請求參數"in": "formData"  POST
    // let host = "mt.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "TranslateGeneral";
    // let version = "2018-10-12";
    // // Context在中繼資料中顯示的類型是String,"in":"query",非必填
    // let query_params = &[("Context", "早上")];
    // // FormatType、SourceLanguage、TargetLanguage等參數,在中繼資料中顯示"in":"formData"
    // let mut body = HashMap::new();  //  HashMap<String, FormValue>   FormValue  可支援Vec<String>, HashSet<String> 或者 HashMap<String, String> ...,更多類型可在FormValue枚舉中添加
    // body.insert(String::from("FormatType"),FormValue::String(String::from("text")));
    // body.insert(String::from("SourceLanguage"),FormValue::String(String::from("zh")));
    // body.insert(String::from("TargetLanguage"),FormValue::String(String::from("en")));
    // body.insert(String::from("SourceText"),FormValue::String(String::from("你好")));
    // body.insert(String::from("Scene"),FormValue::String(String::from("general")));
    
    // ROA介面POST請求  API:CreateCluster建立叢集  
    // 定義API請求常量
    // let host = "cs.cn-beijing.aliyuncs.com";
    // let canonical_uri = "/clusters";
    // let action = "CreateCluster";
    // let version = "2015-12-15";
    // // 佈建要求體參數
    // let mut body = HashMap::new();  //  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.insert(String::from("name"),json!("測試叢集"));
    // body.insert(String::from("region_id"),json!("cn-beijing"));
    // body.insert(String::from("cluster_type"),json!("ExternalKubernetes"));
    // body.insert(String::from("vpcid"),json!("vpc-2zeou1uod4ylaXXXXXXXX"));
    // body.insert(String::from("container_cidr"),json!("10.0.0.0/8"));
    // body.insert(String::from("service_cidr"),json!("10.2.0.0/24"));
    // body.insert(String::from("security_group_id"),json!("sg-2ze1a0rlgeo7XXXXXXXX"));
    // body.insert(String::from("vswitch_ids"),json!(vec!["vsw-2zei30dhfldu8XXXXXXXX"]));

    // ROA介面GET請求   API:DeleteCluster  查詢指定叢集的關聯資源
    // let host = "cs.cn-beijing.aliyuncs.com"; // endpoint
    // // 拼接資源路徑
    // let uri = format!("/clusters/{}/resources", percent_code("c1311ba68f3af45f39ee3f4d2XXXXXXXX").as_ref()); 
    // let canonical_uri = uri.as_str(); // 資源路徑  轉化為&Str類型
    // let action = "DescribeClusterResources";   // API名稱
    // let version = "2015-12-15"; // API版本號碼
    // // 設定查詢參數
    // let query_params = &[("with_addon_resources", if true { "true" } else { "false" })];  // "true" or "false"

    // ROA介面DELETE請求   API:DeleteCluster  DELETE請求刪除一個隨用隨付的叢集
    // let host = "cs.cn-beijing.aliyuncs.com";
    // let uri = format!("/clusters/{}", percent_code("c72b778e79d3647cdb95c8b86XXXXXXXX").as_ref()); 
    // let canonical_uri = uri.as_str(); // 資源路徑轉化為&Str類型
    // let action = "DeleteCluster";
    // let version = "2015-12-15";

    // 調用介面
    match call_api(
        client.clone(),
        Method::POST,                                               // 要求方法:GET,DELETE,PUT,POST
        host,                                                       // endpoint
        canonical_uri,                                              // 資源路徑
        // &[],                                                        // 當查詢參數為空白時,query_params 設定為  &[]
        query_params,                                            // 當查詢參數不為空白時, query_params 設定為 &[("K", "V")]
        action,                                                 
        version,
        RequestBody:: None,                                      // 當body參數類型為空白時,使用 RequestBody:: None 設定為 None;
        // RequestBody::Json(body),                                 // 當body參數類型為Map時,使用 RequestBody::Json 傳遞 Map
        // RequestBody::Binary(binary_data),                           // 當body參數類型為二進位時,使用 RequestBody::Binary 傳遞位元據
        // RequestBody::FormData(body),                                // 當body參數類型為 formDate 時,使用 RequestBody::FormData 傳遞 FormData
        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


// HMAC-SHA256計算
void hmac256(const char *key, const char *message, char *output) {
    unsigned char hmac[SHA256_DIGEST_LENGTH];
    unsigned int result_len;

    // 計算 HMAC
    HMAC(EVP_sha256(), key, strlen(key), (unsigned char *)message, strlen(message), hmac, &result_len);

    // 將 HMAC 轉換為十六進位字串輸出
    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';
}

// URL編碼
char* percentEncode(const char* str) {
    if (str == NULL) {
        fprintf(stderr, "輸入字串不可為null\n");
        return NULL;
    }

    size_t len = strlen(str);
    // 最壞情況下,每個字元都需要被編碼為3個字元(%XX),所以分配的空間為3倍
    char* encoded = (char*)malloc(len * 3 + 1);
    if (encoded == NULL) {
        fprintf(stderr, "記憶體配置失敗\n");
        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 {
            // 否則,進行URL編碼
            ptr += sprintf(ptr, "%%%02X", c);
        }
    }
    *ptr = '\0'; // 以null字元結尾

    // 處理加號的替換
    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'; // 以null字元結尾
    }

    free(encoded); // 釋放臨時編碼的空間
    return finalEncoded;
}

// nonce隨機數
void generate_uuid(char *uuid, size_t size) {
    if (size < 37) { // UUID格式需要36個字元+結束符
        fprintf(stderr, "Buffer size too small for UUID\n");
        return;
    }
    // 使用隨機數產生器產生16個隨機位元組
    unsigned char random_bytes[16];
    RAND_bytes(random_bytes, sizeof(random_bytes));
    // 有效版本是4,用於隨機產生的UUID
    random_bytes[6] &= 0x0f; // 保留高4位
    random_bytes[6] |= 0x40; // 將版本設定為4
    random_bytes[8] &= 0x3f; // 保留高2位
    random_bytes[8] |= 0x80; // 將變體設定為10xx

    // 格式化為UUID字串
    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
void 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) {

    // 產生UUID
    generate_uuid(uuid, 37);

    // 產生x-acs-date
    time_t now = time(NULL);
    struct tm *utc_time = gmtime(&now);
    strftime(x_acs_date, 64, "%Y-%m-%dT%H:%M:%SZ", utc_time); 
    // 列印時間戳記
    printf("Generated x-acs-date: %s\n", x_acs_date);


    // 計算請求體的 SHA-256 雜湊 (即 x-acs-content-sha256)
    sha256_hex(body ? body : "", hashed_payload);
    // 列印雜湊值
    printf("Generated x-acs-content-sha256: %s\n", hashed_payload);


    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);

    // 簽名欄位
    char signed_headers[] = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";


    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, query_params ? strdup(query_params) : "", // 使用percentCode
             canonical_headers, signed_headers, hashed_payload);
    // 列印正常化請求
    printf("Canonical Request:\n%s\n", canonical_request);

    // 計算正常化請求的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);

    // 構建待簽名字串
    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);

    // 計算簽名
    char signature[SHA256_DIGEST_LENGTH * 2 + 1];
    hmac256(ACCESS_KEY_SECRET, string_to_sign, signature);
    // 列印簽名
    printf("Signature: %s\n", signature);


    // 構建最終的 Authorization 頭,包含 SignedHeaders
    snprintf(authorization_header, BUFFER_SIZE,
             "%s Credential=%s,SignedHeaders=%s,Signature=%s",
             ALGORITHM, ACCESS_KEY_ID, signed_headers, signature);
    // 列印 Authorization
    printf("Authorization: %s\n", authorization_header);
}

// 發送請求
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];

    // 擷取授權頭
    get_authorization(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, authorization_header, hashed_payload, x_acs_date, uuid);

    // 拼接請求路徑 URL
    char url[BUFFER_SIZE];
    if (query_params && strlen(query_params) > 0) {
        snprintf(url, sizeof(url), "https://%s%s?%s", host, canonical_uri, query_params);
    } else {
        snprintf(url, sizeof(url), "https://%s%s", host, canonical_uri);
    }
    // 列印請求url
    printf("Request URL: %s\n", url);

    // 初始化cURL
    CURL *curl = curl_easy_init();
    if(!curl) {
        fprintf(stderr, "curl_easy_init() failed\n");
        return;
    }

    // 定義數組用於添加要求標頭
    struct curl_slist *headers = NULL;
    // 建立字元數組以儲存要求標頭並添加 headers
    char header_value[BUFFER_SIZE];
    // 添加headers資訊
    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選項
    // 設定cURL要求方法
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); // 設定 HTTP 方法
    // 設定 url
    curl_easy_setopt(curl, CURLOPT_URL, url);
    // 禁用 SSL 驗證,(調試)
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
    // 添加調試資訊
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    // 添加要求標頭
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    // 添加 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);
        }
    }

    // 列印添加的 headers
    struct curl_slist *header_ptr = headers;
    while (header_ptr) {
        printf("Header: %s\n", header_ptr->data);
        header_ptr = header_ptr->next;
    }

    // 執行請求並檢查響應
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        return;
    }

    // 清理
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
}

/**
*
     * 簽名樣本,您需要根據實際情況替換main方法中的樣本參數。
     * <p>
     * 通過API中繼資料擷取要求方法(methods)、請求參數名稱(name)、請求參數類型(type)、請求參數位置(in),並將參數封裝到SignatureRequest中。
     * 1. 請求參數在中繼資料中顯示"in":"query",通過queryParam傳參。
     * 2. 請求參數在中繼資料中顯示"in": "body",通過body傳參。
     * 2. 請求參數在中繼資料中顯示"in": "formData",通過body傳參。
*/
int main() {
    // 設定響應格式為UTF-8
    SetConsoleOutputCP(CP_UTF8);
    srand((unsigned int)time(NULL));

    /**
      * 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-bp11ht4h2kd1ic5fXXXX",
        "i-bp16maz3h3xg83raXXXX"
    };
    // 拼接InstanceId數組
    char InstanceId[BUFFER_SIZE];
    snprintf(InstanceId, sizeof(InstanceId),
             "InstanceId.1=%s&InstanceId.2=%s",
             percentEncode(instance_ids[0]),
             percentEncode(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);
    // 列印參數
    printf("Query Params: %s\n", query_params);
    // body 為空白
    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("C:\\Users\\issuser\\Desktop\\img\\001.png", &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";
//
//        // 調用 API
//        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";
//    // formdate類型,編碼查詢參數的Value
//    // 定義查詢參數 Context="早上"
//    char query_params[BUFFER_SIZE];
//    snprintf(query_params, sizeof(query_params), "Context=%s", percentEncode("早上"));
//
//    // 定義formdate型別參數的value值
//    const char *format_type = "text";
//    const char *source_language = "zh";
//    const char *target_language = "en";
//    const char *source_text = "你好";
//    const char *scene = "general";
//    // body 為 formdate類型 構建表單資料字串,進行編碼
//    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));

      /**
        * ROA介面POST請求
      */
//    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 = "";
//    // body 為json類型
//    // 構建JSON格式的請求體
//    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-2zeou1uod4ylaf35tXXXX", "10.0.0.0/8",
//             "10.2.0.0/24", "sg-2ze1a0rlgeo7dj37XXXX",
//             "vsw-2zei30dhfldu8ytmtXXXX");
//    // 列印請求體
//    printf("Request Body: %s\n", 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));

      /**
        * ROA介面GET請求
      */
//    const char *http_method = "GET";
//    // 拼接url  拼接請求參數  canonical_uri:/clusters/cluster_id/resources
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s/resources", percentEncode("cd1f5ba0dbfa144c69e48b75df47bXXXX"));
//    // 列印資源路徑
//    printf("canonical_uri: %s\n", canonical_uri);
//
//    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";
//    // 定義body
//    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";
//    // 拼接url  拼接請求參數  canonical_uri:/clusters/cluster_id
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s", percentEncode("cd1f5ba0dbfa144c69e48b75df47bXXXX"));
//    // 列印資源路徑
//    printf("canonical_uri: %s\n", canonical_uri);

//    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 = "";
//    // 定義body
//    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

解決方案:

說明

建議您先根據固定參數樣本檢查本地代碼計算結果是否正確。

  1. 從報錯資訊中擷取CanonicalRequest和StringToSign的值。

  2. 首先,請對比報錯資訊中的CanonicalRequest與您計算的CanonicalRequest內容是否一致。若存在不一致,請查看步驟一:構造正常化請求,並檢查您的代碼中與文檔介紹內容不一致的部分。常見的錯誤之一是參數傳遞位置不當,例如參數應當拼接在URL中,但卻錯誤地通過請求體(body)進行傳遞,這將導致參數CanonicalQueryStringx-acs-content-sha256的結果均出現錯誤。

  3. 當報錯資訊中的CanonicalRequest與您計算的CanonicalRequest內容一致時,請對比StringToSign與您計算的StringToSign的值是否一致。需要注意的是,StringToSign中HashedCanonicalRequest的值應使用與x-acs-content-sha256相同的函數產生。

  4. 若仍然無法解決,請聯絡我們

請求參數該如何傳

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的值不是RPC或ROA

RPC或ROA僅影響CanonicalURI的值。當style的值是其他類型時,可在API中繼資料中檢查將調用的API概要資訊中是否包含path參數,若存在,則CanonicalURI的值為該path的值,若不存在,則CanonicalURI的值為正斜杠(/)。例如在查詢ACK叢集列表的API中繼資料中path的值為/api/v1/clusters,那麼CanonicalURI的值為/api/v1/clustersimage

當參數類型是集合時,該如何傳參

當參數類型是複雜資料結構時,需要將複雜資料結構平鋪為一個映射結構(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"
}

自簽名時,如果使用GET調試成功了,可以使用POST嗎?

  • 對於RPC介面,通常既支援GET請求也支援POST請求。

  • 對於ROA介面,則僅支援單一的請求方式。

如何擷取API支援的請求方式,請參見OpenAPI中繼資料

調用API時提示“You are not authorized to do this operation. ”

問題原因:您所使用的AccessKey對應的RAM使用者沒有許可權調用該API。

解決方案:請為該RAM使用者授予相應雲產品的管理或唯讀許可權。關於如何為RAM使用者進行授權,請參見為RAM使用者授權

如何擷取AccessKey

AccessKey是阿里雲為使用者提供的永久訪問憑據,由AccessKey ID和AccessKey Secret組成的一對密鑰。在通過調用API訪問阿里雲資源時,系統將對請求中攜帶的AccessKey ID以及通過AccessKey Secret加密產生的簽名進行身分識別驗證和請求合法性校正。如何擷取AccessKey,請參見建立RAM使用者的AccessKey

聯絡我們

當您在計算簽名時遇到無法解決的問題,可以加入DingTalk群:78410016550,聯絡值班同學進行諮詢。