本文将通过介绍V3版本的签名机制,帮助您实现直接使用HTTP请求调用阿里云OpenAPI。
在OpenAPI门户上提供了阿里云SDK的相关产品,这些产品所提供的API支持使用V3版本签名。如果您当前使用的是V2版本签名进行API调用,也可以直接替换为V3版本签名以进行调用。
HTTP 请求结构
一个完整的阿里云OpenAPI请求,包含以下部分。
名称 | 是否必选 | 描述 | 示例值 |
协议 | 是 | 您可以查阅不同云产品的 API 参考文档进行配置。支持通过 | https:// |
服务地址 | 是 | 即 Endpoint。您可以查阅不同云产品的服务接入地址文档,查阅不同服务区域下的服务地址。 | ecs.cn-shanghai.aliyuncs.com |
resource_URI_parameters | 是 | 接口URL,包括接口路径和位置在 path、 query的接口请求参数。 | ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai |
RequestHeader | 是 | 公共请求头信息,通常包含API的版本、Host、Authorization等信息。后文将详细说明。 | Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0 x-acs-action: RunInstances host: ecs.cn-shanghai.aliyuncs.com x-acs-date: 2023-10-26T09:01:01Z x-acs-version: 2014-05-26 x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0 |
RequestBody | 是 | 定义在body中的业务请求参数,可通过OpenAPI元数据获取。 | |
HTTPMethod | 是 | 请求方法,可通过OpenAPI元数据获取。 | POST |
RequestHeader
调用阿里云OpenAPI时,公共请求头需要包含如下信息。
名称 | 类型 | 是否必选 | 描述 | 示例值 |
host | String | 是 | 即服务地址,参见HTTP 请求结构。 | ecs.cn-shanghai.aliyuncs.com |
x-acs-action | String | 是 | API的名称。您可以访问阿里云 OpenAPI 开发者门户,搜索您想调用的 OpenAPI。 | RunInstances |
x-acs-content-sha256 | String | 是 | 请求正文Hash摘要后再base-16编码的结果,与HashedRequestPayload一致。 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
x-acs-date | String | 是 | 按照ISO 8601标准表示的UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ,例如2018-01-01T12:00:00Z。值为请求发出前15分钟内的时间。 | 2023-10-26T10:22:32Z |
x-acs-signature-nonce | String | 是 | 签名唯一随机数。该随机数用于防止网络重放攻击,每一次请求都必须使用不同的随机数。 | 3156853299f313e23d1673dc12e1703d |
x-acs-version | String | 是 | API 版本。您可以访问阿里云 OpenAPI 开发者门户,查看您调用OpenAPI对应的 API 版本。 | 2014-05-26 |
Authorization | String | 非匿名请求必须 | 用于验证请求合法性的认证信息,格式为Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature。 其中SignatureAlgorithm为签名加密方式,为ACS3-HMAC-SHA256。 Credential 为用户的访问密钥ID。您可以在RAM 控制台查看您的 AccessKeyId。如需创建 AccessKey,请参见创建AccessKey。 SignedHeaders为请求头中包含的参与签名字段键名,【说明】:除了Authorization之外,建议对所有公共请求头添加签名,以提高安全性。 Signature为请求签名,取值参见签名机制。 | ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0 |
x-acs-security-token | String | STS认证必传 | 为调用AssumeRole接口返回值中SecurityToken的值。 |
签名机制
采用AK/SK方式进行签名与认证。对于每一次HTTP或HTTPS协议请求,阿里云API网关将依据请求参数信息重新计算签名,通过对比该签名与请求中提供的签名是否一致,从而验证请求者的身份,以确保传输数据的完整性与安全性。
请求及返回结果都使用UTF-8字符集进行编码。
步骤一:构造规范化请求
构造规范化请求(CanonicalRequest)的伪代码如下:
CanonicalRequest =
HTTPRequestMethod + '\n' + // http请求方法,全大写
CanonicalURI + '\n' + // 规范化URI
CanonicalQueryString + '\n' + // 规范化查询字符串
CanonicalHeaders + '\n' + // 规范化消息头
SignedHeaders + '\n' + // 已签名消息头
HashedRequestPayload // 请求体(body)的hash值
HTTPRequestMethod(请求方法)
即大写的HTTP方法名,如GET、POST。
CanonicalURI(规范化URI)
URL的资源路径部分经过编码之后的结果。资源路径部分指URL中host与查询字符串之间的部分,包含host之后的
/
但不包含查询字符串前的?
。用户发起请求时的URI应使用规范化URI,编码方式使用UTF-8字符集按照RFC3986的规则对URI中的每一部分(即被/
分割开的字符串)进行编码:字符A~Z、a~z、0~9以及字符
-
、_
、.
、~
不编码。其他字符编码成
%
加字符对应ASCII码的16进制。示例:半角双引号("
)对应%22
。需要注意的是,部分特殊字符需要特殊处理,具体如下:编码前
编码后
空格( )
%20
星号(
*
)%2A
%7E
波浪号(
~
)如果您使用的是Java标准库中的
java.net.URLEncoder
,可以先用标准库中encode
编码,随后将编码后的字符中加号(+
)替换为%20
、星号(*
)替换为%2A
、%7E
替换为波浪号(~
),即可得到上述规则描述的编码字符串。
重要RPC风格API使用正斜杠(
/
)作为CanonicalURI,ROA风格API该参数为OpenAPI元数据中
path
的值,例如/api/v1/clusters
。CanonicalQueryString(规范化查询字符串)
在OpenAPI元数据中,如果API的请求参数信息包含了
"in":"query"
时,需要将这请求参数按照如下构造方法拼接起来:将查询字符串中的参数按照参数名的字符顺序升序排列。
使用UTF-8字符集按照RFC3986的规则对每个参数的参数名和参数值分别进行URI编码,具体规则与上一节中的CanonicalURI编码规则相同。
使用等号(
=
)连接编码后的请求参数名和参数值,对于没有值的参数使用空字符串。多个请求参数之间使用与号(
&
)连接。
重要当请求的查询字符串为空时,使用空字符串作为规范化查询字符串。
示例值:
ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
HashedRequestPayload
使用哈希函数对RequestPayload进行转换得到HashedRequestPayload,并将RequestHeader中
x-acs-content-sha256
的值修改为HashedRequestPayload的值。伪代码如下:HashedRequestPayload = HexEncode(Hash(RequestPayload))
RequestPayload的值为请求体(body)对应的JSON字符串。在OpenAPI元数据中,如果API的请求参数信息包含了
"in": "body"
或"in": "formData"
时,需通过请求体(body)传递参数。若没有请求体(body)时,RequestPayload的值固定为一个空字符串。重要当请求参数类型是集合(Array)、映射(map)时,需要将参数平铺。例如
{"key":["value1","value2"]}
平铺后为{"key.2":"value2","key.1":"value1"}
。当请求参数信息包含
"in": "formData"
时,需要将这类参数按照固定格式拼接为一个字符串,拼接格式为:key1=value1&key2=value2&key3=value3
。同时需要在RequestHeader中添加content-type,content-type的值为application/x-www-form-urlencoded
。当请求参数信息包含
"in": "body"
时,需要在RequestHeader中添加content-type,content-type的值与请求内容类型有关。例如:请求内容类型为JSON数据时,content-type的值为
application/json
。请求内容类型为二进制文件流时,content-type的值为
application/octet-stream
。
Hash表示消息摘要函数,目前仅支持SHA256算法。
HexEncode表示以小写的十六进制的形式返回摘要的编码函数(即Base16编码)。
当请求体(body)为空时的示例值:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
CanonicalHeaders(规范化请求头)
将RequestHeader中的参数按照如下构造方法拼接起来:
过滤出RequestHeader中包含以
x-acs-
为前缀、host
、content-type
的参数。将参数的名称转换为小写,并按照字符顺序升序排列。
将参数的值去除首尾空格。
将参数名和参数值以英文冒号(
:
)连接,并在尾部添加换行符(\n
),组成一个规范化消息头(CanonicalHeaderEntry)。将多个规范化消息头(CanonicalHeaderEntry)拼接成一个字符串。
说明除Authorization外的所有RequestHeader,只要符合要求都必须被加入签名。
伪代码如下:
CanonicalHeaderEntry = Lowercase(HeaderName) + ':' + Trim(HeaderValue) + '\n' CanonicalHeaders = CanonicalHeaderEntry0 + CanonicalHeaderEntry1 + ... + CanonicalHeaderEntryN
示例值:
host:ecs.cn-shanghai.aliyuncs.com x-acs-action:RunInstances x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 x-acs-date:2023-10-26T10:22:32Z x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d x-acs-version:2014-05-26
SignedHeaders(已签名消息头列表)
用于说明此次请求中参与签名的公共请求头信息,与CanonicalHeaders中的参数名一一对应。其构造方法如下:
将CanonicalHeaders中包含的请求头的名称转为小写。
按首字母升序排列并以英文分号(
;
)分隔。
伪代码如下:
SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ... + Lowercase(HeaderNameN)
示例值:
host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version
步骤二:构造待签名字符串
按照以下伪代码构造待签名字符串(stringToSign):
StringToSign =
SignatureAlgorithm + '\n' +
HashedCanonicalRequest
SignatureAlgorithm
签名协议目前仅支持ACS3-HMAC-SHA256算法。
HashedCanonicalRequest
规范化请求摘要串,计算方法伪代码如下:
HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
Hash表示消息摘要函数,目前仅支持SHA256算法。
HexEncode表示以小写的十六进制的形式返回摘要的编码函数(即Base16编码)。
示例值:
ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
步骤三:计算签名
按照以下伪代码计算签名值(Signature)。
Signature = HexEncode(SignatureMethod(Secret, StringToSign))
StringToSign:步骤二中构造的待签名字符串,UTF-8编码。
SignatureMethod:使用HMAC-SHA256作为签名算法。
Secret:AccessKey Secret。
HexEncode:以小写的十六进制的形式返回摘要的编码函数(即Base16编码)。
示例值:
06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
步骤四:将签名添加到请求中
计算完签名后,构造Authorization请求头,格式为:Authorization:<SignatureAlgorithm> Credential=<AccessKeyId>,SignedHeaders=<SignedHeaders>,Signature=<Signature>
。
示例值:
ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
签名示例
为了让您更清晰地理解上述签名机制,下面以主流编程语言为例,将签名机制完整实现。示例代码只是让您更好地理解签名机制,存在不通用性,阿里云OpenAPI提供多种编程语言和开发框架的SDK,使用这些SDK可以免去签名过程,便于您快速构建与阿里云相关的应用程序,建议您使用SDK。
在签名之前,请务必要先查看OpenAPI元数据,获取API的请求方式、请求参数名称、请求参数类型以及参数如何传等信息!否则,签名极有可能会失败!
固定参数示例
本示例是以假设的参数值为例,展示了签名机制中每个步骤所产生的正确输出内容。您可以在代码中使用本示例提供的假设参数值进行计算,并通过对比您的输出结果与本示例的内容,以验证签名过程的正确性。
所需参数名称 | 假设的参数值 |
AccessKeyID | YourAccessKeyId |
AccessKeySecret | YourAccessKeySecret |
x-acs-signature-nonce | 3156853299f313e23d1673dc12e1703d |
x-acs-date | 2023-10-26T10:22:32Z |
x-acs-action | RunInstances |
x-acs-version | 2014-05-26 |
host | ecs.cn-shanghai.aliyuncs.com |
API请求参数:
ImageId | win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd |
RegionId | cn-shanghai |
签名流程如下:
构造规范化请求。
POST
/
ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
host:ecs.cn-shanghai.aliyuncs.com
x-acs-action:RunInstances
x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date:2023-10-26T10:22:32Z
x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
x-acs-version:2014-05-26
host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
构造待签名字符串。
ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
计算签名。
06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
将签名添加到请求中。
POST /?ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai HTTP/1.1
Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
x-acs-action: RunInstances
host: ecs.cn-shanghai.aliyuncs.com
x-acs-date: 2023-10-26T09:01:01Z
x-acs-version: 2014-05-26
x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0
user-agent: AlibabaCloud (Mac OS X; x86_64) Java/1.8.0_352-b08 tea-util/0.2.6 TeaDSL/1
accept: application/json
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.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.ContentType;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
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.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* signature demo
*/
public class SignatureDemo {
/**
* 日期格式化工具,用于将日期时间字符串格式化为"yyyy-MM-dd'T'HH:mm:ss'Z'"的格式。
*/
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static class SignatureRequest {
// HTTP Method
private final String httpMethod;
// 请求路径,当资源路径为空时,使用正斜杠(/)作为CanonicalURI
private final String canonicalUri;
// endpoint
private final String host;
// API name
private final String xAcsAction;
// API version
private final String xAcsVersion;
// headers
TreeMap<String, String> headers = new TreeMap<>();
// body参数对应的字节数组,请求参数在元数据中显示"in":"body"或"in": "formData",表示参数放在body中
byte[] body;
// query参数,请求参数在元数据中显示"in":"query",表示参数拼接在请求URL上
TreeMap<String, Object> queryParam = new TreeMap<>();
public SignatureRequest(String httpMethod, String canonicalUri, String host, String xAcsAction, String xAcsVersion) {
this.httpMethod = httpMethod;
this.canonicalUri = canonicalUri;
this.host = host;
this.xAcsAction = xAcsAction;
this.xAcsVersion = xAcsVersion;
initHeader();
}
// init headers
private void initHeader() {
headers.put("host", host);
headers.put("x-acs-action", xAcsAction);
headers.put("x-acs-version", xAcsVersion);
SDF.setTimeZone(new SimpleTimeZone(0, "GMT")); // 设置日期格式化时区为GMT
headers.put("x-acs-date", SDF.format(new Date()));
headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
}
}
/**
* System.getenv()表示通过环境变量获取Access Key ID和Access Key Secret。
*/
private final static String ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
private final static String ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
/**
* 签名协议
*/
private static final String ALGORITHM = "ACS3-HMAC-SHA256";
/**
* 签名示例,您需要根据实际情况替换main方法中的示例参数。
* ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
* <p>
* 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
* 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
* 2. 请求参数在元数据中显示"in": "body",通过body传参。
* 3. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
public static void main(String[] args) throws IOException {
// RPC接口请求示例一:请求参数"in":"query"
String httpMethod = "POST"; // 请求方式,从元数据中可以获取,建议使用POST。
String canonicalUri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
String host = "ecs.cn-hangzhou.aliyuncs.com"; // 云产品服务接入点
String xAcsAction = "DescribeInstanceStatus"; // API名称
String xAcsVersion = "2014-05-26"; // API版本号
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// DescribeInstanceStatus请求参数如下:
// RegionId在元数据中显示的类型是String,"in":"query",必填
signatureRequest.queryParam.put("RegionId", "cn-hangzhou");
// InstanceId的在元数据中显示的类型是array,"in":"query",非必填
String[] instanceIds = {"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"};
signatureRequest.queryParam.put("InstanceId", Arrays.asList(instanceIds));
/*// RPC接口请求示例二:请求参数"in":"body"
String httpMethod = "POST";
String canonicalUri = "/";
String host = "ocr-api.cn-hangzhou.aliyuncs.com";
String xAcsAction = "RecognizeGeneral";
String xAcsVersion = "2021-07-07";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// 请求参数在元数据中显示"in": "body",通过body传参。
signatureRequest.body = Files.readAllBytes(Paths.get("D:\\test.png"));
signatureRequest.headers.put("content-type", "application/octet-stream");*/
/*// RPC接口请求示例三:请求参数"in": "formData"
String httpMethod = "POST";
String canonicalUri = "/";
String host = "mt.aliyuncs.com";
String xAcsAction = "TranslateGeneral";
String xAcsVersion = "2018-10-12";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// TranslateGeneral请求参数如下:
// Context在元数据中显示的类型是String,"in":"query",非必填
signatureRequest.queryParam.put("Context", "早上");
// FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
Map<String, Object> body = new HashMap<>();
body.put("FormatType", "text");
body.put("SourceLanguage", "zh");
body.put("TargetLanguage", "en");
body.put("SourceText", "你好");
body.put("Scene", "general");
String formDataToString = formDataToString(body);
signatureRequest.body = formDataToString.getBytes(StandardCharsets.UTF_8);
signatureRequest.headers.put("content-type", "application/x-www-form-urlencoded");*/
/*// ROA接口POST请求
String httpMethod = "POST";
String canonicalUri = "/clusters"; // 从元数据中获取:"path": "/clusters"
String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
String xAcsAction= "CreateCluster"; // API名称
String xAcsVersion= "2015-12-15"; // API版本号
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// 调用API所需要的参数,请求参数在元数据中显示"in": "body",表示参数放在body中
TreeMap<String, Object> body = new TreeMap<>();
body.put("name", "测试");
body.put("region_id", "cn-beijing");
body.put("cluster_type", "ExternalKubernetes");
body.put("vpcid", "vpc-2zeou1uod4ylaXXXXXXXX");
body.put("container_cidr","10.0.0.0/8");
body.put("service_cidr", "10.2.0.0/24");
body.put("security_group_id", "sg-2ze1a0rlgeo7XXXXXXXX");
body.put("vswitch_ids", Collections.singletonList(
"vsw-2zei30dhfldu8XXXXXXXX"
));
Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
signatureRequest.body = gson.toJson(body).getBytes(StandardCharsets.UTF_8);
signatureRequest.headers.put("content-type", "application/json");*/
/*// ROA接口GET请求
String httpMethod = "GET";
// canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
String canonicalUri = "/clusters/" + percentCode("cdb14b4f85130407da748fd3fXXXXXXXX") + "/resources";
String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
String xAcsAction = "DescribeClusterResources"; // API名称
String xAcsVersion = "2015-12-15"; // API版本号
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
signatureRequest.queryParam.put("with_addon_resources", true);*/
/*// ROA接口DELETE请求
String httpMethod = "DELETE";
String canonicalUri = "/clusters/" + percentCode("cdb14b4f85130407da748fd3fXXXXXXXX");
String host = "cs.cn-beijing.aliyuncs.com";
String xAcsAction = "DeleteCluster";
String xAcsVersion = "2015-12-15";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);*/
// 签名过程
getAuthorization(signatureRequest);
// 调用API
callApi(signatureRequest);
}
private static void callApi(SignatureRequest signatureRequest) {
try {
// 通过HttpClient发送请求
String url = "https://" + signatureRequest.host + signatureRequest.canonicalUri;
URIBuilder uriBuilder = new URIBuilder(url);
// 添加请求参数
for (Map.Entry<String, Object> entry : signatureRequest.queryParam.entrySet()) {
uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
}
System.out.println(uriBuilder.build());
HttpUriRequest httpRequest;
switch (signatureRequest.httpMethod) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (signatureRequest.body != null) {
httpPost.setEntity(new ByteArrayEntity(signatureRequest.body, ContentType.create(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");
}
// 添加http请求头
for (Map.Entry<String, String> entry : signatureRequest.headers.entrySet()) {
httpRequest.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
}
// 发送请求
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");
e.printStackTrace();
}
} catch (URISyntaxException e) {
// 异常处理
System.out.println("Invalid URI syntax");
e.printStackTrace();
}
}
/**
* 该方法用于根据传入的HTTP请求方法、规范化的URI、查询参数等,计算并生成授权信息。
*/
private static void getAuthorization(SignatureRequest signatureRequest) {
try {
// 处理queryParam中参数值为List、Map类型的参数,将参数平铺
TreeMap<String, Object> newQueryParam = new TreeMap<>();
processObject(newQueryParam, "", signatureRequest.queryParam);
signatureRequest.queryParam = newQueryParam;
// 步骤 1:拼接规范请求串
// 请求参数,当请求的查询字符串为空时,使用空字符串作为规范化查询字符串
StringBuilder canonicalQueryString = new StringBuilder();
signatureRequest.queryParam.entrySet().stream().map(entry -> percentCode(entry.getKey()) + "="
+ percentCode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
// 如果canonicalQueryString已经不是空的,则在查询参数前添加"&"
if (canonicalQueryString.length() > 0) {
canonicalQueryString.append("&");
}
canonicalQueryString.append(queryPart);
});
// 计算请求体的哈希值
String requestPayload = ""; // 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
String hashedRequestPayload = signatureRequest.body != null ? sha256Hex(signatureRequest.body) : sha256Hex(requestPayload.getBytes(StandardCharsets.UTF_8));
signatureRequest.headers.put("x-acs-content-sha256", hashedRequestPayload);
// 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
StringBuilder canonicalHeaders = new StringBuilder();
// 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
StringBuilder signedHeadersSb = new StringBuilder();
signatureRequest.headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") || "host".equalsIgnoreCase(entry.getKey()) || "content-type".equalsIgnoreCase(entry.getKey())).sorted(Map.Entry.comparingByKey()).forEach(entry -> {
String lowerKey = entry.getKey().toLowerCase();
String value = String.valueOf(entry.getValue()).trim();
canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
signedHeadersSb.append(lowerKey).append(";");
});
String signedHeaders = signedHeadersSb.substring(0, signedHeadersSb.length() - 1);
String canonicalRequest = signatureRequest.httpMethod + "\n" + signatureRequest.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
System.out.println("canonicalRequest=========>\n" + canonicalRequest);
// 步骤 2:拼接待签名字符串
String hashedCanonicalRequest = sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8)); // 计算规范化请求的哈希值
String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
System.out.println("stringToSign=========>\n" + stringToSign);
// 步骤 3:计算签名
String signature = DatatypeConverter.printHexBinary(hmac256(ACCESS_KEY_SECRET.getBytes(StandardCharsets.UTF_8), stringToSign)).toLowerCase();
System.out.println("signature=========>" + signature);
// 步骤 4:拼接 Authorization
String authorization = ALGORITHM + " " + "Credential=" + ACCESS_KEY_ID + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature;
System.out.println("authorization=========>" + authorization);
signatureRequest.headers.put("Authorization", authorization);
} catch (Exception e) {
// 异常处理
System.out.println("Failed to get authorization");
e.printStackTrace();
}
}
/**
* 处理请求参数类型为formData的参数。
*
* @param formData formData类型参数
* @return String
*/
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(percentCode(entry.getKey()));
result.append("=");
result.append(percentCode(value));
}
}
return result.toString();
}
/**
* 递归处理对象,将复杂对象(如Map和List)展开为平面的键值对
*
* @param map 原始的键值对集合,将被递归地更新
* @param key 当前处理的键,随着递归的深入,键会带有嵌套路径信息
* @param value 对应于键的值,可以是嵌套的Map、List或其他类型
*/
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));
}
}
}
/**
* 使用HmacSHA256算法生成消息认证码(MAC)。
*
* @param secretKey 密钥,用于生成MAC的密钥,必须保密。
* @param str 需要进行MAC认证的消息。
* @return 返回使用HmacSHA256算法计算出的消息认证码。
* @throws Exception 如果初始化MAC或计算MAC过程中遇到错误,则抛出异常。
*/
public static byte[] hmac256(byte[] secretKey, String str) throws Exception {
// 实例化HmacSHA256消息认证码生成器
Mac mac = Mac.getInstance("HmacSHA256");
// 创建密钥规范,用于初始化MAC生成器
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, mac.getAlgorithm());
// 初始化MAC生成器
mac.init(secretKeySpec);
// 计算消息认证码并返回
return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
}
/**
* 使用SHA-256算法计算字符串的哈希值并以十六进制字符串形式返回。
*
* @param input 需要进行SHA-256哈希计算的字节数组。
* @return 计算结果为小写十六进制字符串。
* @throws Exception 如果在获取SHA-256消息摘要实例时发生错误。
*/
public static String sha256Hex(byte[] input) throws Exception {
// 获取SHA-256消息摘要实例
MessageDigest md = MessageDigest.getInstance("SHA-256");
// 计算字符串s的SHA-256哈希值
byte[] d = md.digest(input);
// 将哈希值转换为小写十六进制字符串并返回
return DatatypeConverter.printHexBinary(d).toLowerCase();
}
/**
* 对指定的字符串进行URL编码。
* 使用UTF-8编码字符集对字符串进行编码,并对特定的字符进行替换,以符合URL编码规范。
*
* @param str 需要进行URL编码的字符串。
* @return 编码后的字符串。其中,加号"+"被替换为"%20",星号"*"被替换为"%2A",波浪号"%7E"被替换为"~"。
*/
public static String percentCode(String str) {
if (str == null) {
throw new IllegalArgumentException("输入字符串不可为null");
}
try {
return URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8编码不被支持", e);
}
}
}
Python示例
示例代码的运行环境是Python 3.12.3,您可能需要根据具体情况对代码进行相应的调整。
需要您手动安装pytz和requests,请根据您所使用的Python版本在终端(Terminal)执行以下命令。
Python3
pip3 install pytz
pip3 install requests
import hashlib
import hmac
import json
import os
import uuid
from collections import OrderedDict
from urllib.parse import urlencode, quote_plus
import pytz
import requests
from datetime import datetime
class SignatureRequest:
def __init__(self, http_method, canonical_uri, host, x_acs_action, x_acs_version):
self.http_method = http_method
self.canonical_uri = canonical_uri
self.host = host
self.x_acs_action = x_acs_action
self.x_acs_version = x_acs_version
self.headers = self._init_headers()
self.query_param = OrderedDict()
self.body = None
def _init_headers(self):
headers = OrderedDict()
headers['host'] = self.host
headers['x-acs-action'] = self.x_acs_action
headers['x-acs-version'] = self.x_acs_version
current_time = datetime.now(pytz.timezone('Etc/GMT'))
headers['x-acs-date'] = current_time.strftime('%Y-%m-%dT%H:%M:%SZ')
headers['x-acs-signature-nonce'] = str(uuid.uuid4())
return headers
def sorted_query_params(self):
# 对查询参数按名称排序并返回编码后的字符串
sorted_query_params = sorted(self.query_param.items(), key=lambda item: item[0])
self.query_param = {k: v for k, v in sorted_query_params}
def sorted_headers(self):
# 对请求头按名称排序并返回编码后的字符串
sorted_headers = sorted(self.headers.items(), key=lambda item: item[0])
self.headers = {k: v for k, v in sorted_headers}
def get_authorization(request):
try:
new_query_param = OrderedDict()
process_object(new_query_param, '', request.query_param)
request.query_param = new_query_param
# 步骤 1:拼接规范请求串
canonical_query_string = '&'.join(
f'{percent_code(quote_plus(k))}={percent_code(quote_plus(str(v)))}' for k, v in
request.query_param.items())
hashed_request_payload = sha256_hex(request.body or ''.encode('utf-8'))
request.headers['x-acs-content-sha256'] = hashed_request_payload
request.sorted_headers()
# 构建规范化请求头和已签名消息头列表
filtered_headers = OrderedDict()
for k, v in request.headers.items():
if k.lower().startswith('x-acs-') or k.lower() in ['host', 'content-type']:
filtered_headers[k.lower()] = v
canonical_headers = '\n'.join(f'{k}:{v}' for k, v in filtered_headers.items()) + '\n'
signed_headers = ';'.join(k for k in filtered_headers.keys())
canonical_request = (
f'{request.http_method}\n{request.canonical_uri}\n{canonical_query_string}\n'
f'{canonical_headers}\n{signed_headers}\n{hashed_request_payload}')
print(canonical_request)
# 步骤 2:拼接待签名字符串
hashed_canonical_request = sha256_hex(canonical_request.encode('utf-8'))
string_to_sign = f'{ALGORITHM}\n{hashed_canonical_request}'
print(string_to_sign)
# 步骤 3:计算签名
signature = hmac256(ACCESS_KEY_SECRET.encode('utf-8'), string_to_sign).hex().lower()
print(signature)
# 步骤 4:拼接Authorization
authorization = f'{ALGORITHM} Credential={ACCESS_KEY_ID},SignedHeaders={signed_headers},Signature={signature}'
print(authorization)
request.headers['Authorization'] = authorization
except Exception as e:
print("Failed to get authorization")
print(e)
def form_data_to_string(form_data):
tile_map = OrderedDict()
process_object(tile_map, '', form_data)
return urlencode(tile_map)
def process_object(result_map, key, value):
# 如果值为空,则无需进一步处理
if value is None:
return
if key is None:
key = ""
# 当值为列表类型时,遍历列表中的每个元素,并递归处理
if isinstance(value, (list, tuple)):
for i, item in enumerate(value):
process_object(result_map, f"{key}.{i + 1}", item)
elif isinstance(value, dict):
# 当值为字典类型时,遍历字典中的每个键值对,并递归处理
for sub_key, sub_value in value.items():
process_object(result_map, f"{key}.{sub_key}", sub_value)
else:
# 对于以"."开头的键,移除开头的"."以保持键的连续性
if key.startswith("."):
key = key[1:]
# 对于字节类型的值,将其转换为UTF-8编码的字符串
if isinstance(value, bytes):
result_map[key] = value.decode('utf-8')
else:
# 对于其他类型的值,直接转换为字符串
result_map[key] = str(value)
def hmac256(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
def sha256_hex(s):
return hashlib.sha256(s).hexdigest()
def call_api(request):
url = f'https://{request.host}{request.canonical_uri}'
if request.query_param:
url += '?' + urlencode(request.query_param, doseq=True, safe='*')
print(url)
headers = {k: v for k, v in request.headers.items()}
if request.body:
data = request.body
else:
data = None
try:
response = requests.request(method=request.http_method, url=url, headers=headers, data=data)
response.raise_for_status()
print(response.text)
except requests.RequestException as e:
print("Failed to send request")
print(e)
def percent_code(encoded_str):
return encoded_str.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
# 环境变量中获取Access Key ID和Access Key Secret
ACCESS_KEY_ID = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID')
ACCESS_KEY_SECRET = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET')
ALGORITHM = 'ACS3-HMAC-SHA256'
"""
签名示例,您需要根据实际情况替换main方法中的示例参数。
ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
2. 请求参数在元数据中显示"in": "body",通过body传参。
3. 请求参数在元数据中显示"in": "formData",通过body传参。
"""
if __name__ == "__main__":
# RPC接口请求示例一:请求参数"in":"query"
http_method = "POST" # 请求方式,从元数据中可以获取,建议使用POST。
canonical_uri = "/" # RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
host = "ecs.cn-hangzhou.aliyuncs.com" # 云产品服务接入点
x_acs_action = "DescribeInstanceStatus" # API名称
x_acs_version = "2014-05-26" # API版本号
signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# DescribeInstanceStatus请求参数如下:
# RegionId在元数据中显示的类型是String,"in":"query",必填
signature_request.query_param['RegionId'] = 'cn-hangzhou'
# InstanceId的在元数据中显示的类型是array,"in":"query",非必填
signature_request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"]
# # RPC接口请求示例二:请求参数"in":"body"
# http_method = "POST"
# canonical_uri = "/"
# host = "ocr-api.cn-hangzhou.aliyuncs.com"
# x_acs_action = "RecognizeGeneral"
# x_acs_version = "2021-07-07"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# # 请求参数在元数据中显示"in": "body",通过body传参。
# file_path = "D:\\test.png"
# with open(file_path, 'rb') as file:
# # 读取图片内容为字节数组
# signature_request.body = file.read()
# signature_request.headers["content-type"] = "application/octet-stream"
# # RPC接口请求示例三:请求参数"in": "formData"
# http_method = "POST"
# canonical_uri = "/"
# host = "mt.aliyuncs.com"
# x_acs_action = "TranslateGeneral"
# x_acs_version = "2018-10-12"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# # TranslateGeneral请求参数如下:
# # Context在元数据中显示的类型是String,"in":"query",非必填
# signature_request.query_param['Context'] = '早上'
# # FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
# form_data = OrderedDict()
# form_data["FormatType"] = "text"
# form_data["SourceLanguage"] = "zh"
# form_data["TargetLanguage"] = "en"
# form_data["SourceText"] = "你好"
# form_data["Scene"] = "general"
# signature_request.body = bytes(form_data_to_string(form_data), 'utf-8')
# signature_request.headers["content-type"] = "application/x-www-form-urlencoded"
# # ROA接口POST请求
# http_method = "POST"
# canonical_uri = "/clusters"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "CreateCluster"
# x_acs_version = "2015-12-15"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# 请求参数在元数据中显示"in":"body",通过body传参。
# body = OrderedDict()
# body["name"] = "testDemo"
# body["region_id"] = "cn-beijing"
# body["cluster_type"] = "ExternalKubernetes"
# body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
# body["container_cidr"] = "172.16.1.0/20"
# body["service_cidr"] = "10.2.0.0/24"
# body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
# body["vswitch_ids"] = ["vsw-2zei30dhfldu8XXXXXXXX"]
# signature_request.body = bytes(json.dumps(body, separators=(',', ':')), 'utf-8')
# signature_request.headers["content-type"] = "application/json; charset=utf-8"
# # ROA接口GET请求
# http_method = "GET"
# # canonicalUri如果存在path参数,需要对path参数encode,percent_code({path参数})
# cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
# canonical_uri = f"/clusters/{cluster_id_encode}/resources"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "DescribeClusterResources"
# x_acs_version = "2015-12-15"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# signature_request.query_param['with_addon_resources'] = True
# # ROA接口GET请求
# http_method = "DELETE"
# # canonicalUri如果存在path参数,需要对path参数encode,percent_code({path参数})
# cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
# canonical_uri = f"/clusters/{cluster_id_encode}"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "DeleteCluster"
# x_acs_version = "2015-12-15"
# signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
signature_request.sorted_query_params()
get_authorization(signature_request)
call_api(signature_request)
Go示例
示例代码的运行环境是go1.22.2,您可能需要根据具体情况对代码进行相应的调整。
需要您在终端(Terminal)执行以下命令:
go get github.com/google/uuid
go get golang.org/x/exp/maps
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"os"
"sort"
"golang.org/x/exp/maps"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/google/uuid"
)
type Request struct {
httpMethod string
canonicalUri string
host string
xAcsAction string
xAcsVersion string
headers map[string]string
body []byte
queryParam map[string]interface{}
}
func NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion string) *Request {
req := &Request{
httpMethod: httpMethod,
canonicalUri: canonicalUri,
host: host,
xAcsAction: xAcsAction,
xAcsVersion: xAcsVersion,
headers: make(map[string]string),
queryParam: make(map[string]interface{}),
}
req.headers["host"] = host
req.headers["x-acs-action"] = xAcsAction
req.headers["x-acs-version"] = xAcsVersion
req.headers["x-acs-date"] = time.Now().UTC().Format(time.RFC3339)
req.headers["x-acs-signature-nonce"] = uuid.New().String()
return req
}
// os.Getenv()表示从环境变量中获取AccessKey ID和AccessKey Secret。
var (
AccessKeyId = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
ALGORITHM = "ACS3-HMAC-SHA256"
)
// 签名示例,您需要根据实际情况替换main方法中的示例参数。
// ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
// 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
// 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
// 2. 请求参数在元数据中显示"in": "body",通过body传参。
// 3. 请求参数在元数据中显示"in": "formData",通过body传参。
func main() {
// RPC接口请求示例一:请求参数"in":"query"
httpMethod := "POST" // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
canonicalUri := "/" // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
host := "ecs.cn-hangzhou.aliyuncs.com" // 云产品服务接入点
xAcsAction := "DescribeInstanceStatus" // API名称
xAcsVersion := "2014-05-26" // API版本号
req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// DescribeInstanceStatus请求参数如下:
// RegionId在元数据中显示的类型是String,"in":"query",必填
req.queryParam["RegionId"] = "cn-hangzhou"
// InstanceId的在元数据中显示的类型是array,"in":"query",非必填
instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
req.queryParam["InstanceId"] = instanceIds
// // RPC接口请求示例二:请求参数"in":"body"
// httpMethod := "POST"
// canonicalUri := "/"
// host := "ocr-api.cn-hangzhou.aliyuncs.com"
// xAcsAction := "RecognizeGeneral"
// xAcsVersion := "2021-07-07"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// // 读取文件内容
// filePath := "D:\\test.png"
// bytes, err := os.ReadFile(filePath)
// if err != nil {
// fmt.Println("Error reading file:", err)
// return
// }
// req.body = bytes
// req.headers["content-type"] = "application/octet-stream"
// // RPC接口请求示例三:请求参数"in": "formData"
// httpMethod := "POST"
// canonicalUri := "/"
// host := "mt.aliyuncs.com"
// xAcsAction := "TranslateGeneral"
// xAcsVersion := "2018-10-12"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// // TranslateGeneral请求参数如下:
// // Context在元数据中显示的类型是String,"in":"query",非必填
// req.queryParam["Context"] = "早上"
// // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
// body := make(map[string]interface{})
// body["FormatType"] = "text"
// body["SourceLanguage"] = "zh"
// body["TargetLanguage"] = "en"
// body["SourceText"] = "你好"
// body["Scene"] = "general"
// str := formDataToString(body)
// req.body = []byte(*str)
// req.headers["content-type"] = "application/x-www-form-urlencoded"
// // ROA接口POST请求
// httpMethod := "POST"
// canonicalUri := "/clusters"
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "CreateCluster"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// // 封装请求参数,请求参数在元数据中显示"in": "body",表示参数放在body中
// body := make(map[string]interface{})
// body["name"] = "testDemo"
// body["region_id"] = "cn-beijing"
// body["cluster_type"] = "ExternalKubernetes"
// body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
// body["container_cidr"] = "10.0.0.0/8"
// body["service_cidr"] = "172.16.1.0/20"
// body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
// vswitch_ids := []interface{}{"vsw-2zei30dhfldu8XXXXXXXX"}
// body["vswitch_ids"] = vswitch_ids
// jsonBytes, err := json.Marshal(body)
// if err != nil {
// fmt.Println("Error marshaling to JSON:", err)
// return
// }
// req.body = []byte(jsonBytes)
// req.headers["content-type"] = "application/json; charset=utf-8"
// // ROA接口GET请求
// httpMethod := "GET"
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66") + "/resources"
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "DescribeClusterResources"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// req.queryParam["with_addon_resources"] = "true"
// // ROA接口DELETE请求
// httpMethod := "DELETE"
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66")
// host := "cs.cn-beijing.aliyuncs.com"
// xAcsAction := "DeleteCluster"
// xAcsVersion := "2015-12-15"
// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
// 签名过程
getAuthorization(req)
// 调用API
error := callAPI(req)
if error != nil {
println(error.Error())
}
}
func callAPI(req *Request) error {
urlStr := "https://" + req.host + req.canonicalUri
q := url.Values{}
keys := maps.Keys(req.queryParam)
sort.Strings(keys)
for _, k := range keys {
v := req.queryParam[k]
q.Set(k, fmt.Sprintf("%v", v))
}
urlStr += "?" + q.Encode()
fmt.Println(urlStr)
httpReq, err := http.NewRequest(req.httpMethod, urlStr, strings.NewReader(string(req.body)))
if err != nil {
return err
}
for key, value := range req.headers {
httpReq.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
var respBuffer bytes.Buffer
_, err = io.Copy(&respBuffer, resp.Body)
if err != nil {
return err
}
respBytes := respBuffer.Bytes()
fmt.Println(string(respBytes))
return nil
}
func getAuthorization(req *Request) {
// 处理queryParam中参数值为List、Map类型的参数,将参数平铺
newQueryParams := make(map[string]interface{})
processObject(newQueryParams, "", req.queryParam)
req.queryParam = newQueryParams
// 步骤 1:拼接规范请求串
canonicalQueryString := ""
keys := maps.Keys(req.queryParam)
sort.Strings(keys)
for _, k := range keys {
v := req.queryParam[k]
canonicalQueryString += percentCode(url.QueryEscape(k)) + "=" + percentCode(url.QueryEscape(fmt.Sprintf("%v", v))) + "&"
}
canonicalQueryString = strings.TrimSuffix(canonicalQueryString, "&")
fmt.Printf("canonicalQueryString========>%s\n", canonicalQueryString)
var bodyContent []byte
if req.body == nil {
bodyContent = []byte("")
} else {
bodyContent = req.body
}
hashedRequestPayload := sha256Hex(bodyContent)
req.headers["x-acs-content-sha256"] = hashedRequestPayload
canonicalHeaders := ""
signedHeaders := ""
HeadersKeys := maps.Keys(req.headers)
sort.Strings(HeadersKeys)
for _, k := range HeadersKeys {
lowerKey := strings.ToLower(k)
if lowerKey == "host" || strings.HasPrefix(lowerKey, "x-acs-") || lowerKey == "content-type" {
canonicalHeaders += lowerKey + ":" + req.headers[k] + "\n"
signedHeaders += lowerKey + ";"
}
}
signedHeaders = strings.TrimSuffix(signedHeaders, ";")
canonicalRequest := req.httpMethod + "\n" + req.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
fmt.Printf("canonicalRequest========>\n%s\n", canonicalRequest)
// 步骤 2:拼接待签名字符串
hashedCanonicalRequest := sha256Hex([]byte(canonicalRequest))
stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
fmt.Printf("stringToSign========>\n%s\n", stringToSign)
// 步骤 3:计算签名
byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
if err != nil {
fmt.Println(err)
}
signature := strings.ToLower(hex.EncodeToString(byteData))
// 步骤 4:拼接Authorization
authorization := ALGORITHM + " Credential=" + AccessKeyId + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
fmt.Printf("authorization========>%s\n", authorization)
req.headers["Authorization"] = authorization
}
func hmac256(key []byte, toSignString string) ([]byte, error) {
// 实例化HMAC-SHA256哈希
h := hmac.New(sha256.New, key)
// 写入待签名的字符串
_, err := h.Write([]byte(toSignString))
if err != nil {
return nil, err
}
// 计算签名并返回
return h.Sum(nil), nil
}
func sha256Hex(byteArray []byte) string {
// 实例化SHA-256哈希函数
hash := sha256.New()
// 将字符串写入哈希函数
_, _ = hash.Write(byteArray)
// 计算SHA-256哈希值并转换为小写的十六进制字符串
hexString := hex.EncodeToString(hash.Sum(nil))
return hexString
}
func percentCode(str string) string {
// 替换特定的编码字符
str = strings.ReplaceAll(str, "+", "%20")
str = strings.ReplaceAll(str, "*", "%2A")
str = strings.ReplaceAll(str, "%7E", "~")
return str
}
func formDataToString(formData map[string]interface{}) *string {
tmp := make(map[string]interface{})
processObject(tmp, "", formData)
res := ""
urlEncoder := url.Values{}
for key, value := range tmp {
v := fmt.Sprintf("%v", value)
urlEncoder.Add(key, v)
}
res = urlEncoder.Encode()
return &res
}
// processObject 递归处理对象,将复杂对象(如Map和List)展开为平面的键值对
func processObject(mapResult map[string]interface{}, key string, value interface{}) {
if value == nil {
return
}
switch v := value.(type) {
case []interface{}:
for i, item := range v {
processObject(mapResult, fmt.Sprintf("%s.%d", key, i+1), item)
}
case map[string]interface{}:
for subKey, subValue := range v {
processObject(mapResult, fmt.Sprintf("%s.%s", key, subKey), subValue)
}
default:
if strings.HasPrefix(key, ".") {
key = key[1:]
}
if b, ok := v.([]byte); ok {
mapResult[key] = string(b)
} else {
mapResult[key] = fmt.Sprintf("%v", v)
}
}
}
Node.js示例
示例代码的运行环境是Node.js v20.13.1,您可能需要根据具体情况对代码进行相应的调整。
本示例所用语言是javaScript。
const crypto = require('crypto');
const fs = require('fs');
class Request {
constructor(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion) {
this.httpMethod = httpMethod;
this.canonicalUri = canonicalUri || '/';
this.host = host;
this.xAcsAction = xAcsAction;
this.xAcsVersion = xAcsVersion;
this.headers = {};
this.body = null;
this.queryParam = {};
this.initHeader();
}
initHeader() {
const date = new Date();
this.headers = {
'host': this.host,
'x-acs-action': this.xAcsAction,
'x-acs-version': this.xAcsVersion,
'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
}
}
}
const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
const encoder = new TextEncoder()
if (!accessKeyId || !accessKeySecret) {
console.error('ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables must be set.');
process.exit(1);
}
function getAuthorization(signRequest) {
try {
newQueryParam = {};
processObject(newQueryParam, "", signRequest.queryParam);
signRequest.queryParam = newQueryParam;
// 步骤 1:拼接规范请求串
const canonicalQueryString = Object.entries(signRequest.queryParam)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
.join('&');
// 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
const requestPayload = signRequest.body || encoder.encode('');
const hashedRequestPayload = sha256Hex(requestPayload);
signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;
// 将所有key都转换为小写
signRequest.headers = Object.fromEntries(
Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
);
const sortedKeys = Object.keys(signRequest.headers)
.filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
.sort();
// 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
const signedHeaders = sortedKeys.join(";")
// 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
const canonicalHeaders = sortedKeys.map(key => `${key}:${signRequest.headers[key]}`).join('\n') + '\n';
const canonicalRequest = [
signRequest.httpMethod,
signRequest.canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedRequestPayload
].join('\n');
console.log('canonicalRequest=========>\n', canonicalRequest);
// 步骤 2:拼接待签名字符串
const hashedCanonicalRequest = sha256Hex(encoder.encode(canonicalRequest));
const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
console.log('stringToSign=========>', stringToSign);
// 步骤 3:计算签名
const signature = hmac256(accessKeySecret, stringToSign);
console.log('signature=========>', signature);
// 步骤 4:拼接 Authorization
const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
console.log('authorization=========>', authorization);
signRequest.headers['Authorization'] = authorization;
} catch (error) {
console.error('Failed to get authorization');
console.error(error);
}
}
async function callApi(signRequest) {
try {
let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
// 添加请求参数
if (signRequest.queryParam) {
const query = new URLSearchParams(signRequest.queryParam);
url += '?' + query.toString();
}
console.log('url=========>', url);
// 配置请求选项
let options = {
method: signRequest.httpMethod.toUpperCase(),
headers: signRequest.headers
};
// 处理请求体
if (signRequest.body && ['POST', 'PUT'].includes(signRequest.httpMethod.toUpperCase())) {
options.body = signRequest.body;
}
return (await fetch(url, options)).text();
} catch (error) {
console.error('Failed to send request:', error);
}
}
function percentCode(str) {
return encodeURIComponent(str)
.replace(/\+/g, '%20')
.replace(/\*/g, '%2A')
.replace(/~/g, '%7E');
}
function hmac256(key, data) {
const hmac = crypto.createHmac('sha256', key);
hmac.update(data, 'utf8');
return hmac.digest('hex').toLowerCase();
}
function sha256Hex(bytes) {
const hash = crypto.createHash('sha256');
const digest = hash.update(bytes).digest('hex');
return digest.toLowerCase();
}
function formDataToString(formData) {
const tmp = {};
processObject(tmp, "", formData);
let queryString = '';
for (let [key, value] of Object.entries(tmp)) {
if (queryString !== '') {
queryString += '&';
}
queryString += encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
return queryString;
}
function processObject(map, key, value) {
// 如果值为空,则无需进一步处理
if (value === null) {
return;
}
if (key === null) {
key = "";
}
// 当值为Array类型时,遍历Array中的每个元素,并递归处理
if (Array.isArray(value)) {
value.forEach((item, index) => {
processObject(map, `${key}.${index + 1}`, item);
});
} else if (typeof value === 'object' && value !== null) {
// 当值为Object类型时,遍历Object中的每个键值对,并递归处理
Object.entries(value).forEach(([subKey, subValue]) => {
processObject(map, `${key}.${subKey}`, subValue);
});
} else {
// 对于以"."开头的键,移除开头的"."以保持键的连续性
if (key.startsWith('.')) {
key = key.slice(1);
}
map[key] = String(value);
}
}
/**
* 签名示例,您需要根据实际情况替换main方法中的示例参数。
* ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
*
* 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
* 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
* 2. 请求参数在元数据中显示"in": "body",通过body传参。
* 3. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
// RPC接口请求示例一:请求参数"in":"query"
const httpMethod = 'POST'; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
const canonicalUri = '/'; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
const host = 'ecs.cn-hangzhou.aliyuncs.com'; // endpoint
const xAcsAction = 'DescribeInstanceStatus'; // API名称
const xAcsVersion = '2014-05-26'; // API版本号
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// DescribeInstanceStatus请求参数如下:
signRequest.queryParam = {
// RegionId在元数据中显示的类型是String,"in":"query",必填
RegionId: 'cn-hangzhou',
// InstanceId的在元数据中显示的类型是array,"in":"query",非必填
InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}
// // RPC接口请求示例二:请求参数"in":"body"
// const httpMethod = 'POST';
// const canonicalUri = '/';
// const host = 'ocr-api.cn-hangzhou.aliyuncs.com';
// const xAcsAction = 'RecognizeGeneral';
// const xAcsVersion = '2021-07-07';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// const filePath = 'D:\\test.png';
// const bytes = fs.readFileSync(filePath);
// // 请求参数在元数据中显示"in": "body",表示参数放在body中
// signRequest.body = bytes;
// signRequest.headers['content-type'] = 'application/octet-stream';
// // RPC接口请求示例三:请求参数"in": "formData"
// const httpMethod = 'POST'; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
// const canonicalUri = '/'; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
// const host = 'mt.aliyuncs.com'; // endpoint
// const xAcsAction = 'TranslateGeneral'; // API名称
// const xAcsVersion = '2018-10-12'; // API版本号
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// // TranslateGeneral请求参数如下:
// // Context在元数据中显示的类型是String,"in":"query",非必填
// signRequest.queryParam["Context"] = "早上";
// // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
// const formData = {
// SourceLanguage: "zh",
// TargetLanguage: "en",
// FormatType: "text",
// Scene: "general",
// SourceText: '你好'
// }
// const str = formDataToString(formData)
// signRequest.body = encoder.encode(str);
// signRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
// // ROA接口POST请求
// const httpMethod = 'POST';
// const canonicalUri = '/clusters';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'CreateCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// // 请求参数在元数据中显示"in": "body",表示参数放在body中
// const body = {
// name: 'testDemo',
// region_id: 'cn-beijing',
// cluster_type: 'ExternalKubernetes',
// vpcid: 'vpc-2zeou1uod4ylaf35teei9',
// container_cidr: '10.0.0.0/8',
// service_cidr: '172.16.3.0/20',
// security_group_id: 'sg-2ze1a0rlgeo7dj37dd1q',
// vswitch_ids: [
// 'vsw-2zei30dhfldu8ytmtarro'
// ],
// }
// signRequest.body = encoder.encode(JSON.stringify(body));
// signRequest.headers['content-type'] = 'application/json';
// // ROA接口GET请求
// const httpMethod = 'GET';
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96") + '/resources';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DescribeClusterResources';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// signRequest.queryParam = {
// with_addon_resources: true,
// }
// // ROA接口DELETE请求
// const httpMethod = 'DELETE';
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96");
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DeleteCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
getAuthorization(signRequest);
callApi(signRequest).then(r => {
console.log(r);
}).catch(error => {
console.error(error);
});
PHP示例
示例代码的运行环境是PHP 7.4.33,您可能需要根据具体情况对代码进行相应的调整。
<?php
class SignatureDemo
{
// 加密算法
private $ALGORITHM;
// Access Key ID
private $AccessKeyId;
// Access Key Secret
private $AccessKeySecret;
public function __construct()
{
date_default_timezone_set('UTC'); // 设置时区为GMT
$this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // getenv()表示从环境变量中获取RAM用户Access Key ID
$this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // getenv()表示从环境变量中获取RAM用户Access Key Secret
$this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 设置加密算法
}
/**
* 签名示例,您需要根据实际情况替换main方法中的示例参数。
* ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
*
* 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
* 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
* 2. 请求参数在元数据中显示"in": "body",通过body传参。
* 3. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
public function main()
{
// RPC接口请求示例一:请求参数"in":"query"
$request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
// DescribeInstanceStatus请求参数如下:
$request['queryParam'] = [
// RegionId在元数据中显示的类型是String,"in":"query",必填
'RegionId' => 'cn-hangzhou',
// InstanceId的在元数据中显示的类型是array,"in":"query",非必填
'InstanceId' => ["i-bp11ht4h2kdXXXXXXXX", "i-bp16maz3h3xgXXXXXXXX", "i-bp10r67hmslXXXXXXXX"]
];
// // RPC接口请求示例二:请求参数"in":"body"
// $request = $this->createRequest('POST', '/', 'ocr-api.cn-hangzhou.aliyuncs.com', 'RecognizeGeneral', '2021-07-07');
// // 请求参数在元数据中显示"in": "body",通过body传参。
// $filePath = 'D:\\test.png';
// // 使用文件资源传入二进制格式文件
// $fileResource = fopen($filePath, 'rb');
// $request['body'] = stream_get_contents($fileResource);
// $request['headers']['content-type'] = 'application/octet-stream'; // 设置 Content-Type 为 application/octet-stream
// // 关闭文件资源
// fclose($fileResource);
// // RPC接口请求示例三:请求参数"in": "formData"
// $request = $this->createRequest('POST', '/', 'mt.aliyuncs.com', 'TranslateGeneral', '2018-10-12');
// // TranslateGeneral请求参数如下:
// $request['queryParam'] = [
// // Context在元数据中显示的类型是String,"in":"query",非必填
// 'Context' => '早上',
// ];
// $formData = [
// 'FormatType' => 'text',
// 'SourceLanguage' => 'zh',
// 'TargetLanguage' => 'en',
// 'SourceText' => '你好',
// 'Scene' => 'general',
// ];
// $str = self::formDataToString($formData);
// $request['body'] = $str;
// $request['headers']['content-type'] = 'application/x-www-form-urlencoded';
// // ROA接口POST请求
// $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
// $bodyData = [
// 'name' => '测试集群',
// 'region_id' => 'cn-beijing',
// 'cluster_type' => 'ExternalKubernetes',
// 'vpcid' => 'vpc-2zeou1uod4ylaXXXXXXXX',
// 'service_cidr' => '10.2.0.0/24',
// 'security_group_id' => 'sg-2ze1a0rlgeo7XXXXXXXX',
// "vswitch_ids" => [
// "vsw-2zei30dhfldu8XXXXXXXX"
// ]
// ];
// $request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
// $request['headers']['content-type'] = 'application/json; charset=utf-8';
// // ROA接口GET请求
// // canonicalUri如果存在path参数,需要对path参数encode,rawurlencode({path参数})
// $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
// $canonicalUri = sprintf("/clusters/%s/resources", rawurlencode($cluster_id));
// $request = $this->createRequest('GET', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DescribeClusterResources', '2015-12-15');
// $request['queryParam'] = [
// 'with_addon_resources' => true,
// ];
// // ROA接口DELETE请求
// $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
// $canonicalUri = sprintf("/clusters/%s", rawurlencode($cluster_id));
// $request = $this->createRequest('DELETE', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DeleteCluster', '2015-12-15');
$this->getAuthorization($request);
$this->callApi($request);
}
private function createRequest($httpMethod, $canonicalUri, $host, $xAcsAction, $xAcsVersion)
{
$headers = [
'host' => $host,
'x-acs-action' => $xAcsAction,
'x-acs-version' => $xAcsVersion,
'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'),
'x-acs-signature-nonce' => bin2hex(random_bytes(16)),
];
return [
'httpMethod' => $httpMethod,
'canonicalUri' => $canonicalUri,
'host' => $host,
'headers' => $headers,
'queryParam' => [],
'body' => null,
];
}
private function getAuthorization(&$request)
{
$request['queryParam'] = $this->processObject($request['queryParam']);
$canonicalQueryString = $this->buildCanonicalQueryString($request['queryParam']);
$hashedRequestPayload = hash('sha256', $request['body'] ?? '');
$request['headers']['x-acs-content-sha256'] = $hashedRequestPayload;
$canonicalHeaders = $this->buildCanonicalHeaders($request['headers']);
$signedHeaders = $this->buildSignedHeaders($request['headers']);
$canonicalRequest = implode("\n", [
$request['httpMethod'],
$request['canonicalUri'],
$canonicalQueryString,
$canonicalHeaders,
$signedHeaders,
$hashedRequestPayload,
]);
$hashedCanonicalRequest = hash('sha256', $canonicalRequest);
$stringToSign = "{$this->ALGORITHM}\n$hashedCanonicalRequest";
$signature = strtolower(bin2hex(hash_hmac('sha256', $stringToSign, $this->AccessKeySecret, true)));
$authorization = "{$this->ALGORITHM} Credential={$this->AccessKeyId},SignedHeaders=$signedHeaders,Signature=$signature";
$request['headers']['Authorization'] = $authorization;
}
private function callApi($request)
{
try {
// 通过cURL发送请求
$url = "https://" . $request['host'] . $request['canonicalUri'];
// 添加请求参数到URL
if (!empty($request['queryParam'])) {
$url .= '?' . http_build_query($request['queryParam']);
}
echo $url;
// 初始化cURL会话
$ch = curl_init();
// 设置cURL选项
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 禁用SSL证书验证,请注意,这会降低安全性,不应在生产环境中使用(不推荐!!!)
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回而不是输出内容
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // 添加请求头
// 根据请求类型设置cURL选项
switch ($request['httpMethod']) {
case "GET":
break;
case "POST":
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
break;
case "DELETE":
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
break;
default:
echo "Unsupported HTTP method: " . $request['body'];
throw new Exception("Unsupported HTTP method");
}
// 发送请求
$result = curl_exec($ch);
// 检查是否有错误发生
if (curl_errno($ch)) {
echo "Failed to send request: " . curl_error($ch);
} else {
echo $result;
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
} finally {
// 关闭cURL会话
curl_close($ch);
}
}
function formDataToString($formData)
{
$res = self::processObject($formData);
return http_build_query($res);
}
function processObject($value)
{
// 如果值为空,则无需进一步处理
if ($value === null) {
return;
}
$tmp = [];
foreach ($value as $k => $v) {
if (0 !== strpos($k, '_')) {
$tmp[$k] = $v;
}
}
return self::flatten($tmp);
}
private static function flatten($items = [], $delimiter = '.', $prepend = '')
{
$flatten = [];
foreach ($items as $key => $value) {
$pos = \is_int($key) ? $key + 1 : $key;
if (\is_object($value)) {
$value = get_object_vars($value);
}
if (\is_array($value) && !empty($value)) {
$flatten = array_merge(
$flatten,
self::flatten($value, $delimiter, $prepend . $pos . $delimiter)
);
} else {
if (\is_bool($value)) {
$value = true === $value ? 'true' : 'false';
}
$flatten["$prepend$pos"] = $value;
}
}
return $flatten;
}
private function convertHeadersToArray($headers)
{
$headerArray = [];
foreach ($headers as $key => $value) {
$headerArray[] = "$key: $value";
}
return $headerArray;
}
private function buildCanonicalQueryString($queryParams)
{
ksort($queryParams);
// Build and encode query parameters
$params = [];
foreach ($queryParams as $k => $v) {
if (null === $v) {
continue;
}
$str = rawurlencode($k);
if ('' !== $v && null !== $v) {
$str .= '=' . rawurlencode($v);
} else {
$str .= '=';
}
$params[] = $str;
}
return implode('&', $params);
}
private function buildCanonicalHeaders($headers)
{
// Sort headers by key and concatenate them
uksort($headers, 'strcasecmp');
$canonicalHeaders = '';
foreach ($headers as $key => $value) {
$canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n";
}
return $canonicalHeaders;
}
private function buildSignedHeaders($headers)
{
// Build the signed headers string
$signedHeaders = array_keys($headers);
sort($signedHeaders, SORT_STRING | SORT_FLAG_CASE);
return implode(';', array_map('strtolower', $signedHeaders));
}
}
$demo = new SignatureDemo();
$demo->main();
.NET示例
示例代码的运行环境是.NET 8.0.302,您可能需要根据具体情况对代码进行相应的调整。
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace SignatureDemo
{
public class Request
{
public string HttpMethod { get; private set; }
public string CanonicalUri { get; private set; }
public string Host { get; private set; }
public string XAcsAction { get; private set; }
public string XAcsVersion { get; private set; }
public SortedDictionary<string, object> Headers { get; private set; }
public byte[] Body { get; set; }
public Dictionary<string, object> QueryParam { get; set; }
public Request(string httpMethod, string canonicalUri, string host, string xAcsAction, string xAcsVersion)
{
HttpMethod = httpMethod;
CanonicalUri = canonicalUri;
Host = host;
XAcsAction = xAcsAction;
XAcsVersion = xAcsVersion;
Headers = [];
QueryParam = [];
Body = null;
InitHeader();
}
private void InitHeader()
{
Headers["host"] = Host;
Headers["x-acs-action"] = XAcsAction;
Headers["x-acs-version"] = XAcsVersion;
DateTime utcNow = DateTime.UtcNow;
Headers["x-acs-date"] = utcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
Headers["x-acs-signature-nonce"] = Guid.NewGuid().ToString();
}
}
public class Program
{
private static readonly string AccessKeyId = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID") ?? throw new InvalidOperationException("环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 未设置");
private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ?? throw new InvalidOperationException("环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET 未设置");
private const string Algorithm = "ACS3-HMAC-SHA256";
private const string ContentType = "content-type";
/**
* 签名示例,您需要根据实际情况替换main方法中的示例参数。
* ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
*
* 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
* 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
* 2. 请求参数在元数据中显示"in": "body",通过body传参。
* 3. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
public static void Main(string[] args)
{
// RPC接口请求示例一:请求参数"in":"query"
string httpMethod = "POST"; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例
string canonicalUri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
string host = "ecs.cn-hangzhou.aliyuncs.com"; // 云产品服务接入点
string xAcsAction = "DescribeInstanceStatus"; // API名称
string xAcsVersion = "2014-05-26"; // API版本号
var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// DescribeInstanceStatus请求参数如下:
// RegionId在元数据中显示的类型是String,"in":"query",必填
request.QueryParam["RegionId"] = "cn-hangzhou";
// InstanceId的在元数据中显示的类型是array,"in":"query",非必填
List<string> instanceIds = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"];
request.QueryParam["InstanceId"] = instanceIds;
// // RPC接口请求示例二:请求参数"in":"body"
// string httpMethod = "POST";
// string canonicalUri = "/";
// string host = "ocr-api.cn-hangzhou.aliyuncs.com";
// string xAcsAction = "RecognizeGeneral";
// string xAcsVersion = "2021-07-07";
// var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // 请求参数在元数据中显示"in": "body",通过body传参。
// request.Body = File.ReadAllBytes(@"D:\test.png");
// request.Headers["content-type"] = "application/octet-stream";
// // RPC接口请求示例三:请求参数"in": "formData"
// string httpMethod = "POST";
// string canonicalUri = "/";
// string host = "mt.aliyuncs.com";
// string xAcsAction = "TranslateGeneral";
// string xAcsVersion = "2018-10-12";
// var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // TranslateGeneral请求参数如下:
// // Context在元数据中显示的类型是String,"in":"query",非必填
// request.QueryParam["Context"] = "早上";
// // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
// var body = new Dictionary<string, object>
// {
// { "FormatType", "text" },
// { "SourceLanguage", "zh" },
// { "TargetLanguage", "en" },
// { "SourceText", "你好" },
// { "Scene", "general" },
// };
// var str = FormDataToString(body);
// request.Body = Encoding.UTF8.GetBytes(str);
// request.Headers[ContentType] = "application/x-www-form-urlencoded";
// // ROA接口POST请求
// String httpMethod = "POST";
// String canonicalUri = "/clusters";
// String host = "cs.cn-beijing.aliyuncs.com";
// String xAcsAction = "CreateCluster";
// String xAcsVersion = "2015-12-15";
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // 请求body,通过JsonConvert将body转成JSON字符串
// var body = new SortedDictionary<string, object>
// {
// { "name", "testDemo" },
// { "region_id", "cn-beijing" },
// { "cluster_type", "ExternalKubernetes" },
// { "vpcid", "vpc-2zeou1uod4ylaXXXXXXXX" },
// { "container_cidr", "10.0.0.0/8" },
// { "service_cidr", "172.16.1.0/20" },
// { "security_group_id", "sg-2ze1a0rlgeo7XXXXXXXX" },
// { "vswitch_ids", new List<string>{"vsw-2zei30dhfldu8XXXXXXXX"} },
// };
// string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
// request.Body = Encoding.UTF8.GetBytes(jsonBody);
// request.Headers[ContentType] = "application/json; charset=utf-8";
// // ROA接口GET请求
// String httpMethod = "GET";
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX") + "/resources";
// String host = "cs.cn-beijing.aliyuncs.com";
// String xAcsAction = "DescribeClusterResources";
// String xAcsVersion = "2015-12-15";
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// request.QueryParam["with_addon_resources"]=true;
// // ROA接口DELETE请求
// String httpMethod = "DELETE";
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX");
// String host = "cs.cn-beijing.aliyuncs.com";
// String xAcsAction = "DeleteCluster";
// String xAcsVersion = "2015-12-15";
// Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
GetAuthorization(request);
var result = CallApiAsync(request);
Console.WriteLine($"result:{result.Result}");
}
private static async Task<string?> CallApiAsync(Request request)
{
try
{
// 声明 httpClient
using var httpClient = new HttpClient();
// 构建 URL
string url = $"https://{request.Host}{request.CanonicalUri}";
var uriBuilder = new UriBuilder(url);
var query = new List<string>();
// 添加请求参数
foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
{
string value = entry.Value?.ToString() ?? "";
query.Add($"{entry.Key}={Uri.EscapeDataString(value)}");
}
uriBuilder.Query = string.Join("&", query);
Console.WriteLine(uriBuilder.Uri);
var requestMessage = new HttpRequestMessage
{
Method = new HttpMethod(request.HttpMethod),
RequestUri = uriBuilder.Uri,
};
// 设置请求头
foreach (var entry in request.Headers)
{
if (entry.Key == "Authorization")
{
requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ;
}
else if (entry.Key == ContentType) // 与main中定义的要一致
{
continue;
}
else
{
requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
}
}
if (request.Body != null)
{
HttpContent content = new ByteArrayContent(request.Body);
string contentType = request.Headers["content-type"].ToString();
content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
requestMessage.Content = content;
}
// 发送请求
HttpResponseMessage response = await httpClient.SendAsync(requestMessage);
// 读取响应内容
string result = await response.Content.ReadAsStringAsync();
return result;
}
catch (UriFormatException e)
{
Console.WriteLine("Invalid URI syntax");
Console.WriteLine(e.Message);
return null;
}
catch (Exception e)
{
Console.WriteLine("Failed to send request");
Console.WriteLine(e);
return null;
}
}
private static void GetAuthorization(Request request)
{
try
{
// 处理queryParam中参数值为List、Map类型的参数,将参数平铺
request.QueryParam = FlattenDictionary(request.QueryParam);
// 步骤 1:拼接规范请求串
StringBuilder canonicalQueryString = new();
foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
{
if (canonicalQueryString.Length > 0)
{
canonicalQueryString.Append('&');
}
canonicalQueryString.Append($"{PercentCode(entry.Key)}={PercentCode(entry.Value?.ToString() ?? "")}");
}
byte[] requestPayload = request.Body==null ? Encoding.UTF8.GetBytes("") : request.Body;
string hashedRequestPayload = Sha256Hash(requestPayload);
request.Headers["x-acs-content-sha256"] = hashedRequestPayload;
StringBuilder canonicalHeaders = new();
StringBuilder signedHeadersSb = new();
foreach (var entry in request.Headers.OrderBy(e => e.Key.ToLower()))
{
if (entry.Key.StartsWith("x-acs-", StringComparison.CurrentCultureIgnoreCase) || entry.Key.Equals("host", StringComparison.OrdinalIgnoreCase) || entry.Key.Equals(ContentType, StringComparison.OrdinalIgnoreCase))
{
string lowerKey = entry.Key.ToLower();
string value = (entry.Value?.ToString() ?? "").Trim();
canonicalHeaders.Append($"{lowerKey}:{value}\n");
signedHeadersSb.Append($"{lowerKey};");
}
}
string signedHeaders = signedHeadersSb.ToString().TrimEnd(';');
string canonicalRequest = $"{request.HttpMethod}\n{request.CanonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
Console.WriteLine($"canonicalRequest:{canonicalRequest}");
// 步骤 2:拼接待签名字符串
string hashedCanonicalRequest = Sha256Hash(Encoding.UTF8.GetBytes(canonicalRequest));
string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
Console.WriteLine($"stringToSign:{stringToSign}");
// 步骤 3:计算签名
string signature = HmacSha256(AccessKeySecret, stringToSign);
// 步骤 4:拼接 Authorization
string authorization = $"{Algorithm} Credential={AccessKeyId},SignedHeaders={signedHeaders},Signature={signature}";
request.Headers["Authorization"] = authorization;
Console.WriteLine($"authorization:{authorization}");
}
catch (Exception ex)
{
Console.WriteLine("Failed to get authorization");
Console.WriteLine(ex.Message);
}
}
private static string FormDataToString(Dictionary<string, object> formData)
{
Dictionary<string, object> tileMap = FlattenDictionary( formData);
StringBuilder result = new StringBuilder();
bool first = true;
string symbol = "&";
foreach (var entry in tileMap)
{
string value = entry.Value.ToString();
if (!string.IsNullOrEmpty(value))
{
if (!first)
{
result.Append(symbol);
}
first = false;
result.Append(PercentCode(entry.Key));
result.Append("=");
result.Append(PercentCode(value));
}
}
return result.ToString();
}
private static Dictionary<string, object> FlattenDictionary(Dictionary<string, object> dictionary, string prefix = "")
{
var result = new Dictionary<string, object>();
foreach (var kvp in dictionary)
{
string key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";
if (kvp.Value is Dictionary<string, object> nestedDict)
{
var nestedResult = FlattenDictionary(nestedDict, key);
foreach (var nestedKvp in nestedResult)
{
result[nestedKvp.Key] = nestedKvp.Value;
}
}
else if (kvp.Value is List<string> list)
{
for (int i = 0; i < list.Count; i++)
{
result[$"{key}.{i + 1}"] = list[i];
}
}
else
{
result[key] = kvp.Value;
}
}
return result;
}
private static string HmacSha256(string key, string message)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
{
byte[] hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
}
}
private static string Sha256Hash(byte[] input)
{
byte[] hashBytes = SHA256.HashData(input);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
private static string PercentCode(string str)
{
if (string.IsNullOrEmpty(str))
{
throw new ArgumentException("输入字符串不可为null或空");
}
return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
}
}
}
Rust示例
示例代码的运行环境是rustc 1.82.0,您可能需要根据具体情况对代码进行相应的调整。
运行Rust示例,需要您在Cargo.toml中添加以下依赖。
[dependencies]
serde = { version = "1.0" }
serde_json = "1.0"
rand = "0.8"
base64 = "0.21"
sha2 = "0.10"
chrono = "0.4"
hmac = "0.12"
hex = "0.4"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
percent-encoding = "2.1"
use core::str;
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::time::{SystemTime, SystemTimeError};
use chrono::DateTime;
use hmac::{Hmac, Mac};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use rand::Rng;
use serde_json::{json, Value};
use std::borrow::Cow;
use reqwest::{
Client,
header::{HeaderMap, HeaderValue}, Method, Response, StatusCode,
};
use sha2::{Digest, Sha256};
use base64; // 加载base64 crate
/// 获取当前时间戳(秒)
pub fn current_timestamp() -> Result<u64, SystemTimeError> {
Ok(SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs())
}
// 对指定的字符串进行URL编码。返回值类型为&Str,URLEncoder.encode(str, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
pub fn percent_code(encode_str: &str) -> Cow<'_, str> {
let encoded = utf8_percent_encode(encode_str, NON_ALPHANUMERIC)
.to_string()
.replace("+", "20%")
.replace("%5F", "_")
.replace("%2D", "-")
.replace("%2E", ".")
.replace("%7E", "~");
Cow::Owned(encoded) // 返回一个 Cow<str> 可以持有 String 或 &str
}
/// 计算SHA256哈希
pub fn sha256_hex(message: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(message);
format!("{:x}", hasher.finalize()).to_lowercase()
}
/// HMAC SHA256
pub fn hmac256(key: &[u8], message: &str) -> Result<Vec<u8>, String> {
let mut mac = Hmac::<Sha256>::new_from_slice(key)
.map_err(|e| format!("use data key on sha256 fail:{}", e))?;
mac.update(message.as_bytes());
let signature = mac.finalize();
Ok(signature.into_bytes().to_vec())
}
/// 生成指定长度的随机字符串
pub fn generate_random_string(length: usize) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::thread_rng();
(0..length)
.map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char)
.collect()
}
/// 生成随机字符串作为nonce
pub fn generate_nonce() -> String {
generate_random_string(32)
}
// 定义 FormData 数据类型
#[derive(Debug, Clone)]
pub enum FormValue {
String(String),
// 添加类型:Vec<String>, HashSet<String> 或者 HashMap<String, String> 等
Vec(Vec<String>),
HashMap(HashMap<String, String>),
}
// 定义一个body请求体枚举,用于统一处理请求体类型,包含Json,Map,二进制类型
pub enum RequestBody {
Json(HashMap<String, Value>), // Json
Binary(Vec<u8>), // Binary
FormData(HashMap<String, FormValue>), // FormData
None,
}
/// 规范化请求
pub async fn call_api(
client: Client,
method: Method,
host: &str,
canonical_uri: &str,
query_params: &[(&str, &str)], // 添加$query_params 查询参数
action: &str,
version: &str,
body: RequestBody, // 定义接收请求体body参数 (类型为:Json,map,二进制)
access_key_id: &str,
access_key_secret: &str,
) -> Result<String, String> {
// CanonicalQueryString 构建规范化查询字符串
let canonical_query_string = build_sored_encoded_query_string(query_params);
// 处理 body 请求体内容,判断 boby 类型
let body_content = match &body { // 使用引用来避免移动
RequestBody::Json(body_map) => json!(body_map).to_string(), // 若 body 为map,转化为 &str 类型,存储 body_content 变量中
RequestBody::Binary(_) => String::new(), // 若 body 为二进制类型这里可以保留空字符串,body_content 变量为空
RequestBody::FormData(form_data) => {
let params: Vec<String> = form_data
.iter()
.flat_map(|(k, v)| {
match v {
FormValue::String(s) => {
// 当 FormValue 为 String 时
vec![format!("{}={}", percent_code(k), percent_code(&s))]
},
FormValue::Vec(vec) => {
// 当 FormValue 为 Vec 时
vec.iter()
.map(|s| format!("{}={}", percent_code(k), percent_code(s)))
.collect::<Vec<_>>()
},
FormValue::HashMap(map) => {
// 当 FormValue 为 HashMap 时
map.iter()
.map(|(sk, sv)| format!("{}={}", percent_code(sk), percent_code(sv)))
.collect::<Vec<_>>()
},
}
})
.collect();
params.join("&") // 组成 key=value&key=value 的形式
},
RequestBody::None => String::new(),
};
// 打印 body 和 query
println!("Request Body: {}", body_content);
println!("Query Params: {:?}", query_params);
let hashed_request_payload = sha256_hex(&body_content);
// x-acs-date
let now_time = current_timestamp().map_err(|e| format!("Get current timestamp failed: {}", e))?;
let datetime = DateTime::from_timestamp(now_time as i64, 0).ok_or_else(|| format!("Get datetime from timestamp failed: {}", now_time))?;
let datetime_str = datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string();
// x-acs-signature-nonce
let signature_nonce = generate_nonce();
// 构造请求头
let mut headers = HeaderMap::new();
headers.insert("Host", HeaderValue::from_str(host).unwrap());
headers.insert("x-acs-action", HeaderValue::from_str(action).unwrap());
headers.insert("x-acs-version", HeaderValue::from_str(version).unwrap());
headers.insert("x-acs-date", HeaderValue::from_str(&datetime_str).unwrap());
headers.insert("x-acs-signature-nonce", HeaderValue::from_str(&signature_nonce).unwrap());
headers.insert("x-acs-content-sha256", HeaderValue::from_str(&hashed_request_payload).unwrap());
// 签名的消息头
let sign_header_arr = &[
"host",
"x-acs-action",
"x-acs-content-sha256",
"x-acs-date",
"x-acs-signature-nonce",
"x-acs-version",
];
let sign_headers = sign_header_arr.join(";");
// 规范化请求头
let canonical_request = format!(
"{}\n{}\n{}\n{}\n\n{}\n{}",
method.as_str(),
canonical_uri,
canonical_query_string,
sign_header_arr.iter().map(|&header| format!("{}:{}", header, headers[header].to_str().unwrap())).collect::<Vec<_>>().join("\n"),
sign_headers,
hashed_request_payload
);
// 计算待签名字符串
let result = sha256_hex(&canonical_request);
let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", result);
let signature = hmac256(access_key_secret.as_bytes(), &string_to_sign)?;
let data_sign = hex::encode(&signature);
let auth_data = format!(
"ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}",
access_key_id, sign_headers, data_sign
);
headers.insert("Authorization", HeaderValue::from_str(&auth_data).unwrap());
// 发送请求
let url = format!("https://{}{}", host, canonical_uri);
let response = send_request(
&client,
method,
&url,
headers,
query_params, // 接收 query 参数
&body, // 接收body请求体参数(包含Json,Map,二进制)
&body_content, // 接收body请求体参数。当body为map 或者 formDate 的时候,该变量有值;若body参数为二进制,则该变量为空
)
.await?;
// 读取响应
let (_, res) = read_response(response).await?;
Ok(res)
}
/// 发送请求
async fn send_request(
client: &Client,
method: Method,
url: &str,
headers: HeaderMap,
query_params: &[(&str, &str)], // 接收 query 参数
body: &RequestBody, // 接收body请求体参数(包含Json,Map,二进制)
body_content: &str, // 接收body请求体参数。当body为 map 或者 formDate 的时候,该变量有值;若body参数为二进制,则该变量为空
) -> Result<Response, String> {
let mut request_builder = client.request(method, url);
// 添加 query 参数
if !query_params.is_empty() {
// 这里使用 query_params 直接作为 query 的参数
request_builder = request_builder.query(query_params);
}
// 添加请求头
for (k, v) in headers.iter() {
request_builder = request_builder.header(k, v.clone());
}
// 根据 RequestBody 类型设置请求体
match body {
// 如果body是二进制,设置 application/octet-stream
RequestBody::Binary(binary_data) => {
request_builder = request_builder.header("Content-Type", "application/octet-stream");
request_builder = request_builder.body(binary_data.clone()); // 移动这里的值
}
RequestBody::Json(_) => {
// 如果body为map,且不为空,转化为Json后存储在 body_content 变量中,设置 application/json; charset=utf-8
if !body_content.is_empty() {
request_builder = request_builder.body(body_content.to_string());
request_builder = request_builder.header("Content-Type", "application/json; charset=utf-8");
}
}
RequestBody::FormData(_) => {
// 处理 form-data 类型,设置 content-type
if !body_content.is_empty() {
request_builder = request_builder.header("Content-Type", "application/x-www-form-urlencoded");
request_builder = request_builder.body(body_content.to_string());
}
}
RequestBody::None => {
request_builder = request_builder.body(String::new());
}
}
let request = request_builder
.build()
.map_err(|e| format!("build request fail: {}", e))?;
let response = client
.execute(request)
.await
.map_err(|e| format!("execute request fail: {}", e))?;
Ok(response)
}
/// 读取响应
pub async fn read_response(result: Response) -> Result<(StatusCode, String), String> {
let status = result.status();
let data = result.bytes().await.map_err(|e| format!("Read response body failed: {}", e))?;
let res = match str::from_utf8(&data) {
Ok(s) => s.to_string(),
Err(_) => return Err("Body contains non UTF-8 characters".to_string()),
};
Ok((status, res))
}
/// 构建规范化查询字符串
pub fn build_sored_encoded_query_string(query_params: &[(&str, &str)]) -> String {
// 按参数名升序排序并使用 BTreeMap 处理重复
let sorted_query_params: BTreeMap<_, _> = query_params.iter().copied().collect();
// URI 编码
let encoded_params: Vec<String> = sorted_query_params
.into_iter()
.map(|(k, v)| {
let encoded_key = percent_code(k);
let encoded_value = percent_code(v);
format!("{}={}", encoded_key, encoded_value)
})
.collect();
// 使用 & 连接所有编码后的参数
encoded_params.join("&")
}
/**
* 签名示例,您需要根据实际情况替换main方法中的示例参数。
* ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。
* 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
* 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
* 2. 请求参数在元数据中显示"in": "body",通过body传参。
* 2. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
#[tokio::main]
async fn main() {
// 创建 HTTP 客户端
let client = Client::new();
// env::var()表示通过环境变量获取Access Key ID和Access Key Secret
let access_key_id = env::var("ALIBABA_CLOUD_ACCESS_KEY_ID").expect("Cannot get access key id.");
let access_key_secret = env::var("ALIBABA_CLOUD_ACCESS_KEY_SECRET").expect("Cannot get access key id.");
// RPC接口请求示例一:请求参数"in":"query" POST
let host = "ecs.cn-hangzhou.aliyuncs.com"; // endpoint
let canonical_uri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI
let action = "DescribeInstanceStatus"; // API名称
let version = "2014-05-26"; // API版本号
// RegionId在元数据中显示的类型是String,"in":"query",必填
let query_params = &[("RegionId", "cn-hangzhou")];
// 构建查询参数 InstanceId的在元数据中显示的类型是array,"in":"query",非必填
// let region_id = "cn-hangzhou";
// let instance_ids = vec![
// "i-bp11ht4h2kd1XXXXXXXX",
// "i-bp16maz3h3xgXXXXXXXX",
// "i-bp10r67hmsllXXXXXXXX",
// ];
// // // 将 instance_ids 转换为逗号分隔的字符串
// let instance_id_str = instance_ids.join(",");
// // 创建查询参数时,开始的时候添加 RegionId
// let mut query: Vec<(&str, &str)> = vec![("RegionId", region_id), ("InstanceId", &instance_id_str)];
// let query_params = &query[..];
// RPC接口请求示例二:请求参数"in":"body" POST
// let host = "ocr-api.cn-hangzhou.aliyuncs.com";
// let canonical_uri = "/";
// let action = "RecognizeGeneral";
// let version = "2021-07-07";
// // // 上传文件
// let binary_data = std::fs::read("C:\\Users\\issuser\\Desktop\\img\\001.png").expect("读文件失败"); // 参数必须要直接传文件二进制
// RPC接口请求示例三:请求参数"in": "formData" POST
// let host = "mt.aliyuncs.com";
// let canonical_uri = "/";
// let action = "TranslateGeneral";
// let version = "2018-10-12";
// // Context在元数据中显示的类型是String,"in":"query",非必填
// let query_params = &[("Context", "早上")];
// // FormatType、SourceLanguage、TargetLanguage等参数,在元数据中显示"in":"formData"
// let mut body = HashMap::new(); // HashMap<String, FormValue> FormValue 可支持Vec<String>, HashSet<String> 或者 HashMap<String, String> ...,更多类型可在FormValue枚举中添加
// body.insert(String::from("FormatType"),FormValue::String(String::from("text")));
// body.insert(String::from("SourceLanguage"),FormValue::String(String::from("zh")));
// body.insert(String::from("TargetLanguage"),FormValue::String(String::from("en")));
// body.insert(String::from("SourceText"),FormValue::String(String::from("你好")));
// body.insert(String::from("Scene"),FormValue::String(String::from("general")));
// ROA接口POST请求 API:CreateCluster创建集群
// 定义API请求常量
// let host = "cs.cn-beijing.aliyuncs.com";
// let canonical_uri = "/clusters";
// let action = "CreateCluster";
// let version = "2015-12-15";
// // 设置请求体参数
// let mut body = HashMap::new(); // HashMap<String, Value> Value支持类型:Value::String("test".to_string()) // String Value::Number(serde_json::Number::from(42)) // Number Value::Bool(true) // Boolean Value::Null // Null Value::Array(vec![Value::from(1), Value::from(2), Value::from(3)]) //Array json!({"nested_key": "nested_value"})
// body.insert(String::from("name"),json!("测试集群"));
// body.insert(String::from("region_id"),json!("cn-beijing"));
// body.insert(String::from("cluster_type"),json!("ExternalKubernetes"));
// body.insert(String::from("vpcid"),json!("vpc-2zeou1uod4ylaXXXXXXXX"));
// body.insert(String::from("container_cidr"),json!("10.0.0.0/8"));
// body.insert(String::from("service_cidr"),json!("10.2.0.0/24"));
// body.insert(String::from("security_group_id"),json!("sg-2ze1a0rlgeo7XXXXXXXX"));
// body.insert(String::from("vswitch_ids"),json!(vec!["vsw-2zei30dhfldu8XXXXXXXX"]));
// ROA接口GET请求 API:DeleteCluster 查询指定集群的关联资源
// let host = "cs.cn-beijing.aliyuncs.com"; // endpoint
// // 拼接资源路径
// let uri = format!("/clusters/{}/resources", percent_code("c1311ba68f3af45f39ee3f4d2XXXXXXXX").as_ref());
// let canonical_uri = uri.as_str(); // 资源路径 转化为&Str类型
// let action = "DescribeClusterResources"; // API名称
// let version = "2015-12-15"; // API版本号
// // 设置查询参数
// let query_params = &[("with_addon_resources", if true { "true" } else { "false" })]; // "true" or "false"
// ROA接口DELETE请求 API:DeleteCluster DELETE请求删除一个按量付费的集群
// let host = "cs.cn-beijing.aliyuncs.com";
// let uri = format!("/clusters/{}", percent_code("c72b778e79d3647cdb95c8b86XXXXXXXX").as_ref());
// let canonical_uri = uri.as_str(); // 资源路径转化为&Str类型
// let action = "DeleteCluster";
// let version = "2015-12-15";
// 调用接口
match call_api(
client.clone(),
Method::POST, // 请求方法:GET,DELETE,PUT,POST
host, // endpoint
canonical_uri, // 资源路径
// &[], // 当查询参数为空时,query_params 设置为 &[]
query_params, // 当查询参数不为空时, query_params 设置为 &[("K", "V")]
action,
version,
RequestBody:: None, // 当body参数类型为空时,使用 RequestBody:: None 设置为 None;
// RequestBody::Json(body), // 当body参数类型为Map时,使用 RequestBody::Json 传递 Map
// RequestBody::Binary(binary_data), // 当body参数类型为二进制时,使用 RequestBody::Binary 传递二进制数据
// RequestBody::FormData(body), // 当body参数类型为 formDate 时,使用 RequestBody::FormData 传递 FormData
access_key_id,
access_key_secret,
)
.await {
Ok(response) => println!("响应信息: {}", response),
Err(error) => eprintln!("异常: {}", error),
}
}
Shell脚本示例
#!/bin/bash
accessKey_id="<YOUR-ACCESSKEY-ID>"
accessKey_secret="<YOUR-ACCESSKEY-SECRET>"
algorithm="ACS3-HMAC-SHA256"
# 请求参数 --这部分内容需要根据实际情况修改
httpMethod="POST"
host="dns.aliyuncs.com"
queryParam=("DomainName=example.com" "RRKeyWord=@")
action="DescribeDomainRecords"
version="2015-01-09"
canonicalURI="/"
# body类型参数或者formdata类型参数通过body传参
# body类型参数:body的值为json字符串: "{'key1':'value1','key2':'value2'}",且需在签名header中添加content-type:application/json; charset=utf-8
# body类型参数是二进制文件时:body无需修改,只需在签名header中添加content-type:application/octet-stream,并在curl_command中添加--data-binary参数
# formdata类型参数:body参数格式:"key1=value1&key2=value2",且需在签名header中添加content-type:application/x-www-form-urlencoded
body=""
# 按照ISO 8601标准表示的UTC时间
utc_timestamp=$(date +%s)
utc_date=$(date -u -d @${utc_timestamp} +"%Y-%m-%dT%H:%M:%SZ")
# x-acs-signature-nonce 随机数
random=$(uuidgen | sed 's/-//g')
# 签名header
headers="host:${host}
x-acs-action:${action}
x-acs-version:${version}
x-acs-date:${utc_date}
x-acs-signature-nonce:${random}"
# URL编码函数
urlencode() {
local string="${1}"
local strlen=${#string}
local encoded=""
local pos c o
for (( pos=0 ; pos<strlen ; pos++ )); do
c=${string:$pos:1}
case "$c" in
[-_.~a-zA-Z0-9] ) o="${c}" ;;
* ) printf -v o '%%%02X' "'$c"
esac
encoded+="${o}"
done
echo "${encoded}"
}
# 步骤 1:拼接规范请求串
# 将queryParam中的参数全部平铺
newQueryParam=()
# 遍历每一个原始参数
for param in "${queryParam[@]}"; do
# 检查是否包含等号,以确定是键值对
if [[ "$param" == *"="* ]]; then
# 分割键和值
IFS='=' read -r key value <<< "$param"
# 对值进行URL编码
value=$(urlencode "$value")
# 检查值是否为一个列表(通过查找括号)
if [[ "$value" =~ ^\(.+\)$ ]]; then
# 去掉两边的括号
value="${value:1:-1}"
# 使用IFS分割值列表
IFS=' ' read -ra values <<< "$value"
# 对于每个值添加索引
index=1
for val in "${values[@]}"; do
# 去除双引号
val="${val%\"}"
val="${val#\"}"
# 添加到新数组
newQueryParam+=("$key.$index=$val")
((index++))
done
else
# 如果不是列表,则直接添加
newQueryParam+=("$key=$value")
fi
else
# 如果没有等号,直接保留原样
newQueryParam+=("$param")
fi
done
# 处理并排序新的查询参数
sortedParams=()
declare -A paramsMap
for param in "${newQueryParam[@]}"; do
IFS='=' read -r key value <<< "$param"
paramsMap["$key"]="$value"
done
# 根据键排序
for key in $(echo ${!paramsMap[@]} | tr ' ' '\n' | sort); do
sortedParams+=("$key=${paramsMap[$key]}")
done
# 1.1 拼接规范化查询字符串
canonicalQueryString=""
first=true
for item in "${sortedParams[@]}"; do
[ "$first" = true ] && first=false || canonicalQueryString+="&"
# 检查是否存在等号
if [[ "$item" == *=* ]]; then
canonicalQueryString+="$item"
else
canonicalQueryString+="$item="
fi
done
# 1.2 处理请求体
hashedRequestPayload=$(echo -n "$body" | openssl dgst -sha256 | awk '{print $2}')
headers="${headers}
x-acs-content-sha256:$hashedRequestPayload"
# 1.3 构造规范化请求头
canonicalHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
value=$(echo "$line" | cut -d':' -f2-)
echo "${key}:${value}"
done | sort | tr '\n' '\n')
signedHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
echo "$key"
done | sort | tr '\n' ';' | sed 's/;$//')
# 1.4 构造规范请求
canonicalRequest="${httpMethod}\n${canonicalURI}\n${canonicalQueryString}\n${canonicalHeaders}\n\n${signedHeaders}\n${hashedRequestPayload}"
echo -e "canonicalRequest=${canonicalRequest}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
str=$(echo "$canonicalRequest" | sed 's/%/%%/g')
hashedCanonicalRequest=$(printf "${str}" | openssl sha256 -hex | awk '{print $2}')
# 步骤 2:构造签名字符串
stringToSign="${algorithm}\n${hashedCanonicalRequest}"
echo -e "stringToSign=$stringToSign"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
# 步骤 3:计算签名
signature=$(printf "${stringToSign}" | openssl dgst -sha256 -hmac "${accessKey_secret}" | sed 's/^.* //')
echo -e "signature=${signature}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"
# 步骤 4:构造Authorization
authorization="${algorithm} Credential=${accessKey_id},SignedHeaders=${signedHeaders},Signature=${signature}"
echo -e "authorization=${authorization}"
# 构造 curl 命令
url="https://$host$canonicalURI"
curl_command="curl -X $httpMethod '$url?$canonicalQueryString'"
# 添加请求头
IFS=$'\n' # 设置换行符为新的IFS
for header in $headers; do
curl_command="$curl_command -H '$header'"
done
curl_command+=" -H 'Authorization:$authorization'"
# body类型参数是二进制文件时,需要注释掉下面这行代码
curl_command+=" -d '$body'"
# body类型参数是二进制文件时,需要放开下面这行代码的注释
#curl_command+=" --data-binary @"/root/001.png" "
echo "$curl_command"
# 执行 curl 命令
eval "$curl_command"
C语言示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>
#include <stdint.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/rand.h>
#include <curl/curl.h>
// getenv()表示通过环境变量获取Access Key ID和Access Key Secret
#define ACCESS_KEY_ID getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
#define ACCESS_KEY_SECRET getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
#define ALGORITHM "ACS3-HMAC-SHA256"
#define BUFFER_SIZE 4096
// HMAC-SHA256计算
void hmac256(const char *key, const char *message, char *output) {
unsigned char hmac[SHA256_DIGEST_LENGTH];
unsigned int result_len;
// 计算 HMAC
HMAC(EVP_sha256(), key, strlen(key), (unsigned char *)message, strlen(message), hmac, &result_len);
// 将 HMAC 转换为十六进制字符串输出
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
sprintf(output + (i * 2), "%02x", hmac[i]);
}
output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// 计算 SHA-256 哈希
void sha256_hex(const char *input, char *output) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char *)input, strlen(input), hash);
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
sprintf(output + (i * 2), "%02x", hash[i]);
}
output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// URL编码
char* percentEncode(const char* str) {
if (str == NULL) {
fprintf(stderr, "输入字符串不可为null\n");
return NULL;
}
size_t len = strlen(str);
// 最坏情况下,每个字符都需要被编码为3个字符(%XX),所以分配的空间为3倍
char* encoded = (char*)malloc(len * 3 + 1);
if (encoded == NULL) {
fprintf(stderr, "内存分配失败\n");
return NULL;
}
char* ptr = encoded;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
// 如果字符是安全的,可以直接添加
*ptr++ = c;
} else {
// 否则,进行URL编码
ptr += sprintf(ptr, "%%%02X", c);
}
}
*ptr = '\0'; // 以null字符结尾
// 处理加号的替换
char* finalEncoded = malloc(strlen(encoded) + 1);
if (finalEncoded) {
char* fptr = finalEncoded;
for (size_t j = 0; j < strlen(encoded); j++) {
if (encoded[j] == '+') {
strcpy(fptr, "%20");
fptr += 3; // 移动指针
} else if (encoded[j] == '*') {
strcpy(fptr, "%2A");
fptr += 3;
} else if (encoded[j] == '~') {
*fptr++ = '~';
} else {
*fptr++ = encoded[j];
}
}
*fptr = '\0'; // 以null字符结尾
}
free(encoded); // 释放临时编码的空间
return finalEncoded;
}
// nonce随机数
void generate_uuid(char *uuid, size_t size) {
if (size < 37) { // UUID格式需要36个字符+结束符
fprintf(stderr, "Buffer size too small for UUID\n");
return;
}
// 使用随机数生成器生成16个随机字节
unsigned char random_bytes[16];
RAND_bytes(random_bytes, sizeof(random_bytes));
// 有效版本是4,用于随机生成的UUID
random_bytes[6] &= 0x0f; // 保留高4位
random_bytes[6] |= 0x40; // 将版本设置为4
random_bytes[8] &= 0x3f; // 保留高2位
random_bytes[8] |= 0x80; // 将变体设置为10xx
// 格式化为UUID字符串
snprintf(uuid, size,
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
}
// 二进制读取文件
size_t read_file(const char *file_path, char **buffer) {
FILE *file = fopen(file_path, "rb");
if (!file) {
fprintf(stderr, "Cannot open file %s\n", file_path);
return 0; // 读取失败
}
fseek(file, 0, SEEK_END);
size_t file_size = ftell(file);
fseek(file, 0, SEEK_SET);
*buffer = (char *)malloc(file_size);
if (!*buffer) {
fprintf(stderr, "Failed to allocate memory for file buffer\n");
fclose(file);
return 0; // 读取失败
}
fread(*buffer, 1, file_size, file);
fclose(file);
return file_size; // 返回读取的字节数
}
// 获取 authorization
void get_authorization(const char *http_method, const char *canonical_uri, const char *host,
const char *x_acs_action, const char *x_acs_version, const char *query_params,
const char *body, char *authorization_header,
char *hashed_payload, char *x_acs_date, char *uuid) {
// 生成UUID
generate_uuid(uuid, 37);
// 生成x-acs-date
time_t now = time(NULL);
struct tm *utc_time = gmtime(&now);
strftime(x_acs_date, 64, "%Y-%m-%dT%H:%M:%SZ", utc_time);
// 打印时间戳
printf("Generated x-acs-date: %s\n", x_acs_date);
// 计算请求体的 SHA-256 哈希 (即 x-acs-content-sha256)
sha256_hex(body ? body : "", hashed_payload);
// 打印哈希值
printf("Generated x-acs-content-sha256: %s\n", hashed_payload);
char canonical_headers[BUFFER_SIZE];
snprintf(canonical_headers, sizeof(canonical_headers),
"host:%s\nx-acs-action:%s\nx-acs-content-sha256:%s\nx-acs-date:%s\nx-acs-signature-nonce:%s\nx-acs-version:%s",
host, x_acs_action, hashed_payload, x_acs_date, uuid, x_acs_version);
// 打印规范化的请求头
printf("Canonical Headers:===============>\n%s\n", canonical_headers);
// 签名字段
char signed_headers[] = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
char canonical_request[BUFFER_SIZE];
snprintf(canonical_request, sizeof(canonical_request), "%s\n%s\n%s\n%s\n\n%s\n%s",
http_method, canonical_uri, query_params ? strdup(query_params) : "", // 使用percentCode
canonical_headers, signed_headers, hashed_payload);
// 打印规范化请求
printf("Canonical Request:\n%s\n", canonical_request);
// 计算规范化请求的SHA-256哈希
char hashed_canonical_request[SHA256_DIGEST_LENGTH * 2 + 1];
sha256_hex(canonical_request, hashed_canonical_request);
// 打印哈希后的规范化请求
printf("hashedCanonicalRequest: %s\n", hashed_canonical_request);
// 构建待签名字符串
char string_to_sign[BUFFER_SIZE];
snprintf(string_to_sign, sizeof(string_to_sign), "%s\n%s", ALGORITHM, hashed_canonical_request);
// 打印待签名字符串
printf("stringToSign:\n%s\n", string_to_sign);
// 计算签名
char signature[SHA256_DIGEST_LENGTH * 2 + 1];
hmac256(ACCESS_KEY_SECRET, string_to_sign, signature);
// 打印签名
printf("Signature: %s\n", signature);
// 构建最终的 Authorization 头,包含 SignedHeaders
snprintf(authorization_header, BUFFER_SIZE,
"%s Credential=%s,SignedHeaders=%s,Signature=%s",
ALGORITHM, ACCESS_KEY_ID, signed_headers, signature);
// 打印 Authorization
printf("Authorization: %s\n", authorization_header);
}
// 发送请求
void call_api(const char *http_method, const char *canonical_uri, const char *host,
const char *x_acs_action, const char *x_acs_version, const char *query_params,
const char *body,const char *content_type, size_t body_length) {
// 获取签名所需的参数值
char authorization_header[BUFFER_SIZE];
char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
char x_acs_date[64];
char uuid[37];
// 获取授权头
get_authorization(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, authorization_header, hashed_payload, x_acs_date, uuid);
// 拼接请求路径 URL
char url[BUFFER_SIZE];
if (query_params && strlen(query_params) > 0) {
snprintf(url, sizeof(url), "https://%s%s?%s", host, canonical_uri, query_params);
} else {
snprintf(url, sizeof(url), "https://%s%s", host, canonical_uri);
}
// 打印请求url
printf("Request URL: %s\n", url);
// 初始化cURL
CURL *curl = curl_easy_init();
if(!curl) {
fprintf(stderr, "curl_easy_init() failed\n");
return;
}
// 定义数组用于添加请求头
struct curl_slist *headers = NULL;
// 创建字符数组以存储请求头并添加 headers
char header_value[BUFFER_SIZE];
// 添加headers信息
snprintf(header_value, sizeof(header_value), "Content-Type: %s", content_type);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "Authorization: %s", authorization_header);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "host: %s", host);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-action: %s", x_acs_action);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-content-sha256: %s", hashed_payload);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-date: %s", x_acs_date);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-signature-nonce: %s", uuid);
headers = curl_slist_append(headers, header_value);
snprintf(header_value, sizeof(header_value), "x-acs-version: %s", x_acs_version);
headers = curl_slist_append(headers, header_value);
// 设置cURL选项
// 设置cURL请求方法
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); // 设置 HTTP 方法
// 设置 url
curl_easy_setopt(curl, CURLOPT_URL, url);
// 禁用 SSL 验证,(调试)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
// 添加调试信息
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
// 添加请求头
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// 添加 body 请求体
if (body) {
// 设置请求体的大小
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_length);
if (strcmp(content_type, "application/octet-stream") == 0) {
// 添加请求体
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
} else if (strcmp(content_type, "application/x-www-form-urlencoded") == 0) {
// 添加请求体
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
} else if (strcmp(content_type, "application/json; charset=utf-8") == 0) {
// 添加请求体
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
}
}
// 打印添加的 headers
struct curl_slist *header_ptr = headers;
while (header_ptr) {
printf("Header: %s\n", header_ptr->data);
header_ptr = header_ptr->next;
}
// 执行请求并检查响应
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
return;
}
// 清理
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
/**
*
* 签名示例,您需要根据实际情况替换main方法中的示例参数。
* <p>
* 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。
* 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。
* 2. 请求参数在元数据中显示"in": "body",通过body传参。
* 2. 请求参数在元数据中显示"in": "formData",通过body传参。
*/
int main() {
// 设置响应格式为UTF-8
SetConsoleOutputCP(CP_UTF8);
srand((unsigned int)time(NULL));
/**
* RPC接口请求示例:请求参数"in":"query"
*/
// 定义API请求参数
const char *http_method = "POST";
const char *canonical_uri = "/";
const char *host = "ecs.cn-hangzhou.aliyuncs.com";
const char *x_acs_action = "DescribeInstanceStatus";
const char *x_acs_version = "2014-05-26";
// 定义参数 InstanceId 数组 InstanceId为 可选参数
const char *instance_ids[] = {
"i-bp11ht4h2kd1ic5fXXXX",
"i-bp16maz3h3xg83raXXXX"
};
// 拼接InstanceId数组
char InstanceId[BUFFER_SIZE];
snprintf(InstanceId, sizeof(InstanceId),
"InstanceId.1=%s&InstanceId.2=%s",
percentEncode(instance_ids[0]),
percentEncode(instance_ids[1]));
// 定义查询参数 必填参数 :RegionId=cn-hangzhou const char *query_params = "RegionId=cn-hangzhou";
char query_params[BUFFER_SIZE];
snprintf(query_params, sizeof(query_params),
"%s&RegionId=cn-hangzhou", InstanceId);
// 打印参数
printf("Query Params: %s\n", query_params);
// body 为空
const char *body = "";
const char *content_type = "application/json; charset=utf-8";
call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* RPC接口请求示例:请求参数"in":"body"
*/
// 声明存储读取的文件内容的指针
// char *body = NULL;
// // 获取文件长度
// size_t body_length = read_file("C:\\Users\\issuser\\Desktop\\img\\001.png", &body);
//
// if (body_length > 0) {
// const char *http_method = "POST";
// const char *canonical_uri = "/";
// const char *host = "ocr-api.cn-hangzhou.aliyuncs.com";
// const char *x_acs_action = "RecognizeGeneral";
// const char *x_acs_version = "2021-07-07";
// // 定义查询参数
// const char *query_params = "";
// const char *content_type = "application/octet-stream";
//
// // 调用 API
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, body_length);
//
// // 释放分配的内存
// free(body);
// } else {
// fprintf(stderr, "File read error\n");
// }
/**
* RPC接口请求示例:请求参数"in": "formData"
*/
// const char *http_method = "POST";
// const char *canonical_uri = "/";
// const char *host = "mt.aliyuncs.com";
// const char *x_acs_action = "TranslateGeneral";
// const char *x_acs_version = "2018-10-12";
// // formdate类型,编码查询参数的Value
// // 定义查询参数 Context="早上"
// char query_params[BUFFER_SIZE];
// snprintf(query_params, sizeof(query_params), "Context=%s", percentEncode("早上"));
//
// // 定义formdate类型参数的value值
// const char *format_type = "text";
// const char *source_language = "zh";
// const char *target_language = "en";
// const char *source_text = "你好";
// const char *scene = "general";
// // body 为 formdate类型 构建表单数据字符串,进行编码
// char body[BUFFER_SIZE];
// snprintf(body, sizeof(body),
// "FormatType=%s&SourceLanguage=%s&TargetLanguage=%s&SourceText=%s&Scene=%s",
// percentEncode(format_type), percentEncode(source_language), percentEncode(target_language),
// percentEncode(source_text), percentEncode(scene));
// const char *content_type = "application/x-www-form-urlencoded";
// printf("formdate_body: %s\n", body);
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* ROA接口POST请求
*/
// const char *http_method = "POST";
// const char *canonical_uri = "/clusters";
// const char *host = "cs.cn-beijing.aliyuncs.com";
// const char *x_acs_action = "CreateCluster";
// const char *x_acs_version = "2015-12-15";
// // 定义查询参数
// const char *query_params = "";
// // body 为json类型
// // 构建JSON格式的请求体
// char body[BUFFER_SIZE];
// snprintf(body, sizeof(body),
// "{\"name\":\"%s\",\"region_id\":\"%s\",\"cluster_type\":\"%s\","
// "\"vpcid\":\"%s\",\"container_cidr\":\"%s\","
// "\"service_cidr\":\"%s\",\"security_group_id\":\"%s\","
// "\"vswitch_ids\":[\"%s\"]}",
// "测试集群", "cn-beijing", "ExternalKubernetes",
// "vpc-2zeou1uod4ylaf35tXXXX", "10.0.0.0/8",
// "10.2.0.0/24", "sg-2ze1a0rlgeo7dj37XXXX",
// "vsw-2zei30dhfldu8ytmtXXXX");
// // 打印请求体
// printf("Request Body: %s\n", body);
// const char *content_type = "application/json; charset=utf-8";
// // 发送请求
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* ROA接口GET请求
*/
// const char *http_method = "GET";
// // 拼接url 拼接请求参数 canonical_uri:/clusters/cluster_id/resources
// char canonical_uri[BUFFER_SIZE];
// snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s/resources", percentEncode("cd1f5ba0dbfa144c69e48b75df47bXXXX"));
// // 打印资源路径
// printf("canonical_uri: %s\n", canonical_uri);
//
// const char *host = "cs.cn-beijing.aliyuncs.com";
// const char *x_acs_action = "DescribeClusterResources";
// const char *x_acs_version = "2015-12-15";
// // 定义查询参数
// const char *query_params = "with_addon_resources=true";
// // 定义body
// const char *body = "";
// const char *content_type = "";
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
/**
* ROA接口DELETE请求
*/
// const char *http_method = "DELETE";
// // 拼接url 拼接请求参数 canonical_uri:/clusters/cluster_id
// char canonical_uri[BUFFER_SIZE];
// snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s", percentEncode("cd1f5ba0dbfa144c69e48b75df47bXXXX"));
// // 打印资源路径
// printf("canonical_uri: %s\n", canonical_uri);
// const char *host = "cs.cn-beijing.aliyuncs.com";
// const char *x_acs_action = "DeleteCluster";
// const char *x_acs_version = "2015-12-15";
// // 定义查询参数
// const char *query_params = "";
// // 定义body
// const char *body = "";
// const char *content_type = "";
// call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));
// 变量存储生成的值
char authorization_header[BUFFER_SIZE];
char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
char x_acs_date[64];
char uuid[37];
return 0;
}
常见问题
签名失败,提示“Specified signature does not match our calculation.”或者“The request signature does not conform to Aliyun standards.”
问题原因:出现该问题通常是由于参数传递错误或者在签名机制的某个步骤中遗漏了必要的操作。常见的问题原因:
参数可能是要拼接在URL上,但却使用了body传参。
CanonicalQueryString中参数未按字符顺序升序排列。
SignedHeaders未按升序排列。
未将空格编码为
%20
。
解决方案:
建议您先根据固定参数示例检查本地代码计算结果是否正确。
从报错信息中获取CanonicalRequest和StringToSign的值。
首先,请对比报错信息中的CanonicalRequest与您计算的CanonicalRequest内容是否一致。若存在不一致,请查看步骤一:构造规范化请求,并检查您的代码中与文档介绍内容不一致的部分。常见的错误之一是参数传递位置不当,例如参数应当拼接在URL中,但却错误地通过请求体(body)进行传递,这将导致参数
CanonicalQueryString
和x-acs-content-sha256
的结果均出现错误。当报错信息中的CanonicalRequest与您计算的CanonicalRequest内容一致时,请对比StringToSign与您计算的StringToSign的值是否一致。需要注意的是,StringToSign中
HashedCanonicalRequest
的值应使用与x-acs-content-sha256
相同的函数生成。若仍然无法解决,请联系我们。
请求参数该如何传
在OpenAPI元数据中定义了每个API的请求参数该如何传。例如:
"in":"query"
表示参数需拼接在URL上,无需添加content-type。"in": "body"
表示通过body传参,需要在RequestHeader中添加content-type,content-type的值与请求内容类型有关。例如:请求内容类型为JSON数据时,content-type的值为
application/json
。请求内容类型为二进制文件流时,content-type的值为
application/octet-stream
。
"in": "formData"
表示通过body传参。在传参时,需要将参数按照固定格式拼接为一个字符串,拼接格式为:key1=value1&key2=value2&key3=value3
。同时需要在RequestHeader中添加content-type,content-type的值为application/x-www-form-urlencoded
。
API元数据中style
的值不是RPC或ROA
RPC或ROA仅影响CanonicalURI的值。当style
的值是其他类型时,可在API元数据中检查将调用的API概要信息中是否包含path
参数,若存在,则CanonicalURI的值为该path
的值,若不存在,则CanonicalURI的值为正斜杠(/
)。例如在查询ACK集群列表的API元数据中path
的值为/api/v1/clusters
,那么CanonicalURI的值为/api/v1/clusters
。
当参数类型是集合时,该如何传参
当参数类型是复杂数据结构时,需要将复杂数据结构平铺为一个映射结构(map)。例如DescribeInstanceStatus接口中InstanceId的参数类型是array,当参数值有多个时,需要将参数转换为如下结构:
{
"InstanceId.1": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.10": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.11": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.12": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.2": "i-bp1incuofvzxXXXXXXXX",
"InstanceId.3": "i-bp1incuofvzxXXXXXXXX",
"InstanceId.4": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.5": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.6": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.7": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.8": "i-bp10igfmnyttXXXXXXXX",
"InstanceId.9": "i-bp10igfmnyttXXXXXXXX"
}
自签名时,如果使用GET调试成功了,可以使用POST吗?
对于RPC接口,通常既支持GET请求也支持POST请求。
对于ROA接口,则仅支持单一的请求方式。
如何获取API支持的请求方式,请参见OpenAPI元数据。
调用API时提示“You are not authorized to do this operation. ”
问题原因:您所使用的AccessKey对应的RAM用户没有权限调用该API。
解决方法:请为该RAM用户授予相应云产品的管理或只读权限。关于如何为RAM用户进行授权,请参见为RAM用户授权。
如何获取AccessKey
AccessKey是阿里云为用户提供的永久访问凭据,由AccessKey ID和AccessKey Secret组成的一对密钥。在通过调用API访问阿里云资源时,系统将对请求中携带的AccessKey ID以及通过AccessKey Secret加密生成的签名进行身份验证和请求合法性校验。如何获取AccessKey,请参见创建RAM用户的AccessKey。
联系我们
当您在计算签名时遇到无法解决的问题,可以加入钉钉群:78410016550
,联系值班同学进行咨询。