全部產品
Search
文件中心

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

更新時間:Jul 01, 2024

V3版本通過公用要求標頭設定介面必要的參數資訊,在簽名機制的實現上屏蔽了介面風格的差異,更標準、更簡單。本文提供了詳細的指南,用於協助您瞭解和實施阿里雲SDK V3版的請求結構和簽名過程。您會瞭解如何構造標準的HTTP請求,以及如何使用正確的簽名演算法來驗證請求的身份,確保傳輸的資料的完整性和安全性。如果您想自研阿里雲OpenAPI的請求籤名,您可以參考本文。

HTTP 要求結構

一個完整的阿里雲 OpenAPI 請求,包含以下部分。

名稱

是否必選

描述

樣本值

協議

您可以查閱不同雲產品的 API 參考文檔進行配置。支援通過HTTPHTTPS協議進行請求通訊。為了獲得更高的安全性,推薦您使用HTTPS協議發送請求。取值範圍為https://或者 http://

https://

服務地址

即 Endpoint。您可以查閱不同雲產品的服務接入地址文檔,查閱不同服務地區下的服務地址。

cs.aliyuncs.com

resource_URI_parameters(介面URL)

介面URL,包括介面路徑和位置在 path、 query的介面請求參數。

/clusters/{cluster_id}/triggers

RequestHeader(要求標頭資訊)

要求標頭資訊,通常包含API的版本、Host、Authorization等資訊。後文將詳細說明。

x-acs-action

RequestBody

定義在 body 中的業務請求參數,建議您在阿里雲 OpenAPI 開發人員門戶進行試用。

cluster_id

HTTPMethod

請求使用的方法,ROA介面要求方法包括PUT、POST、GET、DELETE。

POST

RequestHeader(公用要求標頭)

一個完整的阿里雲 OpenAPI 請求,包含以下部分。

名稱

類型

是否必選

描述

樣本值

x-acs-action

String

API的名稱。您可以訪問阿里雲 OpenAPI 開發人員門戶,搜尋您想調用的 OpenAPI

RunInstances

x-acs-version

String

API 版本。您可以訪問阿里雲 OpenAPI 開發人員門戶,查看您調用 OpenAPI 對應的 API 版本

2014-05-26

Authorization

String

非匿名請求必須

用於驗證請求合法性的認證資訊,格式為Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature。其中SignatureAlgorithm為簽名加密方式,為ACS3-HMAC-SHA256。

Credential 為使用者的存取金鑰ID。您可以在RAM 控制台查看您的 AccessKeyId。如需建立 AccessKey,請參見建立AccessKeySignedHeaders為要求標頭中包含的參與簽名欄位鍵名,【說明】:除Authorization外的所有公用要求標頭,只要存在必須被加入簽名。

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

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

x-acs-signature-nonce

String

簽名唯一隨機數。用於防止網路重放攻擊,建議您每一次請求都使用不同的隨機數。

d410180a5abf7fe235dd9b74aca91fc0

x-acs-date

String

按照ISO 8601標準表示的UTC時間,格式為yyyy-MM-ddTHH:mm:ssZ,例如2018-01-01T12:00:00Z。值為請求發出前15分鐘內的時間。

2023-10-26T09:01:01Z

host

String

即服務地址,參見HTTP 要求結構

ecs.cn-shanghai.aliyuncs.com

x-acs-content-sha256

String

請求本文Hash摘要後再base-16編碼的結果,與HashedRequestPayload一致。

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-security-token

String

STS認證必傳

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

介面請求構造

一個完整的介面請求構造如下:

HTTPMethod /resource_URI_parameters
RequestHeader
RequestBody

請求參數由公用要求標頭和API自訂參數組成。公用要求標頭中包含API版本號碼、身分識別驗證等資訊。

  • HTTPMethod :請求使用的方法,包括PUT、POST、GET、DELETE。詳情請參看HTTP 要求結構

  • resource_URI_parameters:請求要調用的資源標識符,如/cluster。詳情請參看HTTP 要求結構

  • RequestHeader:要求標頭資訊,通常包含API的版本、Host、Authorization等資訊。詳情請參看HTTP 要求結構

  • RequestBody:在 body 中的業務請求參數。詳情請參看HTTP 要求結構

請求RunInstances介面樣本:

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

請求編碼

請求及返回結果都使用UTF-8字元集進行編碼。

擷取API資訊

訪問API文檔,選擇雲產品。

  1. 單擊雲端服務器名稱下面的擷取中繼資料,在中繼資料中info.style查看雲產品支援的OpenAPI風格(RPC或者ROA)。

    image

    image

    說明

    該位置擷取的中繼資料中包含了雲產品的所有API資訊,如果您想要查看單個API的中繼資料,請查看步驟2。

  2. 選擇將要調用的API,單擊右上方擷取中繼資料

    image

    在中繼資料中,定義了API支援的網路通訊協定、請求方式、參數及參數位置等資訊。如下圖所示的RunInstances中繼資料中:

    • 支援的網路通訊協定有HTTP和HTTPS,建議使用HTTPS。

    • 支援的請求方式有GET和POST,兩者請求方式調用結果無任何差異,但GET請求只支援 32 KB 以內的請求包,所以推薦使用POST請求。

    • 支援的參數有RegionId、ImageId等,參數位置在query,表示參數是要拼接在請求URL後面,例如https://ecs.cn-beijing.aliyuncs.com/?ImageId=aliyun_2_1903_x64_20G_alibase_20231221.vhd&InstanceChargeType=PostPaid&InstanceType=ecs.e-c1m1.large&InternetChargeType=PayByTraffic&MinAmount=1&Password=test%401234&RegionId=cn-beijing&SecurityGroupId=sg-2zec0dm6qi66XXXXXXXX&SystemDisk.Category=cloud_essd&SystemDisk.Size=40&VSwitchId=vsw-2ze3aagwn397gXXXXXXXX。

    image

    說明

    中繼資料中其他支援的內容對簽名無影響,這裡暫不詳細說明。更多中繼資料的資訊,請參見中繼資料使用指南

簽名機制

為保證API的安全調用,在調用API時阿里雲會對每個API請求通過簽名(Signature)進行身分識別驗證。無論使用HTTP還是HTTPS協議提交請求,都需要在請求中包含簽名資訊。本文指導您如何進行簽名處理。

對於每一次HTTP或者HTTPS協議請求,阿里雲會根據訪問中的簽名資訊驗證訪問要求者身份。您在訪問時簽名資訊時,請按照以下方法對請求進行簽名處理:

步驟一:構造正常化請求

使用AK/SK方式進行簽名與認證,首先需要規範請求內容,然後再進行簽名。用戶端與雲端服務API Gateway使用相同的請求規範,可以確保同一個HTTP請求的前後端得到相同的簽名結果,從而完成身份校正。

構造正常化請求(CanonicalRequest)的虛擬碼如下:

CanonicalRequest =
  HTTPRequestMethod + '\n' +    //http要求方法,全大寫
  CanonicalURI + '\n' +         //正常化URI
  CanonicalQueryString + '\n' + //正常化查詢字串
  CanonicalHeaders + '\n' +     //正常化訊息頭
  SignedHeaders + '\n' +        //已簽名訊息頭
  HashedRequestPayload			
  • 要求方法(HTTPRequestMethod)

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

  • 正常化URI(CanonicalURI)

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

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

    • 其他字元編碼成%加字元對應ASCII碼的16進位。樣本:半形雙引號(")對應%22

    • 空格( )編碼成%20,而不是加號(+)、星號(*)替換為%2A%7E替換為波浪號(~)。

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

    重要

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

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

  • 正常化查詢字串(CanonicalQueryString)

    構造方法如下:

    1. 將查詢字串中的參數按照參數名的字元代碼升序排列,具有重複名稱的參數應按值進行排序。

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

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

    4. 按照步驟4中的順序使用與號(&)串連編碼後的請求參數。

    重要

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

  • 正常化要求標頭(CanonicalizedHeaders)

    一個非標準HTTP頭部資訊。需要將請求中包含以x-acs-為首碼、hostcontent-type的參數資訊,添加到正常化要求標頭中,構造方法如下:

    1. 將所有需要簽名的參數的名稱轉換為小寫。

    2. 將所有參數按照參數名稱的字元順序以升序排列。

    3. 將參數的值除去首尾空格。對於有多個值的參數,將多個值分別除去首尾空格後按值升序排列,然後用逗號(,)串連。

    4. 將步驟2、3的結果以英文冒號(:)串連,並在尾部添加分行符號,組成一個正常化訊息頭(CanonicalHeaderEntry)。

    5. 如果沒有需要簽名的訊息頭,使用Null 字元串作為正常化訊息頭列表。

    重要

    除Authorization外的所有公用要求標頭,只要符合要求的參數都必須被加入簽名。

  • 已簽名訊息頭列表(SignedHeaders

    用於說明此次請求包含了哪些訊息頭參與簽名,與CanonicalHeaders中包含的訊息頭是一一對應的,構造方法如下:

    • 將CanonicalHeaders中包含的要求標頭的名稱轉為小寫。

    • 多個要求標頭名稱(小寫)按首字母升序排列並以英文分號(;)分隔,例如 content-type;host;x-acs-date

    • 虛擬碼如下:

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

    當請求體(body)為空白時,RequestPayload固定為空白字串,否則RequestPayload的值為請求體(body)對應的JSON字串。再使用雜湊函數對RequestPayload進行轉換得到HashedRequestPayload,轉換規則用虛擬碼可表示為 HashedRequestPayload = HexEncode(Hash(RequestPayload))

    • Hash表示訊息摘要函數,目前支援SHA256演算法,例如,當簽名協議使用ACS3-HMAC-SHA256時,應使用SHA256作為Hash函數。

    • HexEncode表示以小寫十六進位的形式返回摘要的編碼函數(即Base16編碼)。

    表1:簽名協議與簽名演算法、摘要函數的對應關係

    簽名協議(SignatureAlgorithm)

    處理RequestPayload以及CanonicalRequest時使用的摘要函數(Hash)

    計算簽名時實際使用的簽名演算法

    (SignatureMethod)

    ACS3-HMAC-SHA256

    SHA256

    HMAC-SHA256

步驟二:構造待簽名字串

按照以下虛擬碼構造待簽名字串(stringToSign):

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

    簽名協議,目前支援ACS3-HMAC-SHA256,不再支援基於MD5或SHA1的演算法。

  • HashedCanonicalRequest

    正常化請求摘要串,計算方法虛擬碼如下:

    HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
  1. 使用雜湊函數(Hash)對步驟一中得到的正常化請求(CanonicalRequest)進行摘要處理,具體使用的Hash函數取決於簽名協議(SignatureAlgorithm),參見表1,例如,當簽名協議為ACS3-HMAC-SHA256時,應使用SHA256作為Hash函數。

  2. 將上一步得到的摘要結果以小寫十六進位形式編碼。

步驟三:計算簽名

按照以下虛擬碼計算簽名值(Signature)。

Signature = HexEncode(SignatureMethod(Secret, StringToSign))
  • StringToSign:步驟二中構造的待簽名字串,UTF-8編碼。

  • SignatureMethod:簽名演算法,具體使用的演算法取決於簽名協議(SignatureAlgorithm),其對應關係如表1。

  • Secret:使用者的簽名密鑰,為位元據。

  • HexEncode:以小寫十六進位的形式返回摘要的編碼函數(即Base16編碼)。

步驟四:將簽名添加到請求中

計算完簽名後,構造Authorization要求標頭,格式為:Authorization:<SignatureAlgorithm> Credential=<AccessKeyId>,SignedHeaders=<SignedHeaders>,Signature=<Signature>樣本如下:

Authorization:ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=content-type;host;x-acs-timestamp,Signature=6b595d672d79c15b18edb4ccfba6789a24a6f2b82c400e03162d9279b08555d7

介面簽名樣本

說明
  1. 為了讓您更清晰地理解上述簽名機制,下面以主流程式設計語言為例,將簽名機制完整實現。範例程式碼只是讓您更好地理解簽名機制,存在不通用性,阿里雲OpenAPI提供多種程式設計語言和開發架構的SDK,使用這些SDK可以免去簽名過程,便於您快速構建與阿里雲相關的應用程式,建議您使用SDK。

  2. 在簽名之前,一定要先查看API資訊,擷取API的請求方式、請求參數以及參數位置等資訊。

Java樣本

運行Java樣本,需要您在pom.xml中添加以下Maven依賴。

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>
<dependency>
     <groupId>com.google.code.gson</groupId>
     <artifactId>gson</artifactId>
     <version>2.9.0</version>
 </dependency>
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;

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

    /**
     * 日期格式化工具,用於將日期時間字串格式化為"yyyy-MM-dd'T'HH:mm:ss'Z'"的格式。
     */
    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

    private static class Request {
        // HTTP Method
        private final String httpMethod;
        // 請求路徑,當資源路徑為空白時,使用正斜杠(/)作為CanonicalURI
        private final String canonicalUri;
        // endpoint
        private final String host;
        // API name
        private final String xAcsAction;
        // API version
        private final String xAcsVersion;
        // headers
        TreeMap<String, Object> headers = new TreeMap<>();
        // 調用API所需要的參數,參數位置在body。Json字串
        String body;
        // 調用API所需要的參數,參數位置在query,參數按照參數名的字元代碼升序排列
        TreeMap<String, Object> queryParam = new TreeMap<>();

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

        // init headers
        private void initBuilder() {
            headers.put("host", host);
            headers.put("x-acs-action", xAcsAction);
            headers.put("x-acs-version", xAcsVersion);
            SDF.setTimeZone(new SimpleTimeZone(0, "GMT")); // 設定日期格式化時區為GMT
            headers.put("x-acs-date", SDF.format(new Date()));
            headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
        }
    }

    /**
     * 這裡通過環境變數擷取Access Key ID和Access Key Secret,
     */
    private final static String ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
    private final static String ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");


    /**
     * 簽名協議
     */
    private static final String ALGORITHM = "ACS3-HMAC-SHA256";

    /**
     * 主函數樣本,示範如何使用阿里雲的SDK進行API請求。
     * 通過給定的參數構建請求,並進行簽名認證。
     */
    public static void main(String[] args) {
        // RPC介面請求
        String httpMethod = "POST"; // 請求方式
        String canonicalUri = "/"; 
        String host = "ecs.cn-beijing.aliyuncs.com";  // endpoint
        String xAcsAction = "DescribeInstances";  // API名稱
        String xAcsVersion = "2014-05-26"; // API版本號碼
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

        // 調用API所需要的參數,參數按照參數名的字元代碼升序排列,具有重複名稱的參數應按值進行排序。
        request.queryParam.put("RegionId", "cn-beijing");
        request.queryParam.put("VpcId", "vpc-2zeo42r27y4opXXXXXXXX");


/*         // ROA介面POST請求
        String httpMethod = "POST";
        String canonicalUri = "/clusters"; 
        String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
        String xAcsAction= "CreateCluster"; // API名稱
        String xAcsVersion=  "2015-12-15"; // API版本號碼
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

        // 請求body,通過Gson將body轉成JSON字串
        TreeMap<String, Object> body = new TreeMap<>();
        body.put("name", "testDemo");
        body.put("region_id", "cn-beijing");
        body.put("cluster_type", "Kubernetes");
        body.put("vpcid", "vpc-2zeo42r27y4opXXXXXXXX");
        body.put("service_cidr", "172.16.1.0/20");
        body.put("security_group_id", "sg-2zec0dm6qi66XXXXXXXX");
        Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
        request.body = gson.toJson(body);
        request.headers.put("content-type", "application/json; charset=utf-8");*/

/*        // ROA介面GET請求
        String httpMethod = "GET";
        // canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
        String canonicalUri = "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources";
        String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
        String xAcsAction = "DescribeClusterResources"; // API名稱
        String xAcsVersion = "2015-12-15"; // API版本號碼
        Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

        request.queryParam.put("with_addon_resources", true);*/

        // 簽名過程
        getAuthorization(request);
        // 調用API
        callApi(request);
    }

    private static void callApi(Request request) {
        try {
            // 通過HttpClient發送請求
            String url = "https://" + request.host + request.canonicalUri;
            URIBuilder uriBuilder = new URIBuilder(url);
            // 添加請求參數
            for (Map.Entry<String, Object> entry : request.queryParam.entrySet()) {
                uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
            }
            HttpUriRequest httpRequest;
            switch (request.httpMethod) {
                case "GET":
                    httpRequest = new HttpGet(uriBuilder.build());
                    break;
                case "POST":
                    HttpPost httpPost = new HttpPost(uriBuilder.build());
                    if (request.body != null) {
                        StringEntity postEntity = new StringEntity(request.body);
                        httpPost.setEntity(postEntity);
                    }
                    httpRequest = httpPost;
                    break;
                case "DELETE":
                    httpRequest = new HttpDelete(uriBuilder.build());
                    break;
                case "PUT":
                    HttpPut httpPut = new HttpPut(uriBuilder.build());
                    if (request.body != null) {
                        StringEntity putEntity = new StringEntity(request.body);
                        httpPut.setEntity(putEntity);
                    }
                    httpRequest = httpPut;
                    break;
                default:
                    System.out.println("Unsupported HTTP method: " + request.body);
                    throw new IllegalArgumentException("Unsupported HTTP method");
            }

            // 添加http要求標頭
            for (Map.Entry<String, Object> entry : request.headers.entrySet()) {
                httpRequest.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
            }
            // 發送請求
            try (CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(httpRequest)) {
                String result = EntityUtils.toString(response.getEntity(), "UTF-8");
                System.out.println(result);
            } catch (IOException e) {
                // 異常處理
                System.out.println("Failed to send request");
                e.printStackTrace();
            }
        } catch (URISyntaxException e) {
            // 異常處理
            System.out.println("Invalid URI syntax");
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            // 異常處理
            System.out.println("UnsupportedEncodingException");
            e.printStackTrace();
        }
    }

    /**
     * 該方法用於根據傳入的HTTP要求方法、正常化的URI、查詢參數等,計算並產生授權資訊。
     */
    private static void getAuthorization(Request request) {
        try {
            // 步驟 1:拼接規範請求串
            // 請求參數,當請求的查詢字串為空白時,使用Null 字元串作為正常化查詢字串
            StringBuilder canonicalQueryString = new StringBuilder();
            request.queryParam.entrySet().stream().map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
                // 如果canonicalQueryString已經不是空的,則在查詢參數前添加"&"
                if (canonicalQueryString.length() > 0) {
                    canonicalQueryString.append("&");
                }
                canonicalQueryString.append(queryPart);
            });

            // 請求體,當請求本文為空白時,比如GET請求,RequestPayload固定為空白字串
            String requestPayload = "";
            if (request.body != null) {
                requestPayload = request.body;
            }

            // 計算請求體的雜湊值
            String hashedRequestPayload = sha256Hex(requestPayload);
            request.headers.put("x-acs-content-sha256", hashedRequestPayload);
            // 構造要求標頭,多個正常化訊息頭,按照訊息頭名稱(小寫)的字元代碼順序以升序排列後拼接在一起
            StringBuilder canonicalHeaders = new StringBuilder();
            // 已簽名訊息頭列表,多個要求標頭名稱(小寫)按首字母升序排列並以英文分號(;)分隔
            StringBuilder signedHeadersSb = new StringBuilder();
            request.headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") || entry.getKey().equalsIgnoreCase("host") || entry.getKey().equalsIgnoreCase("content-type")).sorted(Map.Entry.comparingByKey()).forEach(entry -> {
                String lowerKey = entry.getKey().toLowerCase();
                String value = String.valueOf(entry.getValue()).trim();
                canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
                signedHeadersSb.append(lowerKey).append(";");
            });
            String signedHeaders = signedHeadersSb.substring(0, signedHeadersSb.length() - 1);
            String canonicalRequest = request.httpMethod + "\n" + request.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
            System.out.println("canonicalRequest=========>\n" + canonicalRequest);

            // 步驟 2:拼接待簽名字串
            String hashedCanonicalRequest = sha256Hex(canonicalRequest); // 計算正常化請求的雜湊值
            String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
            System.out.println("stringToSign=========>\n" + stringToSign);

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

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

    /**
     * 使用HmacSHA256演算法產生訊息認證碼(MAC)。
     *
     * @param key 密鑰,用於產生MAC的密鑰,必須保密。
     * @param str 需要進行MAC認證的訊息。
     * @return 返回使用HmacSHA256演算法計算出的訊息認證碼。
     * @throws Exception 如果初始化MAC或計算MAC過程中遇到錯誤,則拋出異常。
     */
    public static byte[] hmac256(byte[] key, String str) throws Exception {
        // 執行個體化HmacSHA256訊息認證碼產生器
        Mac mac = Mac.getInstance("HmacSHA256");
        // 建立密鑰規範,用於初始化MAC產生器
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
        // 初始化MAC產生器
        mac.init(secretKeySpec);
        // 計算訊息認證碼並返回
        return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 使用SHA-256演算法計算字串的雜湊值並以十六進位字串形式返回。
     *
     * @param str 需要進行SHA-256雜湊計算的字串。
     * @return 計算結果為小寫十六進位字串。
     * @throws Exception 如果在擷取SHA-256訊息摘要執行個體時發生錯誤。
     */
    public static String sha256Hex(String str) throws Exception {
        // 擷取SHA-256訊息摘要執行個體
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        // 計算字串s的SHA-256雜湊值
        byte[] d = md.digest(str.getBytes(StandardCharsets.UTF_8));
        // 將雜湊值轉換為小寫十六進位字串並返回
        return DatatypeConverter.printHexBinary(d).toLowerCase();
    }

    /**
     * 對指定的字串進行URL編碼。
     * 使用UTF-8編碼字元集對字串進行編碼,並對特定的字元進行替換,以符合URL編碼規範。
     *
     * @param str 需要進行URL編碼的字串。
     * @return 編碼後的字串。其中,加號"+"被替換為"%20",星號"*"被替換為"%2A",波浪號"%7E"被替換為"~"。
     */
    public static String percentCode(String str) {
        if (str == null) {
            throw new IllegalArgumentException("輸入字串不可為null");
        }
        try {
            return URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8編碼不被支援", e);
        }
    }
}

Python樣本

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

Python3

pip3 install pytz
pip3 install requests

Python2

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

import pytz
import requests
from datetime import datetime


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

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

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

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


# 環境變數中擷取Access Key ID和Access Key Secret
ACCESS_KEY_ID = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID')
ACCESS_KEY_SECRET = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET')

ALGORITHM = 'ACS3-HMAC-SHA256'


def get_authorization(request):
    try:
        # Step 1: Construct Canonical Query String and Payload Hash
        canonical_query_string = '&'.join(
            f'{percent_code(quote_plus(k))}={percent_code(quote_plus(str(v)))}' for k, v in request.query_param.items())
        hashed_request_payload = sha256_hex(request.body or '')
        request.headers['x-acs-content-sha256'] = hashed_request_payload
        request.sorted_headers()

        # Construct Canonical Headers and Signed Headers
        canonical_headers = '\n'.join(f'{k.lower()}:{v}' for k, v in request.headers.items() if
                                      k.lower().startswith('x-acs-') or k.lower() in ['host', 'content-type'])
        signed_headers = ';'.join(sorted(request.headers.keys(), key=lambda x: x.lower()))

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

        # Step 2: Construct String to Sign
        hashed_canonical_request = sha256_hex(canonical_request)
        string_to_sign = f'{ALGORITHM}\n{hashed_canonical_request}'
        print(string_to_sign)

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

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


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


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


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

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


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


if __name__ == "__main__":
    # RPC介面請求
    http_method = "POST"
    canonical_uri = "/"
    host = "ecs.cn-beijing.aliyuncs.com"
    x_acs_action = "DescribeInstances"
    x_acs_version = "2014-05-26"
    request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    request.query_param['RegionId'] = 'cn-beijing'
    request.query_param['VpcId'] = 'vpc-2zeo42r27y4opXXXXXXXX'

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

    # ROA介面GET請求
    # http_method = "GET"
    # # canonicalUri如果存在path參數,需要對path參數encode,percent_code({path參數})
    # cluster_id_encode = percent_code("cb7cd6b9bde934f6193801878XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}/resources"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DescribeClusterResources"
    # x_acs_version = "2015-12-15"
    # request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # request.query_param['with_addon_resources'] = True

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

Go樣本

需要您在終端(Terminal)執行以下命令:

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

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"golang.org/x/exp/maps"
	"io"
	"os"
	"sort"

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

	"github.com/google/uuid"
)

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

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]string),
	}
	req.headers["host"] = host
	req.headers["x-acs-action"] = xAcsAction
	req.headers["x-acs-version"] = xAcsVersion
	req.headers["x-acs-date"] = time.Now().UTC().Format(time.RFC3339)
	req.headers["x-acs-signature-nonce"] = uuid.New().String()
	return req
}

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

func main() {
	// RPC介面請求
	httpMethod := "POST"
	canonicalUri := "/"
	host := "ecs.cn-beijing.aliyuncs.com"
	xAcsAction := "DescribeInstances"
	xAcsVersion := "2014-05-26"
	req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	req.queryParam["RegionId"] = "cn-beijing"
	req.queryParam["VpcId"] = "vpc-2zeo42r27y4opXXXXXXXX"

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

	/*	// ROA介面GET請求
		httpMethod := "GET"
                // canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
		canonicalUri := "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources"
		host := "cs.cn-beijing.aliyuncs.com"
		xAcsAction := "DescribeClusterResources"
		xAcsVersion := "2015-12-15"
		req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
		req.queryParam["with_addon_resources"] = "true"*/

	req.queryParam = sorted(req.queryParam)
	// 簽名過程
	getAuthorization(req)
	// 調用API
	err := callAPI(req)
	if err != nil {
		println(err.Error())
	}
}

func sorted(obj map[string]string) map[string]string {
	keys := maps.Keys(obj)
	sort.Strings(keys)
	newObj := make(map[string]string)
	for _, k := range keys {
		newObj[k] = obj[k]
	}
	return newObj
}

func callAPI(req *Request) error {
	urlStr := "https://" + req.host + req.canonicalUri
	q := url.Values{}
	for k, v := range req.queryParam {
		q.Set(k, v)
	}
	urlStr += "?" + q.Encode()

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

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

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

func getAuthorization(req *Request) {
	canonicalQueryString := ""
	for k, v := range req.queryParam {
		canonicalQueryString += percentCode(url.QueryEscape(k)) + "=" + percentCode(url.QueryEscape(v)) + "&"
	}
	canonicalQueryString = strings.TrimSuffix(canonicalQueryString, "&")

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

	canonicalHeaders := ""
	signedHeaders := ""
	HeadersKeys := maps.Keys(req.headers)
	sort.Strings(HeadersKeys)
	for _, k := range HeadersKeys {
		lowerKey := strings.ToLower(k)
		if lowerKey == "host" || strings.HasPrefix(lowerKey, "x-acs-") || lowerKey == "content-type" {
			canonicalHeaders += lowerKey + ":" + req.headers[k] + "\n"
			signedHeaders += lowerKey + ";"
		}
	}
	signedHeaders = strings.TrimSuffix(signedHeaders, ";")

	canonicalRequest := req.httpMethod + "\n" + req.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
	fmt.Printf("canonicalRequest========>\n%s\n", canonicalRequest)

	hashedCanonicalRequest := sha256Hex(canonicalRequest)
	stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
	fmt.Printf("stringToSign========>\n%s\n", stringToSign)

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

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

func hmac256(key []byte, toSignString string) ([]byte, error) {
	// 執行個體化HMAC-SHA256雜湊
	h := hmac.New(sha256.New, key)
	// 寫入待簽名的字串
	_, err := h.Write([]byte(toSignString))
	if err != nil {
		return nil, err
	}
	// 計算簽名並返回
	return h.Sum(nil), nil
}

func sha256Hex(str string) string {
	// 執行個體化SHA-256雜湊函數
	hash := sha256.New()
	// 將字串寫入雜湊函數
	_, _ = hash.Write([]byte(str))
	// 計算SHA-256雜湊值並轉換為小寫十六進位字串
	hexString := hex.EncodeToString(hash.Sum(nil))

	return hexString
}

func percentCode(str string) string {
	// 替換特定的編碼字元
	str = strings.ReplaceAll(str, "+", "%20")
	str = strings.ReplaceAll(str, "*", "%2A")
	str = strings.ReplaceAll(str, "%7E", "~")
	return str
}

Node.js樣本

本樣本所用語言是javaScript。在運行樣本前,請在終端(Terminal)執行以下命令下載所需要的module。

 npm install --save-dev @types/node
const crypto = require('crypto');
const {stringify} = require("node:querystring");
const {request} = require("node:https");

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

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

const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;

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

function getAuthorization(signRequest) {
    try {
        // 步驟 1:拼接規範請求串
        const canonicalQueryString = Object.entries(signRequest.queryParam)
            .sort((a, b) => a[0].localeCompare(b[0]))
            .map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
            .join('&');

        // 請求體,當請求本文為空白時,比如GET請求,RequestPayload固定為空白字串
        const requestPayload = signRequest.body || '';
        const hashedRequestPayload = sha256Hex(requestPayload);
        signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;

        // 將所有key都轉換為小寫
        signRequest.headers = Object.fromEntries(
            Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
        );

        const sortedKeys = Object.keys(signRequest.headers)
            .filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
            .sort();
        // 已簽名訊息頭列表,多個要求標頭名稱(小寫)按首字母升序排列並以英文分號(;)分隔
        const signedHeaders = sortedKeys.join(";")
        // 構造要求標頭,多個正常化訊息頭,按照訊息頭名稱(小寫)的字元代碼順序以升序排列後拼接在一起
        const canonicalHeaders = sortedKeys.reduce((result, key) => {
            const value = signRequest.headers[key];
            // 根據需要格式化結果字串
            return `${result}${key}:${value}\n`;
        }, '');

        const canonicalRequest = `${signRequest.httpMethod}\n${signRequest.canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${hashedRequestPayload}`;
        console.log('canonicalRequest=========>\n', canonicalRequest);

        // 步驟 2:拼接待簽名字串
        const hashedCanonicalRequest = sha256Hex(canonicalRequest);
        const stringToSign = ALGORITHM + '\n' + hashedCanonicalRequest;
        console.log('stringToSign=========>', stringToSign);

        // 步驟 3:計算簽名
        const signature = hmac256(accessKeySecret, stringToSign);
        console.log('signature=========>', signature);

        // 步驟 4:拼接 Authorization
        const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
        console.log('authorization=========>', authorization);
        signRequest.headers['Authorization'] = authorization;
    } catch (error) {
        console.error('Failed to get authorization');
        console.error(error);
    }
}

async function callApi(signRequest) {
    try {
        let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
        // 添加請求參數
        if (signRequest.queryParam) {
            let query = new URLSearchParams();
            for (let [key, value] of Object.entries(signRequest.queryParam)) {
                query.append(key, String(value));
            }
            url += '?' + query.toString();
        }
        // 配置請求選項
        let options = {
            method: signRequest.httpMethod.toUpperCase(),
            headers: {}
        };
        // 添加HTTP要求標頭
        if (signRequest.headers) {
            for (let [key, value] of Object.entries(signRequest.headers)) {
                options.headers[key] = String(value);
            }
        }
        // 處理請求體
        if (signRequest.body && ['POST', 'PUT'].includes(signRequest.httpMethod.toUpperCase())) {
            options.body = signRequest.body;
        }
        try {
            return (await fetch(url, options)).text();
        } catch (error) {
            // handler error
            console.error('Failed to send request:', error);
        }
    } catch (error) {
        if (error instanceof TypeError) {
            // handler error
            console.error('Invalid URI syntax');
        } else {
            // handler error
            console.error('An unexpected error occurred:', 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', Buffer.from(key, 'binary'));
    hmac.update(data, 'utf8');
    return hmac.digest('hex').toLowerCase();
}

function sha256Hex(str) {
    if (str === null || str === undefined) {
        throw new Error('輸入字串不可為null或undefined');
    }
    const hash = crypto.createHash('sha256');
    const digest = hash.update(str, 'utf8').digest('hex');
    return digest.toLowerCase();
}

// 樣本一:RPC介面請求
const httpMethod = 'GET';
const canonicalUri = '/';
const host = 'ecs.cn-beijing.aliyuncs.com';
const xAcsAction = 'DescribeInstances';
const xAcsVersion = '2014-05-26';
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
signRequest.queryParam = {
    RegionId: 'cn-beijing',
    VpcId: 'vpc-2zeo42r27y4opXXXXXXXX',
}

// 樣本二:ROA介面POST請求
// const httpMethod = 'POST';
// const canonicalUri = '/clusters';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'CreateCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// const body = {
//     name: 'testDemo',
//     region_id: 'cn-beijing',
//     cluster_type: 'ExternalKubernetes',
//     vpcid: 'vpc-2zeo42r27y4opXXXXXXXX',
//     service_cidr: '172.16.3.0/20',
//     security_group_id: 'sg-2zeh5ta2iklXXXXXXXX',
//     vswitch_ids: [
//         'vsw-2ze3aagwn397gXXXXXXXX'
//       ],
// }
// signRequest.body = JSON.stringify(body)
// // 根據需要設定正確的Content-Type,這裡假設是JSON
// signRequest.headers['content-type'] = 'application/json';

// 樣本三:ROA介面GET請求
// const httpMethod = 'GET';
// // canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
// const canonicalUri = '/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DescribeClusterResources';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// signRequest.queryParam = {
//     with_addon_resources: true,
// }

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

PHP樣本

<?php

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

    public function __construct()
    {
        date_default_timezone_set('UTC'); // 設定時區為GMT
        $this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // 從環境變數中擷取RAM使用者Access Key ID
        $this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // 從環境變數中擷取RAM使用者Access Key Secret
        $this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 設定密碼編譯演算法
    }

    public function main()
    {
        // RPC介面請求
        $request = $this->createRequest('POST', '/', 'ecs.cn-beijing.aliyuncs.com', 'DescribeRegions', '2014-05-26');
        $request['queryParam']=  ['RegionId' => 'cn-beijing'];

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

        // ROA介面GET請求
        // canonicalUri如果存在path參數,需要對path參數encode,rawurlencode({path參數})
        // $cluster_id = 'cb7cd6b9bde934f6193801878XXXXXXXX';
        // $canonicalUri = sprintf("/clusters/%s/resources", rawurlencode($cluster_id));
        // $request = $this->createRequest('GET', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DescribeClusterResources', '2015-12-15');
        // $request['queryParam'] = [
        //     'with_addon_resources' => true,
        // ];

        $this->getAuthorization($request);
        $this->callApi($request);
    }

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

    private function addRequestBody(&$request, $bodyData)
    {
        $request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
        $request['headers']['content-type'] = 'application/json; charset=utf-8';
    }

    private function getAuthorization(&$request)
    {
        $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'];

            // 初始化cURL會話
            $ch = curl_init();

            // 根據請求類型設定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;
                case "PUT":
                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
                    break;
                default:
                    echo "Unsupported HTTP method: " . $request['body'];
                    throw new Exception("Unsupported HTTP method");
            }

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

            // 設定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'])); // 添加要求標頭

            // 發送請求
            $result = curl_exec($ch);

            // 檢查是否有錯誤發生
            if (curl_errno($ch)) {
                echo "Failed to send request: " . curl_error($ch);
            } else {
                echo $result;
            }

            // 關閉cURL會話
            curl_close($ch);
        } catch (Exception $e) {
            echo "Error: " . $e->getMessage();
        }
    }

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

固定參數樣本

本樣本是以固定的參數值為例,協助您驗證簽名是否正確。根據下面假設的固定值計算完簽名之後,得到與本樣本一樣的簽名,表明您的簽名過程是正確的。

所需參數名稱

假設的參數值

AccessKeyID

YourAccessKeyId

AccessKeySecret

YourAccessKeySecret

x-acs-signature-nonce

3156853299f313e23d1673dc12e1703d

x-acs-date

2023-10-26T10:22:32Z

簽名流程如下:

  1. 構造正常化請求。

POST
/
ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
host:ecs.cn-shanghai.aliyuncs.com
x-acs-action:RunInstances
x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date:2023-10-26T10:22:32Z
x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
x-acs-version:2014-05-26

host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  1. 構造待簽名字串。

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
  1. 計算簽名。

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
  1. 將簽名添加到請求中。

POST /?ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai HTTP/1.1
Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
x-acs-action: RunInstances
host: ecs.cn-shanghai.aliyuncs.com
x-acs-date: 2023-10-26T09:01:01Z
x-acs-version: 2014-05-26
x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0
user-agent: AlibabaCloud (Mac OS X; x86_64) Java/1.8.0_352-b08 tea-util/0.2.6 TeaDSL/1
accept: application/json

簽名失敗常見報錯

Code

Message

解決方案

SignatureDoesNotMatch

Specified signature does not match our calculation.

在簽名過程中,您可能遺漏了對參數進行升序排序,也可能是多加了空格。請您仔細閱讀簽名機制的講解,可以根據提供的固定參數樣本驗證您的簽名過程是否正確。

IncompleteSignature

The request signature does not conform to Aliyun standards.