V3版本通過公用要求標頭設定介面所需的參數資訊,在簽名機制的實現上消除了介面風格的差異,從而實現了更標準、更簡化的設計。本文提供了詳細的指南,以協助您瞭解和實施阿里雲SDK V3版的請求結構及簽名過程。您將學習如何構造標準的HTTP請求,以及如何使用正確的簽名演算法來驗證請求的身份,以確保傳輸資料的完整性和安全性。如您希望自行研發阿里雲OpenAPI的請求籤名,本文將為您提供相關參考。
在OpenAPI門戶上提供了阿里雲SDK的相關產品,這些產品所提供的API支援使用V3簽名。
HTTP 要求結構
一個完整的阿里雲 OpenAPI 請求,包含以下部分。
名稱 | 是否必選 | 描述 | 樣本值 |
協議 | 是 | 您可以查閱不同雲產品的 API 參考文檔進行配置。支援通過 | https:// |
服務地址 | 是 | 即 Endpoint。您可以查閱不同雲產品的服務接入地址文檔,查閱不同服務地區下的服務地址。 | cs.aliyuncs.com |
resource_URI_parameters(介面URL) | 是 | 介面URL,包括介面路徑和位置在 path、 query的介面請求參數。 | /clusters/{cluster_id}/triggers |
RequestHeader(要求標頭資訊) | 是 | 要求標頭資訊,通常包含API的版本、Host、Authorization等資訊。後文將詳細說明。 | x-acs-action |
RequestBody | 是 | 定義在 body 中的業務請求參數,建議您在阿里雲 OpenAPI 開發人員門戶進行試用。 | cluster_id |
HTTPMethod | 是 | 請求使用的方法,ROA介面要求方法包括PUT、POST、GET、DELETE。 | POST |
RequestHeader(公用要求標頭)
一個完整的阿里雲 OpenAPI 請求,包含以下部分。
名稱 | 類型 | 是否必選 | 描述 | 樣本值 |
x-acs-action | String | 是 | API的名稱。您可以訪問阿里雲 OpenAPI 開發人員門戶,搜尋您想調用的 OpenAPI。 | RunInstances |
x-acs-version | String | 是 | API 版本。您可以訪問阿里雲 OpenAPI 開發人員門戶,查看您調用 OpenAPI 對應的 API 版本。 | 2014-05-26 |
Authorization | String | 非匿名請求必須 | 用於驗證請求合法性的認證資訊,格式為Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature。 其中SignatureAlgorithm為簽名加密方式,為ACS3-HMAC-SHA256。 Credential 為使用者的存取金鑰ID。您可以在RAM 控制台查看您的 AccessKeyId。如需建立 AccessKey,請參見建立AccessKey。 SignedHeaders為要求標頭中包含的參與簽名欄位鍵名,【說明】:除了Authorization之外,建議對所有公用要求標頭添加簽名,以提高安全性。 Signature為請求籤名,取值參見簽名機制。 | ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=e521358f7776c97df52e6b2891a8bc73026794a071b50c3323388c4e0df64804 |
x-acs-signature-nonce | String | 是 | 簽名唯一隨機數。該隨機數用於防止網路重放攻擊,每一次請求都必須使用不同的隨機數。 | d410180a5abf7fe235dd9b74aca91fc0 |
x-acs-date | String | 是 | 按照ISO 8601標準表示的UTC時間,格式為yyyy-MM-ddTHH:mm:ssZ,例如2018-01-01T12:00:00Z。值為請求發出前15分鐘內的時間。 | 2023-10-26T09:01:01Z |
host | String | 是 | 即服務地址,參見HTTP 要求結構。 | ecs.cn-shanghai.aliyuncs.com |
x-acs-content-sha256 | String | 是 | 請求本文Hash摘要後再base-16編碼的結果,與HashedRequestPayload一致。 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
x-acs-security-token | String | STS認證必傳 | 為調用AssumeRole介面傳回值中SecurityToken的值。 |
簽名機制
為保證API的安全調用,在調用API時阿里雲會對每個API請求通過簽名(Signature)進行身分識別驗證。無論使用HTTP還是HTTPS協議提交請求,都需要在請求中包含簽名資訊。
請求及返回結果都使用UTF-8字元集進行編碼。
對於每一次HTTP或者HTTPS協議請求,阿里雲會根據訪問中的簽名資訊驗證訪問要求者身份。您在訪問時簽名資訊時,請按照以下方法對請求進行簽名處理:
步驟一:構造正常化請求
使用AK/SK方式進行簽名與認證,首先需要規範請求內容,然後再進行簽名。用戶端與雲端服務API Gateway使用相同的請求規範,可以確保同一個HTTP請求的前後端得到相同的簽名結果,從而完成身份校正。
構造正常化請求(CanonicalRequest)的虛擬碼如下:
CanonicalRequest =
HTTPRequestMethod + '\n' + //http要求方法,全大寫
CanonicalURI + '\n' + //正常化URI
CanonicalQueryString + '\n' + //正常化查詢字串
CanonicalHeaders + '\n' + //正常化訊息頭
SignedHeaders + '\n' + //已簽名訊息頭
HashedRequestPayload
HTTPRequestMethod
要求方法,即大寫的HTTP方法名,如GET、POST。
CanonicalURI
正常化URI,是指URL的資源路徑部分經過編碼之後的結果。資源路徑部分指URL中host與查詢字串之間的部分,包含host之後的
/
但不包含查詢字串前的?
。使用者發起請求時的URI應使用正常化URI,編碼方式使用UTF-8字元集按照RFC3986的規則對URI中的每一部分(即被/
分割開的字串)進行編碼:字元A~Z、a~z、0~9以及字元
-
、_
、.
、~
不編碼。其他字元編碼成
%
加字元對應ASCII碼的16進位。樣本:半形雙引號("
)對應%22
。空格( )編碼成
%20
,而不是加號(+
)、星號(*
)替換為%2A
、%7E
替換為波浪號(~
)。如果您使用的是Java標準庫中的
java.net.URLEncoder
,可以先用標準庫中encode
編碼,隨後將編碼後的字元中加號(+
)替換為%20
、星號(*
)替換為%2A
、%7E
替換為波浪號(~
),即可得到上述規則描述的編碼字串。
重要RPC風格API使用正斜杠(
/
)作為CanonicalURI,ROA風格API該參數為中繼資料檔案中
path
的值,例如/api/v1/clusters。CanonicalQueryString
正常化查詢字串,構造方法如下:
將查詢字串中的參數按照參數名的字元代碼升序排列,具有重複名稱的參數應按值進行排序。
使用UTF-8字元集按照RFC3986的規則對每個參數的參數名和參數值分別進行URI編碼,具體規則與上一節中的CanonicalURI編碼規則相同。
使用等號(
=
)串連編碼後的請求參數名和參數值,對於沒有值的參數使用Null 字元串。多個請求參數之間使用與號(
&
)串連。
重要當請求的查詢字串為空白時,使用Null 字元串作為正常化查詢字串。
CanonicalHeaders
正常化要求標頭,是一個非標準HTTP頭部資訊。需要將請求中包含以
x-acs-
為首碼、host
、content-type
的參數資訊,添加到正常化要求標頭中,構造方法如下:將所有需要簽名的參數的名稱轉換為小寫。
將所有參數按照參數名稱的字元順序以升序排列。
將參數的值去除首尾空格。對於有多個值的參數,將多個值分別去除首尾空格後按值升序排列,然後用逗號(
,
)串連。將步驟2、3的結果以英文冒號(
:
)串連,並在尾部添加分行符號,組成一個正常化訊息頭(CanonicalHeaderEntry)。如果沒有需要簽名的訊息頭,使用Null 字元串作為正常化訊息頭列表。
重要除Authorization外的所有公用要求標頭,只要符合要求的參數都必須被加入簽名。
虛擬碼如下:
CanonicalHeaderEntry = Lowercase(HeaderName) + ':' + Trim(HeaderValue) + '\n' CanonicalHeaders = CanonicalHeaderEntry0 + CanonicalHeaderEntry1 + ... + CanonicalHeaderEntryN
SignedHeaders
已簽名訊息頭列表,用於說明此次請求中參與簽名的訊息頭,與CanonicalHeaders中包含的訊息頭一一對應。其構造方法如下:
將CanonicalHeaders中包含的要求標頭的名稱轉為小寫。
多個要求標頭名稱(小寫)按首字母升序排列並以英文分號(
;
)分隔,例如content-type;host;x-acs-date
。
虛擬碼如下:
SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ... + Lowercase(HeaderNameN)
HashedRequestPayload
當請求體(body)為空白時,RequestPayload固定為空白字串,否則RequestPayload的值為請求體(body)對應的JSON字串。再使用雜湊函數對RequestPayload進行轉換得到HashedRequestPayload,轉換規則用虛擬碼可表示為
HashedRequestPayload = HexEncode(Hash(RequestPayload))
。Hash表示訊息摘要函數,目前支援SHA256演算法,例如,當簽名協議使用ACS3-HMAC-SHA256時,應使用SHA256作為Hash函數。
HexEncode表示以小寫十六進位的形式返回摘要的編碼函數(即Base16編碼)。
表1:簽名協議與簽名演算法、摘要函數的對應關係
簽名協議(SignatureAlgorithm) | 處理RequestPayload以及CanonicalRequest時使用的摘要函數(Hash) | 計算簽名時實際使用的簽名演算法 (SignatureMethod) |
ACS3-HMAC-SHA256 | SHA256 | HMAC-SHA256 |
步驟二:構造待簽名字串
按照以下虛擬碼構造待簽名字串(stringToSign):
StringToSign =
SignatureAlgorithm + '\n' +
HashedCanonicalRequest
SignatureAlgorithm
簽名協議目前支援ACS3-HMAC-SHA256演算法,已不再支援基於MD5或SHA1的演算法。
HashedCanonicalRequest
正常化請求摘要串,計算方法虛擬碼如下:
HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
使用雜湊函數(Hash)對步驟一中得到的正常化請求(CanonicalRequest)進行摘要處理,具體使用的Hash函數取決於簽名協議(SignatureAlgorithm),參見表1,例如,當簽名協議為ACS3-HMAC-SHA256時,應使用SHA256作為Hash函數。
將上一步得到的摘要結果以小寫十六進位形式編碼。
步驟三:計算簽名
按照以下虛擬碼計算簽名值(Signature)。
Signature = HexEncode(SignatureMethod(Secret, StringToSign))
StringToSign:步驟二中構造的待簽名字串,UTF-8編碼。
SignatureMethod:簽名演算法,具體使用的演算法取決於簽名協議(SignatureAlgorithm),其對應關係如表1。
Secret:使用者的簽名密鑰,為位元據。
HexEncode:以小寫十六進位的形式返回摘要的編碼函數(即Base16編碼)。
步驟四:將簽名添加到請求中
計算完簽名後,構造Authorization要求標頭,格式為:Authorization:<SignatureAlgorithm> Credential=<AccessKeyId>,SignedHeaders=<SignedHeaders>,Signature=<Signature>
,樣本如下:
Authorization:ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=6b595d672d79c15b18edb4ccfba6789a24a6f2b82c400e03162d9279b08555d7
簽名樣本
為了讓您更清晰地理解上述簽名機制,下面以主流程式設計語言為例,將簽名機制完整實現。範例程式碼只是讓您更好地理解簽名機制,存在不通用性,阿里雲OpenAPI提供多種程式設計語言和開發架構的SDK,使用這些SDK可以免去簽名過程,便於您快速構建與阿里雲相關的應用程式,建議您使用SDK。
在簽名之前,請先查看OpenAPI中繼資料,擷取API的請求方式、請求參數以及參數位置等資訊。
Java樣本
運行Java樣本,需要您在pom.xml中添加以下Maven依賴。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.ContentType;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* signature demo
*/
public class SignatureDemo {
/**
* 日期格式化工具,用於將日期時間字串格式化為"yyyy-MM-dd'T'HH:mm:ss'Z'"的格式。
*/
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static class Request {
// HTTP Method
private final String httpMethod;
// 請求路徑,當資源路徑為空白時,使用正斜杠(/)作為CanonicalURI
private final String canonicalUri;
// endpoint
private final String host;
// API name
private final String xAcsAction;
// API version
private final String xAcsVersion;
// headers
TreeMap<String, Object> headers = new TreeMap<>();
// 調用API所需要的參數,參數位置在body。Json字串
String body;
// 調用API所需要的參數,參數位置在query,參數按照參數名的字元代碼升序排列
TreeMap<String, Object> queryParam = new TreeMap<>();
public Request(String httpMethod, String canonicalUri, String host, String xAcsAction, String xAcsVersion) {
this.httpMethod = httpMethod;
this.canonicalUri = canonicalUri;
this.host = host;
this.xAcsAction = xAcsAction;
this.xAcsVersion = xAcsVersion;
initHeader();
}
// init headers
private void initHeader() {
headers.put("host", host);
headers.put("x-acs-action", xAcsAction);
headers.put("x-acs-version", xAcsVersion);
SDF.setTimeZone(new SimpleTimeZone(0, "GMT")); // 設定日期格式化時區為GMT
headers.put("x-acs-date", SDF.format(new Date()));
headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
}
}
/**
* 這裡通過環境變數擷取Access Key ID和Access Key Secret,
*/
private final static String ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
private final static String ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
/**
* 簽名協議
*/
private static final String ALGORITHM = "ACS3-HMAC-SHA256";
/**
* 通過給定的參數構建請求,並進行簽名認證,最終發起請求
*/
public static void main(String[] args) {
// RPC介面請求
String httpMethod = "POST"; // 請求方式,大部分RPC介面同時支援POST和GET,此處以POST為例
String canonicalUri = "/"; // RPC介面無資源路徑,故使用正斜杠(/)作為CanonicalURI
String host = "ecs.cn-hangzhou.aliyuncs.com"; // endpoint
String xAcsAction = "DescribeInstanceStatus"; // API名稱
String xAcsVersion = "2014-05-26"; // API版本號碼
Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// 調用API所需要的參數,參數按照參數名的字元代碼升序排列,具有重複名稱的參數應按值進行排序。
request.queryParam.put("RegionId", "cn-hangzhou"); // RegionId的參數類型是String
String[] instanceIds = {"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"};
request.queryParam.put("InstanceId", Arrays.asList(instanceIds)); // InstanceId的參數類型是array
/*// ROA介面POST請求
String httpMethod = "POST";
String canonicalUri = "/clusters";
String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
String xAcsAction= "CreateCluster"; // API名稱
String xAcsVersion= "2015-12-15"; // API版本號碼
Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// 請求body,通過Gson將body轉成JSON字串,以下參數僅做樣本
TreeMap<String, Object> body = new TreeMap<>();
body.put("name", "testDemo");
body.put("region_id", "cn-beijing");
body.put("cluster_type", "ManagedKubernetes");
body.put("vpcid", "vpc-2zeo42r27y4opXXXXXXXX");
body.put("service_cidr", "172.16.1.0/20");
body.put("security_group_id", "sg-2zec0dm6qi66XXXXXXXX");
Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
request.body = gson.toJson(body);
request.headers.put("content-type", "application/json; charset=utf-8");*/
/*// ROA介面GET請求
String httpMethod = "GET";
// canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
String canonicalUri = "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources";
String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
String xAcsAction = "DescribeClusterResources"; // API名稱
String xAcsVersion = "2015-12-15"; // API版本號碼
Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
request.queryParam.put("with_addon_resources", true);*/
/*// ROA介面DELETE請求
String httpMethod = "DELETE";
String canonicalUri = "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX");
String host = "cs.cn-beijing.aliyuncs.com";
String xAcsAction = "DeleteCluster";
String xAcsVersion = "2015-12-15";
Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);*/
// 簽名過程
getAuthorization(request);
// 調用API
callApi(request);
}
private static void callApi(Request request) {
try {
// 通過HttpClient發送請求
String url = "https://" + request.host + request.canonicalUri;
URIBuilder uriBuilder = new URIBuilder(url);
// 添加請求參數
for (Map.Entry<String, Object> entry : request.queryParam.entrySet()) {
uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
}
System.out.println(uriBuilder.build());
HttpUriRequest httpRequest;
switch (request.httpMethod) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (request.body != null) {
httpPost.setEntity(new StringEntity(request.body, ContentType.APPLICATION_JSON));
}
httpRequest = httpPost;
break;
case "DELETE":
httpRequest = new HttpDelete(uriBuilder.build());
break;
default:
System.out.println("Unsupported HTTP method: " + request.body);
throw new IllegalArgumentException("Unsupported HTTP method");
}
// 添加http要求標頭
for (Map.Entry<String, Object> entry : request.headers.entrySet()) {
httpRequest.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
}
// 發送請求
try (CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(httpRequest)) {
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(result);
} catch (IOException e) {
// 異常處理
System.out.println("Failed to send request");
e.printStackTrace();
}
} catch (URISyntaxException e) {
// 異常處理
System.out.println("Invalid URI syntax");
e.printStackTrace();
}
}
/**
* 該方法用於根據傳入的HTTP要求方法、正常化的URI、查詢參數等,計算並產生授權資訊。
*/
private static void getAuthorization(Request request) {
try {
// 處理queryParam中參數值為List、Map類型的參數,將參數平鋪
TreeMap<String, Object> newQueryParam = new TreeMap<>();
processObject(newQueryParam, "", request.queryParam);
request.queryParam = newQueryParam;
// 步驟 1:拼接規範請求串
// 請求參數,當請求的查詢字串為空白時,使用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();
}
}
/**
* 遞迴處理對象,將複雜物件(如Map和List)展開為平面的索引值對
*
* @param map 原始的索引值對集合,將被遞迴地更新
* @param key 當前處理的鍵,隨著遞迴的深入,鍵會帶有嵌套路徑資訊
* @param value 對應於鍵的值,可以是嵌套的Map、List或其他類型
*/
private static void processObject(Map<String, Object> map, String key, Object value) {
// 如果值為空白,則無需進一步處理
if (value == null) {
return;
}
if (key == null) {
key = "";
}
// 當值為List類型時,遍曆List中的每個元素,並遞迴處理
if (value instanceof List<?>) {
List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); ++i) {
processObject(map, key + "." + (i + 1), list.get(i));
}
} else if (value instanceof Map<?, ?>) {
// 當值為Map類型時,遍曆Map中的每個索引值對,並遞迴處理
Map<?, ?> subMap = (Map<?, ?>) value;
for (Map.Entry<?, ?> entry : subMap.entrySet()) {
processObject(map, key + "." + entry.getKey().toString(), entry.getValue());
}
} else {
// 對於以"."開頭的鍵,移除開頭的"."以保持鍵的連續性
if (key.startsWith(".")) {
key = key.substring(1);
}
// 對於byte[]類型的值,將其轉換為UTF-8編碼的字串
if (value instanceof byte[]) {
map.put(key, new String((byte[]) value, StandardCharsets.UTF_8));
} else {
// 對於其他類型的值,直接轉換為字串
map.put(key, String.valueOf(value));
}
}
}
/**
* 使用HmacSHA256演算法產生訊息認證碼(MAC)。
*
* @param secretKey 密鑰,用於產生MAC的密鑰,必須保密。
* @param str 需要進行MAC認證的訊息。
* @return 返回使用HmacSHA256演算法計算出的訊息認證碼。
* @throws Exception 如果初始化MAC或計算MAC過程中遇到錯誤,則拋出異常。
*/
public static byte[] hmac256(byte[] secretKey, String str) throws Exception {
// 執行個體化HmacSHA256訊息認證碼產生器
Mac mac = Mac.getInstance("HmacSHA256");
// 建立密鑰規範,用於初始化MAC產生器
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, mac.getAlgorithm());
// 初始化MAC產生器
mac.init(secretKeySpec);
// 計算訊息認證碼並返回
return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
}
/**
* 使用SHA-256演算法計算字串的雜湊值並以十六進位字串形式返回。
*
* @param stringToSign 需要進行SHA-256雜湊計算的字串。
* @return 計算結果為小寫十六進位字串。
* @throws Exception 如果在擷取SHA-256訊息摘要執行個體時發生錯誤。
*/
public static String sha256Hex(String stringToSign) throws Exception {
// 擷取SHA-256訊息摘要執行個體
MessageDigest md = MessageDigest.getInstance("SHA-256");
// 計算字串s的SHA-256雜湊值
byte[] d = md.digest(stringToSign.getBytes(StandardCharsets.UTF_8));
// 將雜湊值轉換為小寫十六進位字串並返回
return DatatypeConverter.printHexBinary(d).toLowerCase();
}
/**
* 對指定的字串進行URL編碼。
* 使用UTF-8編碼字元集對字串進行編碼,並對特定的字元進行替換,以符合URL編碼規範。
*
* @param str 需要進行URL編碼的字串。
* @return 編碼後的字串。其中,加號"+"被替換為"%20",星號"*"被替換為"%2A",波浪號"%7E"被替換為"~"。
*/
public static String percentCode(String str) {
if (str == null) {
throw new IllegalArgumentException("輸入字串不可為null");
}
try {
return URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8編碼不被支援", e);
}
}
}
Python樣本
需要您手動安裝pytz和requests,請根據您所使用的Python版本在終端(Terminal)執行以下命令。
Python3
pip3 install pytz
pip3 install requests
Python2
pip install pytz
pip install requests
import hashlib
import hmac
import json
import os
import uuid
from collections import OrderedDict
from urllib.parse import urlencode, quote_plus
import pytz
import requests
from datetime import datetime
class Request:
def __init__(self, http_method, canonical_uri, host, x_acs_action, x_acs_version):
self.http_method = http_method
self.canonical_uri = canonical_uri
self.host = host
self.x_acs_action = x_acs_action
self.x_acs_version = x_acs_version
self.headers = self._init_headers()
self.query_param = OrderedDict()
self.body = None
def _init_headers(self):
headers = OrderedDict()
headers['host'] = self.host
headers['x-acs-action'] = self.x_acs_action
headers['x-acs-version'] = self.x_acs_version
current_time = datetime.now(pytz.timezone('Etc/GMT'))
headers['x-acs-date'] = current_time.strftime('%Y-%m-%dT%H:%M:%SZ')
headers['x-acs-signature-nonce'] = str(uuid.uuid4())
return headers
def sorted_query_params(self):
# 對查詢參數按名稱排序並返回編碼後的字串
sorted_query_params = sorted(self.query_param.items(), key=lambda item: item[0])
self.query_param = {k: v for k, v in sorted_query_params}
def sorted_headers(self):
# 對要求標頭按名稱排序並返回編碼後的字串
sorted_headers = sorted(self.headers.items(), key=lambda item: item[0])
self.headers = {k: v for k, v in sorted_headers}
# 環境變數中擷取Access Key ID和Access Key Secret
ACCESS_KEY_ID = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID')
ACCESS_KEY_SECRET = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET')
ALGORITHM = 'ACS3-HMAC-SHA256'
def get_authorization(request):
try:
new_query_param = OrderedDict()
process_object(new_query_param, '', request.query_param)
request.query_param = new_query_param
# 步驟 1:拼接規範請求串
canonical_query_string = '&'.join(
f'{percent_code(quote_plus(k))}={percent_code(quote_plus(str(v)))}' for k, v in request.query_param.items())
hashed_request_payload = sha256_hex(request.body or '')
request.headers['x-acs-content-sha256'] = hashed_request_payload
request.sorted_headers()
# 構建正常化要求標頭和已簽名訊息頭列表
filtered_headers = OrderedDict()
for k, v in request.headers.items():
if k.lower().startswith('x-acs-') or k.lower() in ['host', 'content-type']:
filtered_headers[k.lower()] = v
canonical_headers = '\n'.join(f'{k}:{v}' for k, v in filtered_headers.items()) + '\n'
signed_headers = ';'.join(k for k in filtered_headers.keys())
canonical_request = (f'{request.http_method}\n{request.canonical_uri}\n{canonical_query_string}\n'
f'{canonical_headers}\n{signed_headers}\n{hashed_request_payload}')
print(canonical_request)
# 步驟 2:拼接待簽名字串
hashed_canonical_request = sha256_hex(canonical_request)
string_to_sign = f'{ALGORITHM}\n{hashed_canonical_request}'
print(string_to_sign)
# 步驟 3:計算簽名
signature = hmac256(ACCESS_KEY_SECRET.encode('utf-8'), string_to_sign).hex().lower()
print(signature)
# 步驟 4:拼接Authorization
authorization = f'{ALGORITHM} Credential={ACCESS_KEY_ID},SignedHeaders={signed_headers},Signature={signature}'
print(authorization)
request.headers['Authorization'] = authorization
except Exception as e:
print("Failed to get authorization")
print(e)
def process_object(result_map, key, value):
# 如果值為空白,則無需進一步處理
if value is None:
return
if key is None:
key = ""
# 當值為清單類型時,遍曆列表中的每個元素,並遞迴處理
if isinstance(value, list):
for i, item in enumerate(value):
process_object(result_map, f"{key}.{i + 1}", item)
elif isinstance(value, dict):
# 當值為字典類型時,遍曆字典中的每個索引值對,並遞迴處理
for sub_key, sub_value in value.items():
process_object(result_map, f"{key}.{sub_key}", sub_value)
else:
# 對於以"."開頭的鍵,移除開頭的"."以保持鍵的連續性
if key.startswith("."):
key = key[1:]
# 對於位元組類型的值,將其轉換為UTF-8編碼的字串
if isinstance(value, bytes):
result_map[key] = value.decode('utf-8')
else:
# 對於其他類型的值,直接轉換為字串
result_map[key] = str(value)
def hmac256(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
def sha256_hex(s):
return hashlib.sha256(s.encode('utf-8')).hexdigest()
def call_api(request):
url = f'https://{request.host}{request.canonical_uri}'
if request.query_param:
url += '?' + urlencode(request.query_param, doseq=True, safe='*')
headers = {k: v for k, v in request.headers.items()}
if request.body:
data = request.body
else:
data = None
try:
response = requests.request(method=request.http_method, url=url, headers=headers, data=data)
response.raise_for_status()
print(response.text)
except requests.RequestException as e:
print("Failed to send request")
print(e)
def percent_code(encoded_str):
return encoded_str.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
if __name__ == "__main__":
# RPC介面請求
http_method = "POST"
canonical_uri = "/"
host = "ecs.cn-hangzhou.aliyuncs.com"
x_acs_action = "DescribeInstanceStatus"
x_acs_version = "2014-05-26"
request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# RegionId的參數類型是String
request.query_param['RegionId'] = 'cn-hangzhou'
# InstanceId的參數類型是array
request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"]
# ROA介面POST請求
# http_method = "POST"
# canonical_uri = "/clusters"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "CreateCluster"
# x_acs_version = "2015-12-15"
# request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# body = OrderedDict()
# body["name"] = "testDemo"
# body["region_id"] = "cn-beijing"
# body["cluster_type"] = "Kubernetes"
# body["vpcid"] = "vpc-2zeo42r27y4opXXXXXXXX"
# body["service_cidr"] = "172.16.1.0/20"
# body["security_group_id"] = "sg-2zec0dm6qi66XXXXXXXX"
# request.body = json.dumps(body, separators=(',', ':'))
# request.headers["content-type"] = "application/json; charset=utf-8"
# ROA介面GET請求
# http_method = "GET"
# # canonicalUri如果存在path參數,需要對path參數encode,percent_code({path參數})
# cluster_id_encode = percent_code("cb7cd6b9bde934f6193801878XXXXXXXX")
# canonical_uri = f"/clusters/{cluster_id_encode}/resources"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "DescribeClusterResources"
# x_acs_version = "2015-12-15"
# request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# request.query_param['with_addon_resources'] = True
request.sorted_query_params()
get_authorization(request)
call_api(request)
Go樣本
需要您在終端(Terminal)執行以下命令:
go get github.com/google/uuid
go get golang.org/x/exp/maps
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"os"
"sort"
"golang.org/x/exp/maps"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/google/uuid"
)
type Request struct {
httpMethod string
canonicalUri string
host string
xAcsAction string
xAcsVersion string
headers map[string]string
body string
queryParam map[string]interface{}
}
func NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion string) *Request {
req := &Request{
httpMethod: httpMethod,
canonicalUri: canonicalUri,
host: host,
xAcsAction: xAcsAction,
xAcsVersion: xAcsVersion,
headers: make(map[string]string),
queryParam: make(map[string]interface{}),
}
req.headers["host"] = host
req.headers["x-acs-action"] = xAcsAction
req.headers["x-acs-version"] = xAcsVersion
req.headers["x-acs-date"] = time.Now().UTC().Format(time.RFC3339)
req.headers["x-acs-signature-nonce"] = uuid.New().String()
return req
}
var (
AccessKeyId = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
ALGORITHM = "ACS3-HMAC-SHA256"
)
func main() {
// RPC介面請求
httpMethod := "POST"
canonicalUri := "/"
host := "ecs.cn-hangzhou.aliyuncs.com"
xAcsAction := "DescribeInstanceStatus"
xAcsVersion := "2014-05-26"
req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
req.queryParam["RegionId"] = "cn-hangzhou"
instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
req.queryParam["InstanceId"] = instanceIds
// ROA介面POST請求
// httpMethod := "POST"
// canonicalUri := "/clusters"
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "CreateCluster"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// body := make(map[string]string)
// body["name"] = "testDemo"
// body["region_id"] = "cn-beijing"
// body["cluster_type"] = "Kubernetes"
// body["vpcid"] = "vpc-2zeo42r27y4opXXXXXXXX"
// body["service_cidr"] = "172.16.1.0/20"
// body["security_group_id"] = "sg-2zec0dm6qi66XXXXXXXX"
// jsonBytes, err := json.Marshal(body)
// if err != nil {
// fmt.Println("Error marshaling to JSON:", err)
// return
// }
// req.body = string(jsonBytes)
// req.headers["content-type"] = "application/json; charset=utf-8"
// ROA介面GET請求
// httpMethod := "GET"
// // canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
// canonicalUri := "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources"
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "DescribeClusterResources"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// req.queryParam["with_addon_resources"] = "true"
// 簽名過程
getAuthorization(req)
// 調用API
err := callAPI(req)
if err != nil {
println(err.Error())
}
}
func callAPI(req *Request) error {
urlStr := "https://" + req.host + req.canonicalUri
q := url.Values{}
keys := maps.Keys(req.queryParam)
sort.Strings(keys)
for _, k := range keys {
v := req.queryParam[k]
q.Set(k, fmt.Sprintf("%v", v))
}
urlStr += "?" + q.Encode()
fmt.Println(urlStr)
httpReq, err := http.NewRequest(req.httpMethod, urlStr, strings.NewReader(req.body))
if err != nil {
return err
}
for key, value := range req.headers {
httpReq.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
var respBuffer bytes.Buffer
_, err = io.Copy(&respBuffer, resp.Body)
if err != nil {
return err
}
respBytes := respBuffer.Bytes()
fmt.Println(string(respBytes))
return nil
}
func getAuthorization(req *Request) {
// 處理queryParam中參數值為List、Map類型的參數,將參數平鋪
newQueryParams := make(map[string]interface{})
processObject(newQueryParams, "", req.queryParam)
req.queryParam = newQueryParams
// 步驟 1:拼接規範請求串
canonicalQueryString := ""
keys := maps.Keys(req.queryParam)
sort.Strings(keys)
for _, k := range keys {
v := req.queryParam[k]
canonicalQueryString += percentCode(url.QueryEscape(k)) + "=" + percentCode(url.QueryEscape(fmt.Sprintf("%v", v))) + "&"
}
canonicalQueryString = strings.TrimSuffix(canonicalQueryString, "&")
fmt.Printf("canonicalQueryString========>%s\n", canonicalQueryString)
hashedRequestPayload := sha256Hex(req.body)
req.headers["x-acs-content-sha256"] = hashedRequestPayload
canonicalHeaders := ""
signedHeaders := ""
HeadersKeys := maps.Keys(req.headers)
sort.Strings(HeadersKeys)
for _, k := range HeadersKeys {
lowerKey := strings.ToLower(k)
if lowerKey == "host" || strings.HasPrefix(lowerKey, "x-acs-") || lowerKey == "content-type" {
canonicalHeaders += lowerKey + ":" + req.headers[k] + "\n"
signedHeaders += lowerKey + ";"
}
}
signedHeaders = strings.TrimSuffix(signedHeaders, ";")
canonicalRequest := req.httpMethod + "\n" + req.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
fmt.Printf("canonicalRequest========>\n%s\n", canonicalRequest)
// 步驟 2:拼接待簽名字串
hashedCanonicalRequest := sha256Hex(canonicalRequest)
stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
fmt.Printf("stringToSign========>\n%s\n", stringToSign)
// 步驟 3:計算簽名
byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
if err != nil {
fmt.Println(err)
}
signature := strings.ToLower(hex.EncodeToString(byteData))
// 步驟 4:拼接Authorization
authorization := ALGORITHM + " Credential=" + AccessKeyId + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
fmt.Printf("authorization========>%s\n", authorization)
req.headers["Authorization"] = authorization
}
func hmac256(key []byte, toSignString string) ([]byte, error) {
// 執行個體化HMAC-SHA256雜湊
h := hmac.New(sha256.New, key)
// 寫入待簽名的字串
_, err := h.Write([]byte(toSignString))
if err != nil {
return nil, err
}
// 計算簽名並返回
return h.Sum(nil), nil
}
func sha256Hex(str string) string {
// 執行個體化SHA-256雜湊函數
hash := sha256.New()
// 將字串寫入雜湊函數
_, _ = hash.Write([]byte(str))
// 計算SHA-256雜湊值並轉換為小寫十六進位字串
hexString := hex.EncodeToString(hash.Sum(nil))
return hexString
}
func percentCode(str string) string {
// 替換特定的編碼字元
str = strings.ReplaceAll(str, "+", "%20")
str = strings.ReplaceAll(str, "*", "%2A")
str = strings.ReplaceAll(str, "%7E", "~")
return str
}
// processObject 遞迴處理對象,將複雜物件(如Map和List)展開為平面的索引值對
func processObject(mapResult map[string]interface{}, key string, value interface{}) {
if value == nil {
return
}
switch v := value.(type) {
case []interface{}:
for i, item := range v {
processObject(mapResult, fmt.Sprintf("%s.%d", key, i+1), item)
}
case map[string]interface{}:
for subKey, subValue := range v {
processObject(mapResult, fmt.Sprintf("%s.%s", key, subKey), subValue)
}
default:
if strings.HasPrefix(key, ".") {
key = key[1:]
}
if b, ok := v.([]byte); ok {
mapResult[key] = string(b)
} else {
mapResult[key] = fmt.Sprintf("%v", v)
}
}
}
Node.js樣本
本樣本所用語言是javaScript。
const crypto = require('crypto');
class Request {
constructor(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion) {
this.httpMethod = httpMethod;
this.canonicalUri = canonicalUri || '/';
this.host = host;
this.xAcsAction = xAcsAction;
this.xAcsVersion = xAcsVersion;
this.headers = {};
this.body = null;
this.queryParam = {};
this.initHeader();
}
initHeader() {
const date = new Date();
this.headers = {
'host': this.host,
'x-acs-action': this.xAcsAction,
'x-acs-version': this.xAcsVersion,
'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
}
}
}
const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
if (!accessKeyId || !accessKeySecret) {
console.error('ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables must be set.');
process.exit(1);
}
function getAuthorization(signRequest) {
try {
newQueryParam = {};
processObject(newQueryParam, "", signRequest.queryParam);
signRequest.queryParam = newQueryParam;
// 步驟 1:拼接規範請求串
const canonicalQueryString = Object.entries(signRequest.queryParam)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
.join('&');
// 請求體,當請求本文為空白時,比如GET請求,RequestPayload固定為空白字串
const requestPayload = signRequest.body || '';
const hashedRequestPayload = sha256Hex(requestPayload);
signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;
// 將所有key都轉換為小寫
signRequest.headers = Object.fromEntries(
Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
);
const sortedKeys = Object.keys(signRequest.headers)
.filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
.sort();
// 已簽名訊息頭列表,多個要求標頭名稱(小寫)按首字母升序排列並以英文分號(;)分隔
const signedHeaders = sortedKeys.join(";")
// 構造要求標頭,多個正常化訊息頭,按照訊息頭名稱(小寫)的字元代碼順序以升序排列後拼接在一起
const canonicalHeaders = sortedKeys.map(key => `${key}:${signRequest.headers[key]}`).join('\n') + '\n';
const canonicalRequest = [
signRequest.httpMethod,
signRequest.canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedRequestPayload
].join('\n');
console.log('canonicalRequest=========>\n', canonicalRequest);
// 步驟 2:拼接待簽名字串
const hashedCanonicalRequest = sha256Hex(canonicalRequest);
const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
console.log('stringToSign=========>', stringToSign);
// 步驟 3:計算簽名
const signature = hmac256(accessKeySecret, stringToSign);
console.log('signature=========>', signature);
// 步驟 4:拼接 Authorization
const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
console.log('authorization=========>', authorization);
signRequest.headers['Authorization'] = authorization;
} catch (error) {
console.error('Failed to get authorization');
console.error(error);
}
}
async function callApi(signRequest) {
try {
let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
// 添加請求參數
if (signRequest.queryParam) {
const query = new URLSearchParams(signRequest.queryParam);
url += '?' + query.toString();
}
console.log('url=========>', url);
// 配置請求選項
let options = {
method: signRequest.httpMethod.toUpperCase(),
headers: signRequest.headers
};
// 處理請求體
if (signRequest.body && ['POST', 'PUT'].includes(signRequest.httpMethod.toUpperCase())) {
options.body = signRequest.body;
}
return (await fetch(url, options)).text();
} catch (error) {
console.error('Failed to send request:', error);
}
}
function percentCode(str) {
return encodeURIComponent(str)
.replace(/\+/g, '%20')
.replace(/\*/g, '%2A')
.replace(/~/g, '%7E');
}
function hmac256(key, data) {
const hmac = crypto.createHmac('sha256', key);
hmac.update(data, 'utf8');
return hmac.digest('hex').toLowerCase();
}
function sha256Hex(str) {
if (str === null || str === undefined) {
throw new Error('輸入的字串有誤!');
}
const hash = crypto.createHash('sha256');
const digest = hash.update(str, 'utf8').digest('hex');
return digest.toLowerCase();
}
function processObject(map, key, value) {
// 如果值為空白,則無需進一步處理
if (value === null) {
return;
}
if (key === null) {
key = "";
}
// 當值為Array類型時,遍曆Array中的每個元素,並遞迴處理
if (Array.isArray(value)) {
value.forEach((item, index) => {
processObject(map, `${key}.${index + 1}`, item);
});
} else if (typeof value === 'object' && value !== null) {
// 當值為Object類型時,遍曆Object中的每個索引值對,並遞迴處理
Object.entries(value).forEach(([subKey, subValue]) => {
processObject(map, `${key}.${subKey}`, subValue);
});
} else {
// 對於以"."開頭的鍵,移除開頭的"."以保持鍵的連續性
if (key.startsWith('.')) {
key = key.slice(1);
}
map[key] = String(value);
}
}
// 樣本一:RPC介面請求
const httpMethod = 'GET';
const canonicalUri = '/';
const host = 'ecs.cn-hangzhou.aliyuncs.com';
const xAcsAction = 'DescribeInstanceStatus';
const xAcsVersion = '2014-05-26';
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
signRequest.queryParam = {
RegionId: 'cn-hangzhou',
InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}
// 樣本二:ROA介面POST請求
// const httpMethod = 'POST';
// const canonicalUri = '/clusters';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'CreateCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// const body = {
// name: 'testDemo',
// region_id: 'cn-beijing',
// cluster_type: 'ExternalKubernetes',
// vpcid: 'vpc-2zeo42r27y4opXXXXXXXX',
// service_cidr: '172.16.3.0/20',
// security_group_id: 'sg-2zeh5ta2iklXXXXXXXX',
// vswitch_ids: [
// 'vsw-2ze3aagwn397gXXXXXXXX'
// ],
// }
// signRequest.body = JSON.stringify(body)
// // 根據需要設定正確的Content-Type,這裡假設是JSON
// signRequest.headers['content-type'] = 'application/json';
// 樣本三:ROA介面GET請求
// const httpMethod = 'GET';
// // canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
// const canonicalUri = '/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DescribeClusterResources';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// signRequest.queryParam = {
// with_addon_resources: true,
// }
getAuthorization(signRequest);
callApi(signRequest).then(r => {
console.log(r);
}).catch(error => {
console.error(error);
});
PHP樣本
<?php
class SignatureDemo
{
private $ALGORITHM;
private $AccessKeyId;
private $AccessKeySecret;
public function __construct()
{
date_default_timezone_set('UTC'); // 設定時區為GMT
$this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // 從環境變數中擷取RAM使用者Access Key ID
$this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // 從環境變數中擷取RAM使用者Access Key Secret
$this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 設定密碼編譯演算法
}
public function main()
{
// RPC介面請求
$request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
$request['queryParam'] = [
'RegionId' => 'cn-hangzhou',
'InstanceId' => ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"]
];
// ROA介面POST請求
// $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
// $this->addRequestBody($request, [
// 'name' => 'testDemo',
// 'region_id' => 'cn-beijing',
// 'cluster_type' => 'ExternalKubernetes',
// 'vpcid' => 'vpc-2zeo42r27y4opXXXXXXXX',
// 'service_cidr' => '172.16.5.0/20',
// 'security_group_id' => 'sg-2zeh5ta2ikljXXXXXXXX',
// "vswitch_ids" => [
// "vsw-2zeuntqtklsk0XXXXXXXX"
// ],
// ]);
// ROA介面GET請求
// canonicalUri如果存在path參數,需要對path參數encode,rawurlencode({path參數})
// $cluster_id = 'cb7cd6b9bde934f6193801878XXXXXXXX';
// $canonicalUri = sprintf("/clusters/%s/resources", rawurlencode($cluster_id));
// $request = $this->createRequest('GET', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DescribeClusterResources', '2015-12-15');
// $request['queryParam'] = [
// 'with_addon_resources' => true,
// ];
$this->getAuthorization($request);
$this->callApi($request);
}
private function createRequest($httpMethod, $canonicalUri, $host, $xAcsAction, $xAcsVersion)
{
$headers = [
'host' => $host,
'x-acs-action' => $xAcsAction,
'x-acs-version' => $xAcsVersion,
'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'),
'x-acs-signature-nonce' => bin2hex(random_bytes(16)),
];
return [
'httpMethod' => $httpMethod,
'canonicalUri' => $canonicalUri,
'host' => $host,
'headers' => $headers,
'queryParam' => [],
'body' => null,
];
}
private function addRequestBody(&$request, $bodyData)
{
$request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
$request['headers']['content-type'] = 'application/json; charset=utf-8';
}
private function getAuthorization(&$request)
{
$request['queryParam'] = $this->processObject($request['queryParam']);
$canonicalQueryString = $this->buildCanonicalQueryString($request['queryParam']);
$hashedRequestPayload = hash('sha256', $request['body'] ?? '');
$request['headers']['x-acs-content-sha256'] = $hashedRequestPayload;
$canonicalHeaders = $this->buildCanonicalHeaders($request['headers']);
$signedHeaders = $this->buildSignedHeaders($request['headers']);
$canonicalRequest = implode("\n", [
$request['httpMethod'],
$request['canonicalUri'],
$canonicalQueryString,
$canonicalHeaders,
$signedHeaders,
$hashedRequestPayload,
]);
$hashedCanonicalRequest = hash('sha256', $canonicalRequest);
$stringToSign = "{$this->ALGORITHM}\n$hashedCanonicalRequest";
$signature = strtolower(bin2hex(hash_hmac('sha256', $stringToSign, $this->AccessKeySecret, true)));
$authorization = "{$this->ALGORITHM} Credential={$this->AccessKeyId},SignedHeaders=$signedHeaders,Signature=$signature";
$request['headers']['Authorization'] = $authorization;
}
private function callApi($request)
{
try {
// 通過cURL發送請求
$url = "https://" . $request['host'] . $request['canonicalUri'];
// 添加請求參數到URL
if (!empty($request['queryParam'])) {
$url .= '?' . http_build_query($request['queryParam']);
}
echo $url;
// 初始化cURL會話
$ch = curl_init();
// 設定cURL選項
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 禁用SSL認證驗證,請注意,這會降低安全性,不應在生產環境中使用(不推薦!!!)
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回而不是輸出內容
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // 添加要求標頭
// 根據請求類型設定cURL選項
switch ($request['httpMethod']) {
case "GET":
break;
case "POST":
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
break;
case "DELETE":
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
break;
default:
echo "Unsupported HTTP method: " . $request['body'];
throw new Exception("Unsupported HTTP method");
}
// 發送請求
$result = curl_exec($ch);
// 檢查是否有錯誤發生
if (curl_errno($ch)) {
echo "Failed to send request: " . curl_error($ch);
} else {
echo $result;
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
} finally {
// 關閉cURL會話
curl_close($ch);
}
}
function processObject($value)
{
// 如果值為空白,則無需進一步處理
if ($value === null) {
return;
}
$tmp = [];
foreach ($value as $k => $v) {
if (0 !== strpos($k, '_')) {
$tmp[$k] = $v;
}
}
return self::flatten($tmp);
}
private static function flatten($items = [], $delimiter = '.', $prepend = '')
{
$flatten = [];
foreach ($items as $key => $value) {
$pos = \is_int($key) ? $key + 1 : $key;
if (\is_object($value)) {
$value = get_object_vars($value);
}
if (\is_array($value) && !empty($value)) {
$flatten = array_merge(
$flatten,
self::flatten($value, $delimiter, $prepend . $pos . $delimiter)
);
} else {
if (\is_bool($value)) {
$value = true === $value ? 'true' : 'false';
}
$flatten["$prepend$pos"] = $value;
}
}
return $flatten;
}
private function convertHeadersToArray($headers)
{
$headerArray = [];
foreach ($headers as $key => $value) {
$headerArray[] = "$key: $value";
}
return $headerArray;
}
private function buildCanonicalQueryString($queryParams)
{
ksort($queryParams);
// Build and encode query parameters
$params = [];
foreach ($queryParams as $k => $v) {
if (null === $v) {
continue;
}
$str = rawurlencode($k);
if ('' !== $v && null !== $v) {
$str .= '=' . rawurlencode($v);
} else {
$str .= '=';
}
$params[] = $str;
}
return implode('&', $params);
}
private function buildCanonicalHeaders($headers)
{
// Sort headers by key and concatenate them
uksort($headers, 'strcasecmp');
$canonicalHeaders = '';
foreach ($headers as $key => $value) {
$canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n";
}
return $canonicalHeaders;
}
private function buildSignedHeaders($headers)
{
// Build the signed headers string
$signedHeaders = array_keys($headers);
sort($signedHeaders, SORT_STRING | SORT_FLAG_CASE);
return implode(';', array_map('strtolower', $signedHeaders));
}
}
$demo = new SignatureDemo();
$demo->main();
.NET樣本
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace SignatureDemo
{
public class Request
{
public string HttpMethod { get; private set; }
public string CanonicalUri { get; private set; }
public string Host { get; private set; }
public string XAcsAction { get; private set; }
public string XAcsVersion { get; private set; }
public SortedDictionary<string, object> Headers { get; private set; }
public string Body { get; set; }
public Dictionary<string, object> QueryParam { get; set; }
public Request(string httpMethod, string canonicalUri, string host, string xAcsAction, string xAcsVersion)
{
HttpMethod = httpMethod;
CanonicalUri = canonicalUri;
Host = host;
XAcsAction = xAcsAction;
XAcsVersion = xAcsVersion;
Headers = [];
QueryParam = [];
Body = "";
InitHeader();
}
private void InitHeader()
{
Headers["host"] = Host;
Headers["x-acs-action"] = XAcsAction;
Headers["x-acs-version"] = XAcsVersion;
DateTime utcNow = DateTime.UtcNow;
Headers["x-acs-date"] = utcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
Headers["x-acs-signature-nonce"] = Guid.NewGuid().ToString();
}
}
public class Program
{
private static readonly string AccessKeyId = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID") ?? throw new InvalidOperationException("環境變數 ALIBABA_CLOUD_ACCESS_KEY_ID 未設定");
private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ?? throw new InvalidOperationException("環境變數 ALIBABA_CLOUD_ACCESS_KEY_SECRET 未設定");
private const string Algorithm = "ACS3-HMAC-SHA256";
private const string ContentType = "content-type";
public static void Main(string[] args)
{
// RPC介面請求
string httpMethod = "POST"; // RPC介面大部分是同時支援POST和GET的
string canonicalUri = "/";
string host = "ecs.cn-hangzhou.aliyuncs.com";
string xAcsAction = "DescribeInstanceStatus";
string xAcsVersion = "2014-05-26";
var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
request.QueryParam["RegionId"] = "cn-hangzhou"; // RegionId的類型是string
List<string> instanceIds = ["i-bp10igfmnytt9zhth92h", "i-bp1incuofvzxww9rcz80", "i-bp1incuofvzxww9rcz7z"];
request.QueryParam["InstanceId"] = instanceIds; // InstanceId的類型是array
// // ROA介面POST請求
// String httpMethod = "POST";
// String canonicalUri = "/clusters";
// String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
// String xAcsAction = "CreateCluster"; // API名稱
// String xAcsVersion = "2015-12-15"; // API版本號碼
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // 請求body,通過JsonConvert將body轉成JSON字串
// var body = new SortedDictionary<string, object>
// {
// { "name", "testDemo" },
// { "region_id", "cn-beijing" },
// { "cluster_type", "ManagedKubernetes" },
// { "vpcid", "vpc-2zeo42r27y4opXXXXXXXX" },
// { "service_cidr", "172.16.1.0/20" },
// { "security_group_id", "sg-2zec0dm6qi66XXXXXXXX" }
// };
// string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
// request.Body = jsonBody;
// request.Headers[ContentType] = "application/json; charset=utf-8";
// // ROA介面GET請求
// String httpMethod = "GET";
// // canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
// String canonicalUri = "/clusters/" + PercentCode("c3c2987c8beae4b1b85bef006xxxxxxxx") + "/resources";
// String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
// String xAcsAction = "DescribeClusterResources"; // API名稱
// String xAcsVersion = "2015-12-15"; // API版本號碼
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// request.QueryParam["with_addon_resources"]=true;
// // ROA介面DELETE請求
// String httpMethod = "DELETE";
// // canonicalUri如果存在path參數,需要對path參數encode,percentCode({path參數})
// String canonicalUri = "/clusters/" + PercentCode("c3c2987c8beae4b1b85bef006xxxxxxxx");
// String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
// String xAcsAction = "DeleteCluster"; // API名稱
// String xAcsVersion = "2015-12-15"; // API版本號碼
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
GetAuthorization(request);
var result = CallApiAsync(request);
Console.WriteLine($"result:{result.Result}");
}
private static async Task<string?> CallApiAsync(Request request)
{
try
{
// 聲明 httpClient
using var httpClient = new HttpClient();
// 構建 URL
string url = $"https://{request.Host}{request.CanonicalUri}";
var uriBuilder = new UriBuilder(url);
var query = new List<string>();
// 添加請求參數
foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
{
string value = entry.Value?.ToString() ?? "";
query.Add($"{entry.Key}={Uri.EscapeDataString(value)}");
}
uriBuilder.Query = string.Join("&", query);
Console.WriteLine(uriBuilder.Uri);
var requestMessage = new HttpRequestMessage
{
RequestUri = uriBuilder.Uri,
};
// 佈建要求頭
foreach (var entry in request.Headers)
{
if (entry.Key == "Authorization")
{
requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ;
}
else if (entry.Key == ContentType) // 與main中定義的要一致
{
continue;
}
else
{
requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
}
}
// 發送請求
HttpResponseMessage response;
switch (request.HttpMethod.ToUpper())
{
case "GET":
response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
break;
case "POST":
requestMessage.Method = HttpMethod.Post;
requestMessage.Content = new StringContent(request.Body ?? "", Encoding.UTF8, "application/json");
response = await httpClient.SendAsync(requestMessage);
break;
case "DELETE":
requestMessage.Method = HttpMethod.Delete;
response = await httpClient.SendAsync(requestMessage);
break;
default:
Console.WriteLine("Unsupported HTTP method: " + request.HttpMethod);
throw new ArgumentException("Unsupported HTTP method");
}
// 讀取響應內容
string result = await response.Content.ReadAsStringAsync();
return result;
}
catch (UriFormatException e)
{
Console.WriteLine("Invalid URI syntax");
Console.WriteLine(e.Message);
return null;
}
catch (Exception e)
{
Console.WriteLine("Failed to send request");
Console.WriteLine(e);
return null;
}
}
private static void GetAuthorization(Request request)
{
try
{
// 處理queryParam中參數值為List、Map類型的參數,將參數平鋪
request.QueryParam = FlattenDictionary(request.QueryParam);
// 步驟 1:拼接規範請求串
StringBuilder canonicalQueryString = new();
foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
{
if (canonicalQueryString.Length > 0)
{
canonicalQueryString.Append('&');
}
canonicalQueryString.Append($"{PercentCode(entry.Key)}={PercentCode(entry.Value?.ToString() ?? "")}");
}
string requestPayload = string.IsNullOrEmpty(request.Body) ? "" : request.Body;
string hashedRequestPayload = Sha256Hash(requestPayload);
request.Headers["x-acs-content-sha256"] = hashedRequestPayload;
StringBuilder canonicalHeaders = new();
StringBuilder signedHeadersSb = new();
foreach (var entry in request.Headers.OrderBy(e => e.Key.ToLower()))
{
if (entry.Key.StartsWith("x-acs-", StringComparison.CurrentCultureIgnoreCase) || entry.Key.Equals("host", StringComparison.OrdinalIgnoreCase) || entry.Key.Equals(ContentType, StringComparison.OrdinalIgnoreCase))
{
string lowerKey = entry.Key.ToLower();
string value = (entry.Value?.ToString() ?? "").Trim();
canonicalHeaders.Append($"{lowerKey}:{value}\n");
signedHeadersSb.Append($"{lowerKey};");
}
}
string signedHeaders = signedHeadersSb.ToString().TrimEnd(';');
string canonicalRequest = $"{request.HttpMethod}\n{request.CanonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
Console.WriteLine($"canonicalRequest:{canonicalRequest}");
// 步驟 2:拼接待簽名字串
string hashedCanonicalRequest = Sha256Hash(canonicalRequest);
string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
Console.WriteLine($"stringToSign:{stringToSign}");
// 步驟 3:計算簽名
string signature = HmacSha256(AccessKeySecret, stringToSign);
// 步驟 4:拼接 Authorization
string authorization = $"{Algorithm} Credential={AccessKeyId},SignedHeaders={signedHeaders},Signature={signature}";
request.Headers["Authorization"] = authorization;
Console.WriteLine($"authorization:{authorization}");
}
catch (Exception ex)
{
Console.WriteLine("Failed to get authorization");
Console.WriteLine(ex.Message);
}
}
private static Dictionary<string, object> FlattenDictionary(Dictionary<string, object> dictionary, string prefix = "")
{
var result = new Dictionary<string, object>();
foreach (var kvp in dictionary)
{
string key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";
if (kvp.Value is Dictionary<string, object> nestedDict)
{
var nestedResult = FlattenDictionary(nestedDict, key);
foreach (var nestedKvp in nestedResult)
{
result[nestedKvp.Key] = nestedKvp.Value;
}
}
else if (kvp.Value is List<string> list)
{
for (int i = 0; i < list.Count; i++)
{
result[$"{key}.{i + 1}"] = list[i];
}
}
else
{
result[key] = kvp.Value;
}
}
return result;
}
private static string HmacSha256(string key, string message)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
{
byte[] hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
}
}
private static string Sha256Hash(string input)
{
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
private static string PercentCode(string str)
{
if (string.IsNullOrEmpty(str))
{
throw new ArgumentException("輸入字串不可為null或空");
}
return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
}
}
}
Shell指令碼樣本
#!/bin/bash
accessKey_id="<YOUR-ACCESSKEY-ID>"
accessKey_secret="<YOUR-ACCESSKEY-SECRET>"
algorithm="ACS3-HMAC-SHA256"
# 請求參數
httpMethod="POST"
host="ecs.cn-hangzhou.aliyuncs.com"
queryParam=("RegionId=cn-hangzhou" "InstanceId=(\"i-bp10igfmnyttXXXXXXXX\" \"i-bp1incuofvzxXXXXXXXX\" \"i-bp1incuofvzxXXXXXXXX\")")
action="DescribeInstanceStatus"
version="2014-05-26"
canonicalURI="/"
RequestPayload=""
# 按照ISO 8601標準表示的UTC時間
utc_timestamp=$(date +%s)
utc_date=$(date -u -d @${utc_timestamp} +"%Y-%m-%dT%H:%M:%SZ")
# x-acs-signature-nonce 隨機數
random=$(uuidgen | sed 's/-//g')
# 簽名header
headers="host:${host}
x-acs-action:${action}
x-acs-version:${version}
x-acs-date:${utc_date}
x-acs-signature-nonce:${random}"
# 步驟 1:拼接規範請求串
# 將queryParam中的參數全部平鋪
newQueryParam=()
# 遍曆每一個原始參數
for param in "${queryParam[@]}"; do
# 檢查是否包含等號,以確定是索引值對
if [[ "$param" == *"="* ]]; then
# 分割鍵和值
IFS='=' read -r key value <<< "$param"
# 檢查值是否為一個列表(通過尋找括弧)
if [[ "$value" =~ ^\(.+\)$ ]]; then
# 去掉兩邊的括弧
value="${value:1:-1}"
# 使用IFS分割值列表
IFS=' ' read -ra values <<< "$value"
# 對於每個值添加索引
index=1
for val in "${values[@]}"; do
# 去除雙引號
val="${val%\"}"
val="${val#\"}"
# 添加到新數組
newQueryParam+=("$key.$index=$val")
((index++))
done
else
# 如果不是列表,則直接添加
newQueryParam+=("$param")
fi
else
# 如果沒有等號,直接保留原樣
newQueryParam+=("$param")
fi
done
# 處理並排序新的查詢參數
sortedParams=()
declare -A paramsMap
for param in "${newQueryParam[@]}"; do
IFS='=' read -r key value <<< "$param"
paramsMap["$key"]="$value"
done
# 根據鍵排序
for key in $(echo ${!paramsMap[@]} | tr ' ' '\n' | sort); do
sortedParams+=("$key=${paramsMap[$key]}")
done
# 1.1 拼接正常化查詢字串
canonicalQueryString=""
first=true
for item in "${sortedParams[@]}"; do
[ "$first" = true ] && first=false || canonicalQueryString+="&"
# 檢查是否存在等號
if [[ "$item" == *=* ]]; then
canonicalQueryString+="$item"
else
canonicalQueryString+="$item="
fi
done
# 1.2 處理請求體
hashedRequestPayload=$(echo -n "$body" | openssl dgst -sha256 | awk '{print $2}')
headers="${headers}
x-acs-content-sha256:$hashedRequestPayload"
# 1.3 構造正常化要求標頭
canonicalHeaders=$(echo "$headers" | grep -E '^(host|content-Type|x-acs-)' | while read line; do
key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
value=$(echo "$line" | cut -d':' -f2-)
echo "${key}:${value}"
done | sort | tr '\n' '\n')
signedHeaders=$(echo "$headers" | grep -E '^(host|content-Type|x-acs-)' | while read line; do
key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
echo "$key"
done | sort | tr '\n' ';' | sed 's/;$//')
# 1.4 構造規範請求
canonicalRequest="${httpMethod}\n${canonicalURI}\n${canonicalQueryString}\n${canonicalHeaders}\n\n${signedHeaders}\n${hashedRequestPayload}"
echo -e "canonicalRequest=${canonicalRequest}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
hashedCanonicalRequest=$(printf "${canonicalRequest}" | openssl sha256 -hex | awk '{print $2}')
# 步驟 2:建構簽章字串
stringToSign="${algorithm}\n${hashedCanonicalRequest}"
echo -e "stringToSign=$stringToSign"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
# 步驟 3:計算簽名
signature=$(printf "${stringToSign}" | openssl dgst -sha256 -hmac "${accessKey_secret}" | sed 's/^.* //')
echo -e "signature=${signature}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
# 步驟 4:構造Authorization
authorization="${algorithm} Credential=${accessKey_id},SignedHeaders=${signedHeaders},Signature=${signature}"
echo -e "authorization=${authorization}"
# 構造 curl 命令
url="https://$host$canonicalURI"
curl_command="curl -X $httpMethod '$url?$canonicalQueryString'"
# 添加要求標頭
IFS=$'\n' # 設定分行符號為新的IFS
for header in $headers; do
curl_command="$curl_command -H '$header'"
done
curl_command+=" -H 'Authorization:$authorization'"
echo "$curl_command"
# 執行 curl 命令
eval "$curl_command"
固定參數樣本
本樣本是以固定的參數值為例,協助您驗證簽名是否正確。根據下面假設的固定值計算完簽名之後,得到與本樣本一樣的簽名,表明您的簽名過程是正確的。
所需參數名稱 | 假設的參數值 |
AccessKeyID | YourAccessKeyId |
AccessKeySecret | YourAccessKeySecret |
x-acs-signature-nonce | 3156853299f313e23d1673dc12e1703d |
x-acs-date | 2023-10-26T10:22:32Z |
簽名流程如下:
構造正常化請求。
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
構造待簽名字串。
ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
計算簽名。
06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
將簽名添加到請求中。
POST /?ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai HTTP/1.1
Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
x-acs-action: RunInstances
host: ecs.cn-shanghai.aliyuncs.com
x-acs-date: 2023-10-26T09:01:01Z
x-acs-version: 2014-05-26
x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0
user-agent: AlibabaCloud (Mac OS X; x86_64) Java/1.8.0_352-b08 tea-util/0.2.6 TeaDSL/1
accept: application/json
常見問題
簽名失敗,提示“Specified signature does not match our calculation.”或者“The request signature does not conform to Aliyun standards.”。
問題原因:出現該問題通常是由於在簽名機制的某個步驟中遺漏了必要的操作,例如參數未按升序排列或SignedHeaders未按升序排列等。
解決方案:請首先根據固定簽名樣本驗證您的簽名過程是否正確,逐步對比每個步驟的返回內容與固定簽名樣本的一致性。如發現不一致之處,請仔細閱讀簽名機制中相應步驟的說明。
自簽名時,如果使用GET調試成功了,可以使用POST嗎?
對於RPC介面,通常既支援GET請求也支援POST請求;而對於ROA介面,則僅支援單一的請求方式。如何擷取API支援的請求方式,請參見OpenAPI中繼資料。
聯絡我們
當您在計算簽名時遇到無法解決的問題時,可以加入DingTalk群:78410016550,聯絡值班同學進行諮詢。