本文為您介紹如何通過ROA風格的自簽名方式發起HTTP請求,調用阿里雲OpenAPI。
不再推薦使用該訪問方式,請移步參考V3版本請求體&簽名機制。
HTTP 要求結構
一個完整的阿里雲 OpenAPI 請求,包含以下部分。
名稱 | 是否必選 | 描述 | 樣本值 |
協議 | 是 | 請求協議,您可以通過調用OpenAPI中繼資料查看API支援的請求協議。若API同時支援 | https:// |
服務地址 | 是 | 即 Endpoint。您可以查閱不同雲產品的服務接入地址文檔,查閱不同服務地區下的服務地址。 | bailian.cn-beijing.aliyuncs.com |
resource_URI_parameters | 是 | 介面URL,包括介面路徑和位置在 path、 query的介面請求參數。 | /{WorkspaceId}/datacenter/category |
RequestHeader | 是 | 要求標頭資訊,通常包含API的版本、Host、Authorization等資訊。後文將詳細說明,請參見RequestHeader(要求標頭) | Accept:application/json Authorization:acs YourAccessKeyId:s8ZMF/eAIvvPJwehLLha0bVNFJ0= Content-MD5:LP54yxk8n7KqF1PPgbJizw== Content-Type:application/json Date:Wed, 16 Apr 2025 03:44:46 GMT Host:bailian.cn-beijing.aliyuncs.com x-acs-signature-method:HMAC-SHA1 x-acs-signature-nonce:ef34aae7-7bd2-413d-a541-680cd2c48538 x-acs-signature-version:1.0 x-acs-version:2023-12-29 |
RequestBody | 是 | 定義在 body 中的業務請求參數,建議您在阿里雲 OpenAPI 開發人員門戶進行試用。 | {"CategoryName":"test","CategoryType":"UNSTRUCTURED"} |
HTTPMethod | 是 | 要求方法,要求方法包括PUT、POST、GET、DELETE。介面的要求方法可通過對應的API文檔擷取。 | POST |
RequestHeader(要求標頭)
一個完整的阿里雲 OpenAPI 請求,包含以下部分。
名稱 | 類型 | 是否必選 | 描述 | 樣本值 |
Accept | String | 否 | 指定介面返回資料的格式,該參數未傳時,以XML形式返回資料。ROA 只支援固定值 | application/json |
Content-MD5 | String | 否 | RequestBody的128-bit MD5散列值轉換成BASE64編碼的結果。 | LP54yxk8n7KqF1PPgbJizw== |
Date | String | 是 | 目前時間戳,有效期間為15分鐘,即產生時間戳記後需要在15分鐘內發起請求。HTTP 1.1協議中規定的 GMT 時間,例如:Tue 9 Apr 2019 07:35:29 GMT。 | Wed, 16 Apr 2025 03:44:46 GMT |
Host | String | 是 | 即服務地址,參見HTTP 要求結構中的服務地址。 | bailian.cn-beijing.aliyuncs.com |
x-acs-signature-method | String | 非匿名請求必須 | 簽名方式。目前為固定值 | HMAC-SHA1 |
x-acs-signature-nonce | String | 是 | 簽名唯一隨機數。用於防止網路重放攻擊,建議您每次請求都使用不同的隨機數。 | ef34aae7-7bd2-413d-a541-680cd2c48538 |
x-acs-signature-version | String | 否 | 簽名版本。目前為固定值 | 1.0 |
x-acs-version | String | 是 | API 版本。您可以訪問阿里雲 OpenAPI 開發人員門戶或者通過OpenAPI中繼資料擷取OpenAPI 對應的 API 版本。 | 2023-12-29 |
Authorization | String | 非匿名請求必須 | 用於驗證請求合法性的認證資訊,格式為 Signature為請求籤名,取值參見簽名機制。 | acs YourAccessKeyId:D9uFJAJgLL+dryjBfQK+YeqGtoY= |
簽名機制
為確保API的安全調用,所有HTTP或HTTPS請求均需在請求中包含簽名資訊。當請求到達阿里雲API Gateway時,網關將根據請求中的參數及要求標頭重新計算簽名,並與請求中包含的簽名資訊進行對比,以驗證要求者的身份,並確保傳輸資料的完整性與安全性。以下是簽名計算的步驟:
請求及返回結果都使用UTF-8字元集進行編碼。
步驟一:構造正常化要求標頭
阿里雲正常化要求標頭(CanonicalizedHeaders)是非標準HTTP頭部資訊。它是指請求中出現的以x-acs-
為首碼的參數資訊,構造方法如下:
將RequestHeader(要求標頭)中以
x-acs-
為首碼的要求標頭名稱轉換成小寫字母,並按照字典順序進行升序排列。對要求標頭的名稱和值分別進行去除首尾空格操作。
在每一個要求標頭後添加一個
\n
分隔字元(包括最後一個要求標頭),然後將所有要求標頭拼接在一起即獲得CanonicalizedHeaders。
樣本值:
x-acs-signature-method:HMAC-SHA1
x-acs-signature-nonce:ef34aae7-7bd2-413d-a541-680cd2c48538
x-acs-signature-version:1.0
x-acs-version:2023-12-29
步驟二:構造正常化資源
正常化資源(CanonicalizedResource) 表示您想要訪問資源的規範描述,具體構造方式如下:
將請求的查詢字串(queryString)中的參數按照參數名稱的字典序重新排序,並以
&
分隔字元串連產生已排序查詢字串。將請求資源路徑(指URL中host與查詢字串之間的部分,包含host之後的
/
但不包含查詢字串前的?
)與已排序查詢字串以?
拼接,得到正常化資源。當查詢字串不存在時,直接用請求資源路徑作為正常化資源。
樣本值:
/llm-p2e4XXXXXXXXsvtn/datacenter/category
步驟三:構造待簽名字串
按照以下虛擬碼構造待簽名字串(stringToSign):
String stringToSign =
HTTPMethod + "\n" +
Accept + "\n" +
ContentMD5 + "\n" +
ContentType + "\n" +
Date + "\n" +
CanonicalizedHeaders +
CanonicalizedResource
參數 | 描述 |
HTTPMethod | 大寫的HTTP方法名,例如POST、GET。 |
Accept | Accept要求標頭的值,當要求標頭不存在時,使用Null 字元串代替。 |
ContentMD5 | Content-MD5要求標頭的值,當要求標頭不存在時,使用Null 字元串代替。 |
ContentType | Content-Type要求標頭的值,當要求標頭不存在時,使用Null 字元串代替。 說明 RequestBody對應的MIME類型。 |
Date | Date要求標頭的值。 |
CanonicalizedHeaders | 步驟一:構造正常化要求標頭中獲得的正常化要求標頭。 |
CanonicalizedResource | 步驟二:構造正常化資源中獲得的正常化資源。 |
樣本值:
POST
application/json
LP54yxk8n7KqF1PPgbJizw==
application/json
Wed, 16 Apr 2025 03:44:46 GMT
x-acs-signature-method:HMAC-SHA1
x-acs-signature-nonce:ef34aae7-7bd2-413d-a541-680cd2c48538
x-acs-signature-version:1.0
x-acs-version:2023-12-29
/llm-p2e4XXXXXXXXsvtn/datacenter/category
步驟四:計算簽名
根據RFC2104的定義,按照HMAC-SHA1演算法對上一步產生的待簽名字串進行簽名計算,並以Base64編碼規則將計算結果編碼成字串,即得到最終的簽名值(signature)。
String signature = Base64(HMAC_SHA1(SigningKey, stringToSign))
計算簽名時使用的SigningKey值就是您的AccessKey Secret。更多資訊,請參見建立AccessKey。
樣本值:
s8ZMF/eAIvvPJwehLLha0bVNFJ0=
步驟五:構造Authorization
計算出簽名結果signature後,按照以下規則拼接字串,並將結果作為Authorization要求標頭的值。
String Authorization = "acs " + AccessKeyId + ":" + signature
樣本值:
acs YourAccessKeyId:s8ZMF/eAIvvPJwehLLha0bVNFJ0=
步驟六:將簽名添加到請求中並發起請求
POST https://bailian.cn-beijing.aliyuncs.com/llm-p2e4XXXXXXXXsvtn/datacenter/category HTTP/1.1
Accept:application/json
Authorization:acs YourAccessKeyId:r8Y9ZqVhTrYGl4nieqk7CW0Pwow=
Content-MD5:LP54yxk8n7KqF1PPgbJizw==
Content-Type:application/json
Date:Wed, 16 Apr 2025 06:47:10 GMT
Host:bailian.cn-beijing.aliyuncs.com
x-acs-signature-method:HMAC-SHA1
x-acs-signature-nonce:e3d8efa7-b1d8-42f3-9733-4fe2691e15dc
x-acs-signature-version:1.0
x-acs-version:2023-12-29
{"CategoryName":"test","CategoryType":"UNSTRUCTURED"}
簽名範例程式碼
Java樣本
範例程式碼的運行環境是JDK1.8,您可能需要根據具體情況對代碼進行相應的調整。
運行Java樣本,需要您在pom.xml中添加以下Maven依賴。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.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 javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
public class SignatureDemo {
public static class SignatureRequest {
private final String httpMethod;
private final String host;
private final String version;
private final String canonicalUri;
public TreeMap<String, String> headers = new TreeMap<>();
public TreeMap<String, Object> queryParams = new TreeMap<>();
public byte[] bodyByteArray;
public SignatureRequest(String httpMethod, String host, String version, String canonicalUri) {
this.httpMethod = httpMethod;
this.host = host;
this.version = version;
this.canonicalUri = canonicalUri;
initHeaders();
}
private void initHeaders() {
headers.put("Host", host);
headers.put("x-acs-version", version);
headers.put("x-acs-signature-version", "1.0");
headers.put("Accept", "application/json");
headers.put("x-acs-signature-nonce", java.util.UUID.randomUUID().toString());
headers.put("Date", getGmtDate());
headers.put("x-acs-signature-method", "HMAC-SHA1");
}
private String getGmtDate() {
SimpleDateFormat gmtFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH);
gmtFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
return gmtFormat.format(new Date());
}
public void setBody(Map<String, String> body, ContentType contentType) {
Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
this.bodyByteArray = gson.toJson(body).getBytes(StandardCharsets.UTF_8);
headers.put("Content-MD5", md5Sum(this.bodyByteArray));
headers.put("Content-Type", contentType.getMimeType());
}
}
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");
private static final String ALGORITHM_NAME = "HmacSHA1";
public static void main(String[] args) {
// 樣本一:POST請求
String method = "POST";
String host = "bailian.cn-beijing.aliyuncs.com";
String version = "2023-12-29";
String canonicalUri = "/llm-p2e4XXXXXXXXsvtn/datacenter/category";
SignatureRequest signatureRequest = new SignatureRequest(method, host, version, canonicalUri);
Map<String, String> body = new HashMap<>();
body.put("CategoryName", "test");
body.put("CategoryType", "UNSTRUCTURED");
System.out.println(new Gson().toJson(body));
signatureRequest.setBody(body, ContentType.APPLICATION_JSON);
/*// 樣本二:GET請求
String method = "GET";
String host = "bailian.cn-beijing.aliyuncs.com";
String version = "2023-12-29";
String canonicalUri = "/llm-p2e4XXXXXXXXsvtn/datacenter/files";
SignatureRequest signatureRequest = new SignatureRequest(method, host, version, canonicalUri);
signatureRequest.queryParams.put("CategoryId", "cate_a946*********************_10045991");*/
/*// 樣本三:DELETE請求
String method = "DELETE";
String host = "bailian.cn-beijing.aliyuncs.com";
String version = "2023-12-29";
String canonicalUri = "/llm-p2e4XXXXXXXXsvtn/datacenter/category/cate_a946*********************_10045991";
SignatureRequest signatureRequest = new SignatureRequest(method, host, version, canonicalUri);*/
// 產生Authorization
generateSignature(signatureRequest);
// 通過HTTPClient發送請求
callApi(signatureRequest);
}
private static void generateSignature(SignatureRequest signatureRequest) {
try {
// 步驟一:構造正常化要求標頭
String canonicalHeaders = buildCanonicalHeaders(signatureRequest);
// 步驟二:構造正常化資源
String canonicalQueryString = buildQueryString(signatureRequest);
// 步驟三:構造正常化請求字串
String stringToSign = buildStringToSign(signatureRequest, canonicalQueryString, canonicalHeaders);
// 步驟四:建構簽章字串
String signature = signString(stringToSign);
// 步驟五:構造Authorization
String authorization = "acs " + ACCESS_KEY_ID + ":" + signature;
signatureRequest.headers.put("Authorization", authorization);
} catch (Exception ex) {
throw new IllegalArgumentException(ex.toString());
}
}
private static void callApi(SignatureRequest signatureRequest) {
try {
// 通過HttpClient發送請求
String url = "https://" + signatureRequest.host + signatureRequest.canonicalUri;
URIBuilder uriBuilder = new URIBuilder(url);
// 添加請求參數
signatureRequest.queryParams.forEach((key, value) -> uriBuilder.addParameter(key, String.valueOf(value)));
HttpUriRequest httpRequest;
switch (signatureRequest.httpMethod) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (signatureRequest.bodyByteArray != null) {
httpPost.setEntity(new ByteArrayEntity(signatureRequest.bodyByteArray, ContentType.create(signatureRequest.headers.get("Content-Type"))));
}
httpRequest = httpPost;
break;
case "PUT":
HttpPut httpPut = new HttpPut(uriBuilder.build());
if (signatureRequest.bodyByteArray != null) {
httpPut.setEntity(new ByteArrayEntity(signatureRequest.bodyByteArray, ContentType.create(signatureRequest.headers.get("Content-Type"))));
}
httpRequest = httpPut;
break;
case "DELETE":
httpRequest = new HttpDelete(uriBuilder.build());
break;
default:
System.out.println("Unsupported HTTP method: " + signatureRequest.httpMethod);
throw new IllegalArgumentException("Unsupported HTTP method");
}
// 添加http要求標頭
signatureRequest.headers.forEach(httpRequest::addHeader);
// 發送請求
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");
throw new IllegalArgumentException(e.toString());
}
} catch (URISyntaxException e) {
// 異常處理
System.out.println("Invalid URI syntax");
throw new IllegalArgumentException(e.toString());
}
}
private static String buildCanonicalHeaders(SignatureRequest signatureRequest) {
StringBuilder canonicalHeaders = new StringBuilder();
signatureRequest.headers.entrySet().stream()
.filter(entry -> entry.getKey().startsWith("x-acs-"))
.forEach(entry -> canonicalHeaders.append(entry.getKey().toLowerCase().trim()).append(":").append(entry.getValue().trim()).append("\n"));
return canonicalHeaders.toString();
}
private static String buildQueryString(SignatureRequest signatureRequest) {
StringBuilder queryBuilder = new StringBuilder(signatureRequest.canonicalUri);
if (!signatureRequest.queryParams.isEmpty()) {
queryBuilder.append("?");
}
for (Map.Entry<String, Object> entry : signatureRequest.queryParams.entrySet()) {
queryBuilder.append(entry.getKey());
String value = (String) entry.getValue();
if (value != null && !value.isEmpty()) {
queryBuilder.append("=").append(value).append("&");
} else {
queryBuilder.append("&");
}
}
String queryString = queryBuilder.toString();
if (queryString.endsWith("&")) {
queryString = queryString.substring(0, queryString.length() - 1);
}
return queryString;
}
private static String buildStringToSign(SignatureRequest signatureRequest, String canonicalQueryString, String canonicalHeaders) {
StringBuilder sb = new StringBuilder();
sb.append(signatureRequest.httpMethod).append("\n");
appendIfPresent(sb, signatureRequest.headers.get("Accept"));
appendIfPresent(sb, signatureRequest.headers.get("Content-MD5"));
appendIfPresent(sb, signatureRequest.headers.get("Content-Type"));
appendIfPresent(sb, signatureRequest.headers.get("Date"));
sb.append(canonicalHeaders);
sb.append(canonicalQueryString);
return sb.toString();
}
private static void appendIfPresent(StringBuilder sb, String value) {
if (value != null) {
sb.append(value).append("\n");
} else {
sb.append("\n");
}
}
private static String signString(String stringToSign) {
try {
Mac mac = Mac.getInstance(ALGORITHM_NAME);
mac.init(new SecretKeySpec(ACCESS_KEY_SECRET.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME));
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return DatatypeConverter.printBase64Binary(signData);
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new IllegalArgumentException(ex.toString());
}
}
public static String md5Sum(byte[] buff) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] md5Bytes = md.digest(buff);
return DatatypeConverter.printBase64Binary(md5Bytes);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalArgumentException(ex.toString());
}
}
}
相關文檔
您可以通過以下文檔詳細瞭解兩種API風格的區別,具體請參見區分ROA風格和RPC風格。