本文將為您介紹如何通過自簽名方式發起HTTP請求,調用阿里雲RPC風格的OpenAPI。
不再推薦使用該訪問方式,請移步參考V3版本請求體&簽名機制。
HTTP 要求結構
一個完整的阿里雲 RPC 請求由以下部分組成:
名稱 | 是否必選 | 描述 | 樣本值 |
協議 | 是 | 請求協議,您可以在OpenAPI中繼資料中查看API支援的請求協議。若API同時支援 | https:// |
服務地址 | 是 | 即 Endpoint。您可以查閱不同雲產品的服務接入地址文檔擷取Endpoint。 | ecs.cn-hangzhou.aliyuncs.com |
公用請求參數 | 是 | 阿里雲OpenAPI的公用請求參數,更多資訊,請參見下文公用請求參數。 | Action |
介面自訂請求參數 | 否 | API的請求參數,您可以在OpenAPI中繼資料中查看API定義的請求參數,或者在阿里雲 OpenAPI 開發人員門戶查看。 | RegionId |
HTTPMethod | 是 | 請求方式,您可以在OpenAPI中繼資料中查看API支援的請求方式。 | GET |
公用請求參數
每個OpenAPI請求都需包含以下參數:
名稱 | 類型 | 是否必選 | 描述 | 樣本值 |
Action | String | 是 | API 的名稱。您可以訪問阿里雲 OpenAPI 開發人員門戶,搜尋您想調用的API 。 | CreateInstance |
Version | String | 是 | API 版本。您可以訪問阿里雲 OpenAPI 開發人員門戶,查看雲產品的API版本。例如簡訊服務產品,您可以通過查看雲產品首頁中看到API 版本為 2017-05-25。 | 2014-05-26 |
Format | String | 否 | 指定介面返回資料格式,可選 JSON 或 XML,預設為 XML。 | JSON |
AccessKeyId | String | 是 | 阿里雲存取金鑰 ID。您可以在RAM 控制台查看您的 AccessKeyId。如需建立 AccessKey,請參見建立AccessKey。 | yourAccessKeyId |
SignatureNonce | String | 是 | 簽名唯一隨機數。用於防止網路重放攻擊,建議您每一次請求都使用不同的隨機數,隨機數位元無限制。 | 15215528852396 |
Timestamp | String | 是 | 按照ISO 8601標準表示的UTC時間,格式為yyyy-MM-ddTHH:mm:ssZ,有效期間為31分鐘,即產生時間戳記後需要在31分鐘內發起請求。樣本: | 2018-01-01T12:00:00Z |
SignatureMethod | String | 是 | 簽名方式。目前為固定值 | HMAC-SHA1 |
SignatureVersion | String | 是 | 簽名演算法版本。目前為固定值 | 1.0 |
Signature | String | 是 | 請求籤名,使用者請求的身分識別驗證。更多資訊,請參見簽名機制。 | Pc5WB8gokVn0xfeu%2FZV%2BiNM1dgI%3D |
簽名機制
為了確保 API 的安全性,每個請求都需通過簽名(Signature)進行身分識別驗證。以下是簽名計算的步驟:
步驟一:構造正常化請求字串
1、將公用請求參數和介面自訂請求參數合并,並將合并後的參數按照參數首字母的字典順序進行排序,排序時不包括公用請求參數中的Signature
參數。 虛擬碼如下:
// 合并公用請求參數和介面自訂參數,並根據key排序
params = merged(publicParams,apiReuqestParams)
sortParams = sorted(params.keys())
當請求參數資訊包含
"in": "formData"
時,需要將這類參數按照固定格式拼接為一個字串,拼接格式為:key1=value1&key2=value2&key3=value3
。若請求參數的請求類型同時是array、object時,需要將參數平鋪為一個新的映射(map)。例如{"key":["value1","value2"]}
平鋪後為{"key.1":"value1","key.2":"value2"}
。還需要在公用請求參數中添加content-type=application/x-www-form-urlencoded
。當請求參數資訊包含
"in": "body"
時,需要在公用請求參數中添加content-type,content-type的值與請求內容類型有關。例如:請求內容類型為JSON資料時,content-type的值為
application/json
。請求內容類型為二進位檔案流時,content-type的值為
application/octet-stream
。
2、使用 UTF-8 字元集按照RFC3986規範對請求參數及其值進行編碼,並使用等號(=)將請求參數與參數值串連。
編碼規則:
字元 A~Z、a~z、0~9 以及字元
-
、_
、.
、~
不編碼。對其他 ASCII 碼字元進行編碼。編碼格式為%加上16進位的 ASCII 碼。例如半形雙引號(
"
)將被編碼為%22
。需要注意的是,部分特殊字元需要特殊處理,具體如下:編碼前
編碼後
空格( )
%20
星號(
*
)%2A
%7E
波浪號(
~
)
虛擬碼如下:
encodeURIComponentParam = encodeURIComponent(sortParams.key) + "=" + encodeURIComponent(sortParams.value)
3、將步驟2的結果通過&
串連,即可得到正常化請求字串CanonicalizedQueryString
。請注意,參數的排序與第1步保持一致。虛擬碼如下:
CanonicalizedQueryString = encodeURIComponentParam1 + "&" + encodeURIComponentParam2 + ... + encodeURIComponentParamN
步驟二:建構簽章字串
構造待簽名字串 stringToSign
。該字串構造規則的虛擬碼如下:
stringToSign =
HTTPMethod + "&" + // HTTPMethod:發送請求的 HTTP 方法,例如 GET。
encodeURIComponent("/") + "&" + // encodeURIComponent 為步驟一第2步的編碼方法
encodeURIComponent(CanonicalizedQueryString) // CanonicalizedQueryString 為步驟一擷取的正常化請求字串。
步驟三:計算簽名
按照RFC2104的定義,通過您傳入的 AccessKeyId 對應的密鑰 AccessSecret,使用 HMAC-SHA1
的簽名演算法,計算待簽名字串StringToSign
的簽名。其中 Base64() 為編碼計算函數,HMAC_SHA1() 為 HMAC_SHA1 簽名函數,傳回值為 HMAC_SHA1 加密後原始位元組,而非16進位字串,UTF_8_Encoding_Of() 是 UTF-8 字元編碼函數,虛擬碼如下:
signature = Base64(HMAC_SHA1(AccessSecret + "&", UTF_8_Encoding_Of(stringToSign)))
步驟四:將signature添加到URL
將signature的值按照RFC3986規範編碼之後添加到介面URL上。
簽名樣本
固定參數樣本
本樣本以調用 ECS DescribeDedicatedHosts查詢一台或多台Dedicated Host的詳細資料為例,根據假設的參數值,展示了簽名機制中每個步驟所產生的正確輸出內容。您可以在代碼中使用本樣本提供的假設參數值進行計算,並通過對比您的輸出結果與本樣本的內容,以驗證簽名過程的正確性。
所需參數名稱 | 假設的參數值 |
Endpoint | ecs.cn-beijing.aliyuncs.com |
Action | DescribeDedicatedHosts |
Version | 2014-05-26 |
Format | JSON |
AccessKeyId | testid |
AccessKeySecret | testsecret |
SignatureNonce | edb2b34af0af9a6d14deaf7c1a5315eb |
Timestamp | 2023-03-13T08:34:30Z |
業務請求參數
所需參數名稱 | 假設的參數值 |
RegionId | cn-beijing |
簽名流程如下:
構造正常化請求字串。
AccessKeyId=testid&Action=DescribeDedicatedHosts&Format=JSON&RegionId=cn-beijing&SignatureMethod=HMAC-SHA1&SignatureNonce=edb2b34af0af9a6d14deaf7c1a5315eb&SignatureVersion=1.0&Timestamp=2023-03-13T08%3A34%3A30Z&Version=2014-05-26
構造待簽名字串
stringToSign
。GET&%2F&AccessKeyId%3Dtestid%26Action%3DDescribeDedicatedHosts%26Format%3DJSON%26RegionId%3Dcn-beijing%26SignatureMethod%3DHMAC-SHA1%26SignatureNonce%3Dedb2b34af0af9a6d14deaf7c1a5315eb%26SignatureVersion%3D1.0%26Timestamp%3D2023-03-13T08%253A34%253A30Z%26Version%3D2014-05-26
計算簽名值。根據
AccessKeySecret=testsecret
計算得到的簽名值如下:9NaGiOspFP5UPcwX8Iwt2YJXXuk=
發起請求。根據介面URL組成規則
[協議][服務地址]?[公用參數][業務請求參數]
擷取完整的請求URL:https://ecs.cn-beijing.aliyuncs.com/?AccessKeyId=testid&Action=DescribeDedicatedHosts&Format=JSON&Signature=9NaGiOspFP5UPcwX8Iwt2YJXXuk%3D&SignatureMethod=HMAC-SHA1&SignatureNonce=edb2b34af0af9a6d14deaf7c1a5315eb&SignatureVersion=1.0&Timestamp=2023-03-13T08%3A34%3A30Z&Version=2014-05-26&RegionId=cn-beijing
您可以使用curl或者wget等工具發起HTTP請求調用
DescribeDedicatedHosts
,查詢一台或多台Dedicated Host的詳細資料。
Java簽名樣本
範例程式碼的運行環境是Java 8,您可能需要根據具體情況對代碼進行相應的調整。
運行Java樣本,需要您在pom.xml中添加以下Maven依賴。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
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 java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
public class Demo {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static final String ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
private static final String ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
public static class SignatureRequest {
public final String httpMethod;
public final String host;
public final String action;
public final String version;
public final String canonicalUri = "/";
public TreeMap<String, Object> headers = new TreeMap<>();
public TreeMap<String, Object> queryParams = new TreeMap<>();
public TreeMap<String, Object> body = new TreeMap<>();
public TreeMap<String, Object> allParams = new TreeMap<>();
public byte[] bodyByte;
public SignatureRequest(String httpMethod, String host, String action, String version) {
this.httpMethod = httpMethod;
this.host = host;
this.action = action;
this.version = version;
setExtendedHeaders();
}
public void setExtendedHeaders() {
headers.put("AccessKeyId", ACCESS_KEY_ID);
headers.put("Format", "JSON");
headers.put("SignatureMethod", "HMAC-SHA1");
headers.put("SignatureVersion", "1.0");
headers.put("SignatureNonce", UUID.randomUUID().toString());
DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "GMT"));
headers.put("Timestamp", DATE_FORMAT.format(new Date()));
headers.put("Action", action);
headers.put("Version", version);
}
public void getAllParams() {
allParams.putAll(headers);
if (!queryParams.isEmpty()) {
allParams.putAll(queryParams);
}
if (!body.isEmpty()) {
allParams.putAll(body);
}
}
}
public static void main(String[] args) throws IOException {
// 樣本一:API請求參數無body
String httpMethod = "POST";
String endpoint = "dysmsapi.aliyuncs.com";
String action = "SendSms";
String version = "2017-05-25";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, endpoint, action, version);
signatureRequest.queryParams.put("PhoneNumbers", "123XXXXXXXX");
signatureRequest.queryParams.put("SignName", "XXXXXXX");
signatureRequest.queryParams.put("TemplateCode", "XXXXXXX");
signatureRequest.queryParams.put("TemplateParam", "XXXXXXX");
/*// 樣本二:API請求參數有body
String httpMethod = "POST";
String endpoint = "mt.aliyuncs.com";
String action = "TranslateGeneral";
String version = "2018-10-12";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, endpoint, action, version);
TreeMap<String, Object> body = new TreeMap<>();
body.put("FormatType", "text");
body.put("SourceLanguage", "zh");
body.put("TargetLanguage", "en");
body.put("SourceText", "你好");
body.put("Scene", "general");
signatureRequest.body = body;
String formDataToString = formDataToString(body);
signatureRequest.bodyByte = formDataToString.getBytes(StandardCharsets.UTF_8);
signatureRequest.headers.put("content-type", "application/x-www-form-urlencoded");*/
/*// 樣本三:API請求參數有body,body為二進位檔案
String httpMethod = "POST";
String endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
String action = "RecognizeGeneral";
String version = "2021-07-07";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, endpoint, action, version);
signatureRequest.bodyByte = Files.readAllBytes(Paths.get("D:\\test.png"));
signatureRequest.headers.put("content-type", "application/octet-stream");*/
// 計算簽名
calculateSignature(signatureRequest);
// 發起請求,驗證簽名是否正確
callApi(signatureRequest);
}
private static void calculateSignature(SignatureRequest signatureRequest) {
// 將header、queryParam、body合成一個map,用於構造正常化請求字串
signatureRequest.getAllParams();
// 擷取正常化請求字串
StringBuilder canonicalQueryString = new StringBuilder();
signatureRequest.allParams.entrySet().stream().map(entry -> percentEncode(entry.getKey()) + "="
+ percentEncode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
if (canonicalQueryString.length() > 0) {
canonicalQueryString.append("&");
}
canonicalQueryString.append(queryPart);
});
System.out.println("canonicalQueryString:" + canonicalQueryString);
// 構造待簽名字串
String stringToSign = signatureRequest.httpMethod + "&" + percentEncode(signatureRequest.canonicalUri) + "&" + percentEncode(String.valueOf(canonicalQueryString));
System.out.println("stringToSign:" + stringToSign);
// 計算簽名
String signature = generateSignature(ACCESS_KEY_SECRET, stringToSign);
System.out.println("signature:" + signature);
signatureRequest.allParams.put("Signature", signature);
}
private static void callApi(SignatureRequest signatureRequest) {
try {
String url = String.format("https://%s/", signatureRequest.host);
URIBuilder uriBuilder = new URIBuilder(url);
for (Map.Entry<String, Object> entry : signatureRequest.allParams.entrySet()) {
uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
}
HttpUriRequest httpRequest;
switch (signatureRequest.httpMethod) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (signatureRequest.bodyByte != null) {
httpPost.setEntity(new ByteArrayEntity(signatureRequest.bodyByte, ContentType.create((String) signatureRequest.headers.get("content-type"))));
}
httpRequest = httpPost;
break;
case "DELETE":
httpRequest = new HttpDelete(uriBuilder.build());
break;
default:
System.out.println("Unsupported HTTP method: " + signatureRequest.httpMethod);
throw new IllegalArgumentException("Unsupported HTTP method");
}
try (CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(httpRequest)) {
String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println(result);
} catch (IOException e) {
System.out.println("Failed to send request");
throw new RuntimeException(e);
}
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
private static String formDataToString(Map<String, Object> formData) {
Map<String, Object> tileMap = new HashMap<>();
processObject(tileMap, "", formData);
StringBuilder result = new StringBuilder();
boolean first = true;
String symbol = "&";
for (Map.Entry<String, Object> entry : tileMap.entrySet()) {
String value = String.valueOf(entry.getValue());
if (value != null && !value.isEmpty()) {
if (first) {
first = false;
} else {
result.append(symbol);
}
result.append(percentEncode(entry.getKey()));
result.append("=");
result.append(percentEncode(value));
}
}
return result.toString();
}
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));
}
}
}
public static String generateSignature(String accessSecret, String stringToSign) {
try {
// 建立HMAC-SHA1密鑰
SecretKeySpec signingKey = new SecretKeySpec((accessSecret + "&").getBytes(StandardCharsets.UTF_8), "HmacSHA1");
// 擷取Mac執行個體並初始化
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
// 計算HMAC-SHA1簽名
byte[] rawHmac = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(rawHmac);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
System.out.println("Failed to generate HMAC-SHA1 signature");
throw new RuntimeException(e);
}
}
public static String percentEncode(String str) {
if (str == null) {
throw new IllegalArgumentException("輸入字串不可為null");
}
try {
return URLEncoder.encode(str, StandardCharsets.UTF_8.name()).replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8編碼不被支援", e);
}
}
}
相關文檔
您可以通過以下文檔詳細瞭解兩種API風格的區別,具體請參見區分ROA風格和RPC風格。