V2版本RPC风格请求体&签名机制

更新时间:

本文将为您介绍如何通过计算签名的方式发起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表示北京时间20180101200000秒。

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查询一台或多台专有宿主机的详细信息为例,根据假设的参数值,展示了签名机制中每个步骤所产生的正确输出内容。您可以在代码中使用本示例提供的假设参数值进行计算,并通过对比您的输出结果与本示例的内容,以验证签名过程的正确性。

所需参数名称

假设的参数值

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,查询一台或多台专有宿主机的详细信息。

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风格