全部產品
Search
文件中心

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

更新時間:Oct 10, 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

參數傳遞介紹

OpenAPI中繼資料中,使用欄位in來定義每個參數的位置,參數的位置將決定參數的傳遞方式。

參數位置

說明

content-type

"in": "query"

查詢參數,它們出現在請求URL末尾的問號(?)後,不同的name=value對由與號(&)分隔。

非必傳。若傳時,值為application/json

"in": "formData"

表單參數,參數需按照key1=value1&key2=value2&key3=value3拼接成字串,並通過請求體(body)傳遞。另外,若請求參數類型是arrayobject時,需將value轉化為帶索引的索引值對,例如object類型值{"key":["value1","value2"]}應轉化為{"key.1":"value1","key.2":"value2"}

必傳,值為content-type=application/x-www-form-urlencoded。

"in": "body"

主體參數,通過請求體(body)傳遞。

必傳,content-type的值與請求內容類型有關。例如:

  • 請求內容類型為JSON資料時,content-type的值為application/json

  • 請求內容類型為二進位檔案流時,content-type的值為application/octet-stream

說明

當請求參數是JSON字串類型時,JSON字串中的參數順序不會影響簽名計算。

簽名機制

為了確保 API 的安全性,每個請求都需通過簽名(Signature)進行身分識別驗證。以下是簽名計算的步驟:

步驟一:構造正常化請求字串

1、將公用請求參數介面自訂請求參數合并,並將合并後的參數按照參數首字母的字典順序進行排序,排序時不包括公用請求參數中的Signature參數。 虛擬碼如下:

// 合并公用請求參數和介面自訂參數,並根據key排序
params = merged(publicParams,apiReuqestParams)
sortParams = sorted(params.keys())

2、採用UTF-8字元集,按照RFC3986規範對sortParams的鍵和值進行編碼,並使用等號(=)將鍵和值串連。

編碼規則:

  • 字元 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 對應的密鑰 AccessKeySecret,使用 HMAC-SHA1的簽名演算法,計算待簽名字串StringToSign的簽名。虛擬碼如下:

signature = Base64(HMAC_SHA1(AccessKeySecret + "&", UTF_8_Encoding_Of(stringToSign)))

其中

  • Base64() 為編碼計算函數。

  • HMAC_SHA1() 為 HMAC_SHA1 簽名函數,傳回值為 HMAC_SHA1 加密後原始位元組,而非16進位字串。

  • UTF_8_Encoding_Of() 是 UTF-8 字元編碼函數。

步驟四:將signature添加到URL

將signature的值按照RFC3986規範編碼之後添加到介面URL上。虛擬碼如下:

https://服務地址/?sortParams.key1=sortParams.value1&sortParams.key2=sortParams.value2&...&sortParams.keyN=sortParams.valueN&Signature=signature

簽名範例程式碼

固定參數樣本

本樣本以調用 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 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());
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
            format.setTimeZone(new SimpleTimeZone(0, "GMT"));
            headers.put("Timestamp", 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;
                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);
        }
    }
}

Python樣本

說明

範例程式碼的運行環境是Python 3.12.3,您可能需要根據具體情況對代碼進行相應的調整。

運行範例程式碼需要安裝requests庫:

pip install requests
import base64
import hashlib
import hmac
import os
import urllib.parse
import uuid
from collections import OrderedDict
from datetime import datetime, UTC
from typing import Dict, Any

import requests

# 從環境變數中擷取存取金鑰
ACCESS_KEY_ID = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID")
ACCESS_KEY_SECRET = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET")


class SignatureRequest:
    """
    簽章要求類,用於構建和管理RPC API請求
    """

    def __init__(self, http_method: str, host: str, action: str, version: str):
        """
        初始化簽章要求對象

        Args:
            http_method: HTTP要求方法 (GET/POST等)
            host: API服務網域名稱
            action: API操作名稱
            version: API版本號碼
        """
        self.http_method = http_method.upper()
        self.host = host
        self.action = action
        self.version = version
        self.canonical_uri = "/"  # RPC介面統一使用根路徑
        self.headers: Dict[str, Any] = OrderedDict()  # 要求標頭參數
        self.query_params: Dict[str, Any] = OrderedDict()  # 查詢參數
        self.body: Dict[str, Any] = OrderedDict()  # 請求體參數
        self.body_byte: bytes = b""  # 請求體位元組資料
        self.all_params: Dict[str, Any] = OrderedDict()  # 所有參數集合
        self.set_headers()

    def set_headers(self) -> None:
        """
        設定RPC請求必需的基礎要求標頭參數
        """
        self.headers["AccessKeyId"] = ACCESS_KEY_ID  # 存取金鑰ID
        self.headers["Format"] = "JSON"  # 響應格式
        self.headers["SignatureMethod"] = "HMAC-SHA1"  # 簽名演算法
        self.headers["SignatureVersion"] = "1.0"  # 簽名版本
        self.headers["SignatureNonce"] = "{" + str(uuid.uuid4()) + "}"  # 隨機防重放字串
        self.headers["Timestamp"] = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")  # 時間戳記
        self.headers["Action"] = self.action  # API名稱
        self.headers["Version"] = self.version  # API版本號碼

    def set_content_type(self, content_type):
        self.headers["Content-Type"] = content_type

    def get_all_params(self) -> None:
        """
        收集並排序所有請求參數
        """
        # 合并所有參數:headers、query_params、body
        self.all_params.update(self.headers)
        if self.query_params:
            self.all_params.update(self.query_params)
        if self.body:
            self.body_byte = form_data_to_string(body).encode("utf-8")
            self.all_params.update(self.body)

        # 按參數名ASCII碼順序排序
        self.all_params = OrderedDict(sorted(self.all_params.items()))


def calculate_signature(signature_request: SignatureRequest) -> None:
    """
    計算RPC請求的簽名

    Args:
        signature_request: 簽章要求對象
    """
    signature_request.get_all_params()  # 收集並排序所有參數

    # 構建規範查詢字串
    canonical_query_string = "&".join(
        f"{percent_encode(k)}={percent_encode(v)}"
        for k, v in signature_request.all_params.items()
    )
    print(f"canonicalQueryString:{canonical_query_string}")

    # 構建待簽名字串:HTTP方法 + 規範URI + 規範查詢字串
    string_to_sign = (
        f"{signature_request.http_method}&"
        f"{percent_encode(signature_request.canonical_uri)}&"
        f"{percent_encode(canonical_query_string)}"
    )
    print(f"stringToSign:{string_to_sign}")

    # 產生簽名
    signature = generate_signature(ACCESS_KEY_SECRET, string_to_sign)
    signature_request.all_params["Signature"] = signature  # 將簽名添加到參數中


def form_data_to_string(form_data: Dict[str, Any]) -> str:
    """
    將表單資料轉換為URL編碼字串

    Args:
        form_data: 表單資料字典

    Returns:
        URL編碼後的字串
    """
    tile_map: Dict[str, Any] = {}

    def process_object(key: str, value: Any) -> None:
        """
        遞迴處理對象,將嵌套結構扁平化

        Args:
            key: 參數鍵名
            value: 參數值
        """
        if value is None:
            return
        if isinstance(value, list):
            # 處理清單類型參數
            for i, item in enumerate(value):
                process_object(f"{key}.{i + 1}", item)
        elif isinstance(value, dict):
            # 處理字典型別參數
            for k, v in value.items():
                process_object(f"{key}.{k}", v)
        else:
            # 移除開頭的點
            clean_key = key[1:] if key.startswith(".") else key
            # 處理位元組資料和普通資料
            tile_map[clean_key] = value.decode("utf-8") if isinstance(value, bytes) else str(value)

    # 處理所有表單資料
    for k, v in form_data.items():
        process_object(k, v)

    # URL編碼並拼接
    encoded_items = [
        f"{percent_encode(k)}={percent_encode(v)}"
        for k, v in tile_map.items() if v
    ]

    return "&".join(encoded_items)


def generate_signature(access_secret: str, string_to_sign: str) -> str:
    """
    使用HMAC-SHA1演算法產生簽名

    Args:
        access_secret: 存取金鑰
        string_to_sign: 待簽名字串

    Returns:
        Base64編碼的簽名結果
    """
    try:
        # 簽名密鑰 = 密鑰 + "&"
        signing_key = (access_secret + "&").encode("utf-8")
        # 使用HMAC-SHA1演算法計算簽名
        signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha1).digest()
        # Base64編碼簽名結果
        return base64.b64encode(signature).decode("utf-8")
    except Exception as e:
        print(f"Failed to generate HMAC-SHA1 signature: {e}")
        raise


def percent_encode(s: str) -> str:
    """
    對字串進行百分比符號編碼(RFC 3986標準)

    Args:
        s: 待編碼字串

    Returns:
        編碼後的字串
    """
    if s is None:
        raise ValueError("Input string cannot be None")
    # 使用UTF-8編碼後進行URL編碼,保留~字元
    encoded = urllib.parse.quote(s.encode("utf-8"), safe=b"~")
    # 替換特殊字元編碼
    return encoded.replace("+", "%20").replace("*", "%2A")


def call_api(signature_request: SignatureRequest) -> None:
    """
    發起API請求樣本
    """
    url = f"https://{signature_request.host}/"

    # 構造請求參數
    params = {k: str(v) for k, v in signature_request.all_params.items()}

    # 準備請求參數
    request_kwargs = {
        "params": params
    }

    # 添加請求體資料(如果存在)
    if signature_request.body_byte:
        request_kwargs["data"] = signature_request.body_byte
        headers = {"Content-Type": signature_request.headers.get("Content-Type")}
        request_kwargs["headers"] = headers

    try:
        # 使用requests.request統一處理不同HTTP方法
        response = requests.request(
            method=signature_request.http_method,
            url=url,
            **request_kwargs
        )

        print(f"Request URL: {response.url}")
        print(f"Response: {response.text}")

    except requests.RequestException as e:
        print(f"HTTP request failed: {e}")
        raise
    except Exception as e:
        print(f"Failed to send request: {e}")
        raise


if __name__ == "__main__":
    # 樣本一:無body的請求,content-type非必傳,若傳請使用application/json
    signature_request = SignatureRequest(
        http_method="POST",
        host="dysmsapi.aliyuncs.com",
        action="SendSms",
        version="2017-05-25"
    )
    # query_params用於配置查詢參數
    signature_request.query_params["SignName"] = "******"
    signature_request.query_params["TemplateCode"] = "SMS_******"
    signature_request.query_params["PhoneNumbers"] = "******"
    signature_request.query_params["TemplateParam"] = "{'code':'1234'}"

    # 樣本二:帶body的請求,content-type為application/x-www-form-urlencoded,禁止使用application/json
    """
    signature_request = SignatureRequest(
        http_method="POST",
        host="mt.aliyuncs.com",
        action="TranslateGeneral",
        version="2018-10-12"
    )
    body = {
        "FormatType": "text",
        "SourceLanguage": "zh",
        "TargetLanguage": "en",
        "SourceText": "你好",
        "Scene": "general"
    }
    signature_request.body = body
    signature_request.set_content_type("application/x-www-form-urlencoded")
    """

    # 樣本三:上傳二進位檔案流,content-type為application/octet-stream
    """
    signature_request = SignatureRequest(
        http_method="POST",
        host="ocr-api.cn-hangzhou.aliyuncs.com",
        action="RecognizeGeneral",
        version="2021-07-07"
    )
    with open("D:\\test.jpeg", "rb") as f:
        signature_request.body_byte = f.read()
    signature_request.set_content_type("application/octet-stream")
    """

    # 計算簽名
    calculate_signature(signature_request)

    # 發起請求樣本
    call_api(signature_request)

相關文檔

您可以通過以下文檔詳細瞭解兩種API風格的區別,具體請參見區分ROA風格和RPC風格