V3版本请求体&签名机制
V3版本通过公共请求头设置接口必要的参数信息,在签名机制的实现上屏蔽了接口风格的差异,更标准、更简单。本文提供了详细的指南,用于帮助您了解和实施阿里云SDK V3版的请求结构和签名过程。您会了解如何构造标准的HTTP请求,以及如何使用正确的签名算法来验证请求的身份,确保传输的数据的完整性和安全性。如果您想自研阿里云OpenAPI的请求签名,您可以参考本文。
HTTP 请求结构
一个完整的阿里云 OpenAPI 请求,包含以下部分。
名称 | 是否必选 | 描述 | 示例值 |
协议 | 是 | 您可以查阅不同云产品的 API 参考文档进行配置。支持通过 | https:// |
服务地址 | 是 | 即 Endpoint。您可以查阅不同云产品的服务接入地址文档,查阅不同服务区域下的服务地址。 | cs.aliyuncs.com |
resource_URI_parameters(接口URL) | 是 | 接口URL,包括接口路径和位置在 path、 query的接口请求参数。 | /clusters/{cluster_id}/triggers |
RequestHeader(请求头信息) | 是 | 请求头信息,通常包含API的版本、Host、Authorization等信息。后文将详细说明。 | x-acs-action |
RequestBody | 是 | 定义在 body 中的业务请求参数,建议您在阿里云 OpenAPI 开发者门户进行试用。 | cluster_id |
HTTPMethod | 是 | 请求使用的方法,ROA接口请求方法包括PUT、POST、GET、DELETE。 | POST |
RequestHeader(公共请求头)
一个完整的阿里云 OpenAPI 请求,包含以下部分。
名称 | 类型 | 是否必选 | 描述 | 示例值 |
x-acs-action | String | 是 | API的名称。您可以访问阿里云 OpenAPI 开发者门户,搜索您想调用的 OpenAPI。 | RunInstances |
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=e521358f7776c97df52e6b2891a8bc73026794a071b50c3323388c4e0df64804 |
x-acs-signature-nonce | String | 是 | 签名唯一随机数。该随机数用于防止网络重放攻击,每一次请求都必须使用不同的随机数。 | d410180a5abf7fe235dd9b74aca91fc0 |
x-acs-date | String | 是 | 按照ISO 8601标准表示的UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ,例如2018-01-01T12:00:00Z。值为请求发出前15分钟内的时间。 | 2023-10-26T09:01:01Z |
host | String | 是 | 即服务地址,参见HTTP 请求结构。 | ecs.cn-shanghai.aliyuncs.com |
x-acs-content-sha256 | String | 是 | 请求正文Hash摘要后再base-16编码的结果,与HashedRequestPayload一致。 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
x-acs-security-token | String | STS认证必传 | 为调用AssumeRole接口返回值中SecurityToken的值。 |
前提条件
在签名时,您需了解产品的API风格,以及API所支持的网络协议、请求方式、参数及其位置等相关信息。
访问API文档,选择对应的云产品,本文以云服务器ECS为例。单击云服务器ECS名称下面的获取元数据,在元数据中
info.style
查看云产品支持的API风格(RPC或者ROA)。说明获取的元数据中包含了云产品的所有API信息,如果您想要查看单个API的元数据,请查看步骤2。
选择将要调用的API,单击右上角获取元数据。
在元数据中,定义了API支持的网络协议、请求方式、参数及参数位置等信息。如下图所示的RunInstances元数据中:
支持的网络协议有HTTP和HTTPS,建议使用HTTPS。
支持的请求方式有GET和POST,两者请求方式调用结果无任何差异,但GET请求只支持 32 KB 以内的请求包,所以推荐使用POST请求。
支持的参数有RegionId、ImageId等,参数位置在query,表示参数是要拼接在请求URL后面,例如https://ecs.cn-beijing.aliyuncs.com/?ImageId=aliyun_2_1903_x64_20G_alibase_20231221.vhd&InstanceChargeType=PostPaid&InstanceType=ecs.e-c1m1.large&InternetChargeType=PayByTraffic&MinAmount=1&Password=test%401234&RegionId=cn-beijing&SecurityGroupId=sg-2zec0dm6qi66XXXXXXXX&SystemDisk.Category=cloud_essd&SystemDisk.Size=40&VSwitchId=vsw-2ze3aagwn397gXXXXXXXX。
说明元数据中其他支持的内容对签名无影响,这里暂不详细说明。更多元数据的信息,请参见元数据使用指南。
签名机制
为保证API的安全调用,在调用API时阿里云会对每个API请求通过签名(Signature)进行身份验证。无论使用HTTP还是HTTPS协议提交请求,都需要在请求中包含签名信息。
请求及返回结果都使用UTF-8字符集进行编码。
对于每一次HTTP或者HTTPS协议请求,阿里云会根据访问中的签名信息验证访问请求者身份。您在访问时签名信息时,请按照以下方法对请求进行签名处理:
步骤一:构造规范化请求
使用AK/SK方式进行签名与认证,首先需要规范请求内容,然后再进行签名。客户端与云服务API网关使用相同的请求规范,可以确保同一个HTTP请求的前后端得到相同的签名结果,从而完成身份校验。
构造规范化请求(CanonicalRequest)的伪代码如下:
CanonicalRequest =
HTTPRequestMethod + '\n' + //http请求方法,全大写
CanonicalURI + '\n' + //规范化URI
CanonicalQueryString + '\n' + //规范化查询字符串
CanonicalHeaders + '\n' + //规范化消息头
SignedHeaders + '\n' + //已签名消息头
HashedRequestPayload
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该参数为元数据文件中
path
的值,例如/api/v1/clusters。CanonicalQueryString
规范化查询字符串,构造方法如下:
将查询字符串中的参数按照参数名的字符代码升序排列,具有重复名称的参数应按值进行排序。
使用UTF-8字符集按照RFC3986的规则对每个参数的参数名和参数值分别进行URI编码,具体规则与上一节中的CanonicalURI编码规则相同。
使用等号(
=
)连接编码后的请求参数名和参数值,对于没有值的参数使用空字符串。多个请求参数之间使用与号(
&
)连接。
重要当请求的查询字符串为空时,使用空字符串作为规范化查询字符串。
CanonicalHeaders
规范化请求头,是一个非标准HTTP头部信息。需要将请求中包含以
x-acs-
为前缀、host
、content-type
的参数信息,添加到规范化请求头中,构造方法如下:将所有需要签名的参数的名称转换为小写。
将所有参数按照参数名称的字符顺序以升序排列。
将参数的值去除首尾空格。对于有多个值的参数,将多个值分别去除首尾空格后按值升序排列,然后用逗号(
,
)连接。将步骤2、3的结果以英文冒号(
:
)连接,并在尾部添加换行符,组成一个规范化消息头(CanonicalHeaderEntry)。如果没有需要签名的消息头,使用空字符串作为规范化消息头列表。
重要除Authorization外的所有公共请求头,只要符合要求的参数都必须被加入签名。
伪代码如下:
CanonicalHeaderEntry = Lowercase(HeaderName) + ':' + Trim(HeaderValue) + '\n' CanonicalHeaders = CanonicalHeaderEntry0 + CanonicalHeaderEntry1 + ... + CanonicalHeaderEntryN
SignedHeaders
已签名消息头列表,用于说明此次请求中参与签名的消息头,与CanonicalHeaders中包含的消息头一一对应。其构造方法如下:
将CanonicalHeaders中包含的请求头的名称转为小写。
多个请求头名称(小写)按首字母升序排列并以英文分号(
;
)分隔,例如content-type;host;x-acs-date
。
伪代码如下:
SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ... + Lowercase(HeaderNameN)
HashedRequestPayload
当请求体(body)为空时,RequestPayload固定为空字符串,否则RequestPayload的值为请求体(body)对应的JSON字符串。再使用哈希函数对RequestPayload进行转换得到HashedRequestPayload,转换规则用伪代码可表示为
HashedRequestPayload = HexEncode(Hash(RequestPayload))
。Hash表示消息摘要函数,目前支持SHA256算法,例如,当签名协议使用ACS3-HMAC-SHA256时,应使用SHA256作为Hash函数。
HexEncode表示以小写的十六进制的形式返回摘要的编码函数(即Base16编码)。
表1:签名协议与签名算法、摘要函数的对应关系
签名协议(SignatureAlgorithm) | 处理RequestPayload以及CanonicalRequest时使用的摘要函数(Hash) | 计算签名时实际使用的签名算法 (SignatureMethod) |
ACS3-HMAC-SHA256 | SHA256 | HMAC-SHA256 |
步骤二:构造待签名字符串
按照以下伪代码构造待签名字符串(stringToSign):
StringToSign =
SignatureAlgorithm + '\n' +
HashedCanonicalRequest
SignatureAlgorithm
签名协议,目前支持ACS3-HMAC-SHA256,不再支持基于MD5或SHA1的算法。
HashedCanonicalRequest
规范化请求摘要串,计算方法伪代码如下:
HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
使用哈希函数(Hash)对步骤一中得到的规范化请求(CanonicalRequest)进行摘要处理,具体使用的Hash函数取决于签名协议(SignatureAlgorithm),参见表1,例如,当签名协议为ACS3-HMAC-SHA256时,应使用SHA256作为Hash函数。
将上一步得到的摘要结果以小写的十六进制形式编码。
步骤三:计算签名
按照以下伪代码计算签名值(Signature)。
Signature = HexEncode(SignatureMethod(Secret, StringToSign))
StringToSign:步骤二中构造的待签名字符串,UTF-8编码。
SignatureMethod:签名算法,具体使用的算法取决于签名协议(SignatureAlgorithm),其对应关系如表1。
Secret:用户的签名密钥,为二进制数据。
HexEncode:以小写的十六进制的形式返回摘要的编码函数(即Base16编码)。
步骤四:将签名添加到请求中
计算完签名后,构造Authorization请求头,格式为:Authorization:<SignatureAlgorithm> Credential=<AccessKeyId>,SignedHeaders=<SignedHeaders>,Signature=<Signature>
,示例如下:
Authorization:ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=content-type;host;x-acs-timestamp,Signature=6b595d672d79c15b18edb4ccfba6789a24a6f2b82c400e03162d9279b08555d7
签名示例
为了让您更清晰地理解上述签名机制,下面以主流编程语言为例,将签名机制完整实现。示例代码只是让您更好地理解签名机制,存在不通用性,阿里云OpenAPI提供多种编程语言和开发框架的SDK,使用这些SDK可以免去签名过程,便于您快速构建与阿里云相关的应用程序,建议您使用SDK。
在签名之前,一定要先查看前提条件,获取API的请求方式、请求参数以及参数位置等信息。
Java示例
运行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.StringEntity;
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.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 Request {
// 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, Object> headers = new TreeMap<>();
// 调用API所需要的参数,参数位置在body。Json字符串
String body;
// 调用API所需要的参数,参数位置在query,参数按照参数名的字符代码升序排列
TreeMap<String, Object> queryParam = new TreeMap<>();
public Request(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;
initBuilder();
}
// init headers
private void initBuilder() {
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());
}
}
/**
* 这里通过环境变量获取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";
/**
* 主函数示例,演示如何使用阿里云的SDK进行API请求。
* 通过给定的参数构建请求,并进行签名认证。
*/
public static void main(String[] args) {
// RPC接口请求
String httpMethod = "POST"; // 请求方式
String canonicalUri = "/";
String host = "ecs.cn-beijing.aliyuncs.com"; // endpoint
String xAcsAction = "DescribeInstances"; // API名称
String xAcsVersion = "2014-05-26"; // API版本号
Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// 调用API所需要的参数,参数按照参数名的字符代码升序排列,具有重复名称的参数应按值进行排序。
request.queryParam.put("RegionId", "cn-beijing");
request.queryParam.put("VpcId", "vpc-2zeo42r27y4opXXXXXXXX");
/* // ROA接口POST请求
String httpMethod = "POST";
String canonicalUri = "/clusters";
String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
String xAcsAction= "CreateCluster"; // API名称
String xAcsVersion= "2015-12-15"; // API版本号
Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// 请求body,通过Gson将body转成JSON字符串
TreeMap<String, Object> body = new TreeMap<>();
body.put("name", "testDemo");
body.put("region_id", "cn-beijing");
body.put("cluster_type", "Kubernetes");
body.put("vpcid", "vpc-2zeo42r27y4opXXXXXXXX");
body.put("service_cidr", "172.16.1.0/20");
body.put("security_group_id", "sg-2zec0dm6qi66XXXXXXXX");
Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
request.body = gson.toJson(body);
request.headers.put("content-type", "application/json; charset=utf-8");*/
/* // ROA接口GET请求
String httpMethod = "GET";
// canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
String canonicalUri = "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/resources";
String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
String xAcsAction = "DescribeClusterResources"; // API名称
String xAcsVersion = "2015-12-15"; // API版本号
Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
request.queryParam.put("with_addon_resources", true);*/
// 签名过程
getAuthorization(request);
// 调用API
callApi(request);
}
private static void callApi(Request request) {
try {
// 通过HttpClient发送请求
String url = "https://" + request.host + request.canonicalUri;
URIBuilder uriBuilder = new URIBuilder(url);
// 添加请求参数
for (Map.Entry<String, Object> entry : request.queryParam.entrySet()) {
uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
}
HttpUriRequest httpRequest;
switch (request.httpMethod) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (request.body != null) {
httpPost.setEntity(new StringEntity(request.body, ContentType.APPLICATION_JSON));
}
httpRequest = httpPost;
break;
case "DELETE":
httpRequest = new HttpDelete(uriBuilder.build());
break;
case "PUT":
HttpPut httpPut = new HttpPut(uriBuilder.build());
if (request.body != null) {
StringEntity putEntity = new StringEntity(request.body);
httpPut.setEntity(putEntity);
}
httpRequest = httpPut;
break;
default:
System.out.println("Unsupported HTTP method: " + request.body);
throw new IllegalArgumentException("Unsupported HTTP method");
}
// 添加http请求头
for (Map.Entry<String, Object> entry : request.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();
} catch (UnsupportedEncodingException e) {
// 异常处理
System.out.println("UnsupportedEncodingException");
e.printStackTrace();
}
}
/**
* 该方法用于根据传入的HTTP请求方法、规范化的URI、查询参数等,计算并生成授权信息。
*/
private static void getAuthorization(Request request) {
try {
// 步骤 1:拼接规范请求串
// 请求参数,当请求的查询字符串为空时,使用空字符串作为规范化查询字符串
StringBuilder canonicalQueryString = new StringBuilder();
request.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);
});
// 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
String requestPayload = "";
if (request.body != null) {
requestPayload = request.body;
}
// 计算请求体的哈希值
String hashedRequestPayload = sha256Hex(requestPayload);
request.headers.put("x-acs-content-sha256", hashedRequestPayload);
// 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
StringBuilder canonicalHeaders = new StringBuilder();
// 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
StringBuilder signedHeadersSb = new StringBuilder();
request.headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") || entry.getKey().equalsIgnoreCase("host") || entry.getKey().equalsIgnoreCase("content-type")).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 = request.httpMethod + "\n" + request.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
System.out.println("canonicalRequest=========>\n" + canonicalRequest);
// 步骤 2:拼接待签名字符串
String hashedCanonicalRequest = sha256Hex(canonicalRequest); // 计算规范化请求的哈希值
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);
request.headers.put("Authorization", authorization);
} catch (Exception e) {
// 异常处理
System.out.println("Failed to get authorization");
e.printStackTrace();
}
}
/**
* 使用HmacSHA256算法生成消息认证码(MAC)。
*
* @param key 密钥,用于生成MAC的密钥,必须保密。
* @param str 需要进行MAC认证的消息。
* @return 返回使用HmacSHA256算法计算出的消息认证码。
* @throws Exception 如果初始化MAC或计算MAC过程中遇到错误,则抛出异常。
*/
public static byte[] hmac256(byte[] key, String str) throws Exception {
// 实例化HmacSHA256消息认证码生成器
Mac mac = Mac.getInstance("HmacSHA256");
// 创建密钥规范,用于初始化MAC生成器
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
// 初始化MAC生成器
mac.init(secretKeySpec);
// 计算消息认证码并返回
return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
}
/**
* 使用SHA-256算法计算字符串的哈希值并以十六进制字符串形式返回。
*
* @param str 需要进行SHA-256哈希计算的字符串。
* @return 计算结果为小写十六进制字符串。
* @throws Exception 如果在获取SHA-256消息摘要实例时发生错误。
*/
public static String sha256Hex(String str) throws Exception {
// 获取SHA-256消息摘要实例
MessageDigest md = MessageDigest.getInstance("SHA-256");
// 计算字符串s的SHA-256哈希值
byte[] d = md.digest(str.getBytes(StandardCharsets.UTF_8));
// 将哈希值转换为小写十六进制字符串并返回
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示例
需要您手动安装pytz和requests,请根据您所使用的Python版本在终端(Terminal)执行以下命令。
Python3
pip3 install pytz
pip3 install requests
Python2
pip install pytz
pip install requests
import hashlib
import hmac
import os
import uuid
from collections import OrderedDict
from urllib.parse import urlencode, quote_plus
import pytz
import requests
from datetime import datetime
class Request:
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}
# 环境变量中获取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'
def get_authorization(request):
try:
# Step 1: Construct Canonical Query String and Payload Hash
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 '')
request.headers['x-acs-content-sha256'] = hashed_request_payload
request.sorted_headers()
# Construct Canonical Headers and Signed 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{canonical_headers}\n{signed_headers}\n{hashed_request_payload}'
print(canonical_request)
# Step 2: Construct String to Sign
hashed_canonical_request = sha256_hex(canonical_request)
string_to_sign = f'{ALGORITHM}\n{hashed_canonical_request}'
print(string_to_sign)
# Step 3: Compute Signature
signature = hmac256(ACCESS_KEY_SECRET.encode('utf-8'), string_to_sign).hex().lower()
print(signature)
# Step 4: Construct Authorization Header
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 hmac256(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
def sha256_hex(s):
return hashlib.sha256(s.encode('utf-8')).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='*')
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', '~')
if __name__ == "__main__":
# RPC接口请求
http_method = "POST"
canonical_uri = "/"
host = "ecs.cn-beijing.aliyuncs.com"
x_acs_action = "DescribeInstances"
x_acs_version = "2014-05-26"
request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
request.query_param['RegionId'] = 'cn-beijing'
request.query_param['VpcId'] = 'vpc-2zeo42r27y4opXXXXXXXX'
# ROA接口POST请求
# http_method = "POST"
# canonical_uri = "/clusters"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "CreateCluster"
# x_acs_version = "2015-12-15"
# request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# body = OrderedDict()
# body["name"] = "testDemo"
# body["region_id"] = "cn-beijing"
# body["cluster_type"] = "Kubernetes"
# body["vpcid"] = "vpc-2zeo42r27y4opXXXXXXXX"
# body["service_cidr"] = "172.16.1.0/20"
# body["security_group_id"] = "sg-2zec0dm6qi66XXXXXXXX"
#
# request.body = json.dumps(body, separators=(',', ':'))
# 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("cb7cd6b9bde934f6193801878XXXXXXXX")
# canonical_uri = f"/clusters/{cluster_id_encode}/resources"
# host = "cs.cn-beijing.aliyuncs.com"
# x_acs_action = "DescribeClusterResources"
# x_acs_version = "2015-12-15"
# request = Request(http_method, canonical_uri, host, x_acs_action, x_acs_version)
# request.query_param['with_addon_resources'] = True
request.sorted_query_params()
get_authorization(request)
call_api(request)
Go示例
需要您在终端(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 string
queryParam map[string]string
}
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]string),
}
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
}
var (
AccessKeyId = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
ALGORITHM = "ACS3-HMAC-SHA256"
)
func main() {
// RPC接口请求
httpMethod := "POST"
canonicalUri := "/"
host := "ecs.cn-beijing.aliyuncs.com"
xAcsAction := "DescribeInstances"
xAcsVersion := "2014-05-26"
req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
req.queryParam["RegionId"] = "cn-beijing"
req.queryParam["VpcId"] = "vpc-2zeo42r27y4opXXXXXXXX"
/* // 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)
body := make(map[string]string)
body["name"] = "testDemo"
body["region_id"] = "cn-beijing"
body["cluster_type"] = "Kubernetes"
body["vpcid"] = "vpc-2zeo42r27y4opXXXXXXXX"
body["service_cidr"] = "172.16.1.0/20"
body["security_group_id"] = "sg-2zec0dm6qi66XXXXXXXX"
jsonBytes, err := json.Marshal(body)
if err != nil {
fmt.Println("Error marshaling to JSON:", err)
return
}
req.body = string(jsonBytes)
req.headers["content-type"] = "application/json; charset=utf-8"*/
/* // ROA接口GET请求
httpMethod := "GET"
// canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
canonicalUri := "/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/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"*/
// 签名过程
getAuthorization(req)
// 调用API
err := callAPI(req)
if err != nil {
println(err.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, v)
}
urlStr += "?" + q.Encode()
fmt.Println(urlStr)
httpReq, err := http.NewRequest(req.httpMethod, urlStr, strings.NewReader(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) {
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(v)) + "&"
}
canonicalQueryString = strings.TrimSuffix(canonicalQueryString, "&")
fmt.Printf("canonicalQueryString========>%s\n", canonicalQueryString)
hashedRequestPayload := sha256Hex(req.body)
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)
hashedCanonicalRequest := sha256Hex(canonicalRequest)
stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
fmt.Printf("stringToSign========>\n%s\n", stringToSign)
byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
if err != nil {
fmt.Println(err)
}
signature := strings.ToLower(hex.EncodeToString(byteData))
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(str string) string {
// 实例化SHA-256哈希函数
hash := sha256.New()
// 将字符串写入哈希函数
_, _ = hash.Write([]byte(str))
// 计算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
}
Node.js示例
本示例所用语言是javaScript。
const crypto = require('crypto');
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;
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 {
// 步骤 1:拼接规范请求串
const canonicalQueryString = Object.entries(signRequest.queryParam)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
.join('&');
// 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
const requestPayload = signRequest.body || '';
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.reduce((result, key) => {
const value = signRequest.headers[key];
// 根据需要格式化结果字符串
return `${result}${key}:${value}\n`;
}, '');
const canonicalRequest = `${signRequest.httpMethod}\n${signRequest.canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${hashedRequestPayload}`;
console.log('canonicalRequest=========>\n', canonicalRequest);
// 步骤 2:拼接待签名字符串
const hashedCanonicalRequest = sha256Hex(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) {
let query = new URLSearchParams();
for (let [key, value] of Object.entries(signRequest.queryParam)) {
query.append(key, String(value));
}
url += '?' + query.toString();
}
// 配置请求选项
let options = {
method: signRequest.httpMethod.toUpperCase(),
headers: {}
};
// 添加HTTP请求头
if (signRequest.headers) {
for (let [key, value] of Object.entries(signRequest.headers)) {
options.headers[key] = String(value);
}
}
// 处理请求体
if (signRequest.body && ['POST', 'PUT'].includes(signRequest.httpMethod.toUpperCase())) {
options.body = signRequest.body;
}
try {
return (await fetch(url, options)).text();
} catch (error) {
// handler error
console.error('Failed to send request:', error);
}
} catch (error) {
if (error instanceof TypeError) {
// handler error
console.error('Invalid URI syntax');
} else {
// handler error
console.error('An unexpected error occurred:', 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', Buffer.from(key, 'binary'));
hmac.update(data, 'utf8');
return hmac.digest('hex').toLowerCase();
}
function sha256Hex(str) {
if (str === null || str === undefined) {
throw new Error('输入字符串不可为null或undefined');
}
const hash = crypto.createHash('sha256');
const digest = hash.update(str, 'utf8').digest('hex');
return digest.toLowerCase();
}
// 示例一:RPC接口请求
const httpMethod = 'GET';
const canonicalUri = '/';
const host = 'ecs.cn-beijing.aliyuncs.com';
const xAcsAction = 'DescribeInstances';
const xAcsVersion = '2014-05-26';
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
signRequest.queryParam = {
RegionId: 'cn-beijing',
VpcId: 'vpc-2zeo42r27y4opXXXXXXXX',
}
// 示例二: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);
// const body = {
// name: 'testDemo',
// region_id: 'cn-beijing',
// cluster_type: 'ExternalKubernetes',
// vpcid: 'vpc-2zeo42r27y4opXXXXXXXX',
// service_cidr: '172.16.3.0/20',
// security_group_id: 'sg-2zeh5ta2iklXXXXXXXX',
// vswitch_ids: [
// 'vsw-2ze3aagwn397gXXXXXXXX'
// ],
// }
// signRequest.body = JSON.stringify(body)
// // 根据需要设置正确的Content-Type,这里假设是JSON
// signRequest.headers['content-type'] = 'application/json';
// 示例三:ROA接口GET请求
// const httpMethod = 'GET';
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// const canonicalUri = '/clusters/" + percentCode("cb7cd6b9bde934f6193801878XXXXXXXX") + "/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,
// }
getAuthorization(signRequest);
callApi(signRequest).then(r => {
console.log(r);
});
PHP示例
<?php
class SignatureDemo
{
private $ALGORITHM;
private $AccessKeyId;
private $AccessKeySecret;
public function __construct()
{
date_default_timezone_set('UTC'); // 设置时区为GMT
$this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // 从环境变量中获取RAM用户Access Key ID
$this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // 从环境变量中获取RAM用户Access Key Secret
$this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 设置加密算法
}
public function main()
{
// RPC接口请求
$request = $this->createRequest('POST', '/', 'ecs.cn-beijing.aliyuncs.com', 'DescribeRegions', '2014-05-26');
$request['queryParam']= ['RegionId' => 'cn-beijing'];
// ROA接口POST请求
// $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
// $this->addRequestBody($request, [
// 'name' => 'testDemo',
// 'region_id' => 'cn-beijing',
// 'cluster_type' => 'ExternalKubernetes',
// 'vpcid' => 'vpc-2zeo42r27y4opXXXXXXXX',
// 'service_cidr' => '172.16.5.0/20',
// 'security_group_id' => 'sg-2zeh5ta2ikljXXXXXXXX',
// "vswitch_ids" => [
// "vsw-2zeuntqtklsk0XXXXXXXX"
// ],
// ]);
// ROA接口GET请求
// canonicalUri如果存在path参数,需要对path参数encode,rawurlencode({path参数})
// $cluster_id = 'cb7cd6b9bde934f6193801878XXXXXXXX';
// $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,
// ];
$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 addRequestBody(&$request, $bodyData)
{
$request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
$request['headers']['content-type'] = 'application/json; charset=utf-8';
}
private function getAuthorization(&$request)
{
$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'];
// 初始化cURL会话
$ch = curl_init();
// 根据请求类型设置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;
case "PUT":
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
break;
default:
echo "Unsupported HTTP method: " . $request['body'];
throw new Exception("Unsupported HTTP method");
}
// 添加请求参数到URL
if (!empty($request['queryParam'])) {
$url .= '?' . http_build_query($request['queryParam']);
}
// 设置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'])); // 添加请求头
// 发送请求
$result = curl_exec($ch);
// 检查是否有错误发生
if (curl_errno($ch)) {
echo "Failed to send request: " . curl_error($ch);
} else {
echo $result;
}
// 关闭cURL会话
curl_close($ch);
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
}
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示例
using System.Globalization;
using System.Net;
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 string Body { get; set; }
public SortedDictionary<string, object> QueryParam { get; private 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 = "";
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";
public static void Main(string[] args)
{
// RPC接口请求
string httpMethod = "POST"; // RPC接口大部分是同时支持POST和GET的
string canonicalUri = "/";
string host = "ecs.cn-beijing.aliyuncs.com";
string xAcsAction = "DescribeInstances";
string xAcsVersion = "2014-05-26";
var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
request.QueryParam["RegionId"] = "cn-beijing";
// request.QueryParam["VpcId"] = "vpc-2ze46hx33qkj4********";
// List<string> instanceIds = ["i-2zee363xb9gj********", "i-2zegox90v7d1********", "i-2ze0ccts1hjt********"];
// request.QueryParam["InstanceIds"] = JsonConvert.SerializeObject(instanceIds, Formatting.None);
// // ROA接口POST请求
// String httpMethod = "POST";
// String canonicalUri = "/clusters";
// String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
// String xAcsAction = "CreateCluster"; // API名称
// String xAcsVersion = "2015-12-15"; // API版本号
// 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", "ManagedKubernetes" },
// { "vpcid", "vpc-2zeo42r27y4opXXXXXXXX" },
// { "service_cidr", "172.16.1.0/20" },
// { "security_group_id", "sg-2zec0dm6qi66XXXXXXXX" }
// };
// string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
// request.Body = jsonBody;
// request.Headers["content-type"] = "application/json; charset=utf-8";
// // ROA接口GET请求
// String httpMethod = "GET";
// // canonicalUri如果存在path参数,需要对path参数encode,percentCode({path参数})
// String canonicalUri = "/clusters/" + PercentCode("c3c2987c8beae4b1b85bef006xxxxxxxx") + "/resources";
// String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
// String xAcsAction = "DescribeClusterResources"; // API名称
// String xAcsVersion = "2015-12-15"; // API版本号
// 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("c3c2987c8beae4b1b85bef006xxxxxxxx");
// String host = "cs.cn-beijing.aliyuncs.com"; // endpoint
// String xAcsAction = "DeleteCluster"; // API名称
// String xAcsVersion = "2015-12-15"; // API版本号
// 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)
{
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
{
RequestUri = uriBuilder.Uri,
};
// 设置请求头
foreach (var entry in request.Headers)
{
if (entry.Key == "Authorization")
{
requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ;
}
else if (entry.Key == "content-type") // 与main中定义的要一致
{
continue;
}
else
{
requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
}
}
// 发送请求
HttpResponseMessage response;
switch (request.HttpMethod.ToUpper())
{
case "GET":
response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
break;
case "POST":
requestMessage.Method = HttpMethod.Post;
requestMessage.Content = new StringContent(request.Body ?? "", Encoding.UTF8, "application/json");
response = await httpClient.SendAsync(requestMessage);
break;
case "DELETE":
requestMessage.Method = HttpMethod.Delete;
response = await httpClient.SendAsync(requestMessage);
break;
case "PUT":
requestMessage.Method = HttpMethod.Put;
requestMessage.Content = new StringContent(request.Body ?? "", Encoding.UTF8, "application/json");
response = await httpClient.SendAsync(requestMessage);
break;
default:
Console.WriteLine("Unsupported HTTP method: " + request.HttpMethod);
throw new ArgumentException("Unsupported HTTP method");
}
// 读取响应内容
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
{
StringBuilder canonicalQueryString = new();
foreach (var entry in request.QueryParam)
{
if (canonicalQueryString.Length > 0)
{
canonicalQueryString.Append('&');
}
Console.WriteLine(PercentCode(entry.Value?.ToString() ?? ""));
canonicalQueryString.Append($"{PercentCode(entry.Key)}={PercentCode(entry.Value?.ToString() ?? "")}");
}
string requestPayload = string.IsNullOrEmpty(request.Body) ? "" : 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("content-type", 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}");
string hashedCanonicalRequest = Sha256Hash(canonicalRequest);
string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
Console.WriteLine($"stringToSign:{stringToSign}");
string signature = HmacSha256(AccessKeySecret, stringToSign);
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 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(string input)
{
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(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", "~");
}
}
}
固定参数示例
本示例是以固定的参数值为例,帮助您验证签名是否正确。根据下面假设的固定值计算完签名之后,得到与本示例一样的签名,表明您的签名过程是正确的。
所需参数名称 | 假设的参数值 |
AccessKeyID | YourAccessKeyId |
AccessKeySecret | YourAccessKeySecret |
x-acs-signature-nonce | 3156853299f313e23d1673dc12e1703d |
x-acs-date | 2023-10-26T10:22:32Z |
签名流程如下:
构造规范化请求。
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
签名失败常见报错
Code | Message | 解决方案 |
SignatureDoesNotMatch | Specified signature does not match our calculation. | 在签名过程中,您可能遗漏了对参数进行升序排序,也可能是多加了空格。请您仔细阅读签名机制的讲解,可以根据提供的固定参数示例验证您的签名过程是否正确。 |
IncompleteSignature | The request signature does not conform to Aliyun standards. |