全部產品
Search
文件中心

Alibaba Cloud SDK:V2版本RPC風格請求體&簽名機制

更新時間:May 16, 2025

本文將為您介紹如何通過自簽名方式發起HTTP請求,調用阿里雲RPC風格的OpenAPI。

重要

不再推薦使用該訪問方式,請移步參考V3版本請求體&簽名機制

HTTP 要求結構

一個完整的阿里雲 RPC 請求由以下部分組成:

名稱

是否必選

描述

樣本值

協議

請求協議,您可以在OpenAPI中繼資料中查看API支援的請求協議。若API同時支援HTTPHTTPS時,為了確保更高的安全性,建議您使用HTTPS協議發送請求。

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表示北京時間2018年01月01日20點00分00秒。

2018-01-01T12:00:00Z

SignatureMethod

String

簽名方式。目前為固定值 HMAC-SHA1

HMAC-SHA1

SignatureVersion

String

簽名演算法版本。目前為固定值 1.0

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

簽名流程如下:

  1. 構造正常化請求字串。

    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
  2. 構造待簽名字串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
  3. 計算簽名值。根據 AccessKeySecret=testsecret計算得到的簽名值如下:

    9NaGiOspFP5UPcwX8Iwt2YJXXuk=
  4. 發起請求。根據介面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風格