本文将为您介绍如何通过自签名方式发起HTTP请求,调用阿里云RPC风格的OpenAPI。
不再推荐使用该访问方式,请移步参考V3版本请求体&签名机制。
HTTP 请求结构
一个完整的阿里云 RPC 请求由以下部分组成:
名称 | 是否必选 | 描述 | 示例值 |
名称 | 是否必选 | 描述 | 示例值 |
协议 | 是 | 请求协议,您可以在OpenAPI元数据中查看API支持的请求协议。若API同时支持 | https:// |
服务地址 | 是 | 即 Endpoint。您可以查阅不同云产品的服务接入地址文档获取Endpoint。 | ecs.cn-hangzhou.aliyuncs.com |
公共请求参数 | 是 | 阿里云OpenAPI的公共请求参数,更多信息,请参见下文公共请求参数。 | Action |
接口自定义请求参数 | 否 | API的请求参数,您可以在OpenAPI元数据中查看API定义的请求参数,或者在阿里云 OpenAPI 开发者门户查看。 | RegionId |
HTTPMethod | 是 | 请求方式,您可以在OpenAPI元数据中查看API支持的请求方式。 | GET |
公共请求参数
每个OpenAPI请求都需包含以下参数:
名称 | 类型 | 是否必选 | 描述 | 示例值 |
名称 | 类型 | 是否必选 | 描述 | 示例值 |
Action | String | 是 | API 的名称。您可以访问阿里云 OpenAPI 开发者门户,搜索您想调用的API 。 | CreateInstance |
Version | String | 是 | API 版本。您可以访问阿里云 OpenAPI 开发者门户,查看云产品的API版本。例如短信服务产品,您可以通过查看云产品主页中看到API 版本为 2017-05-25。 | 2014-05-26 |
Format | String | 否 | 指定接口返回数据格式,可选 JSON 或 XML,默认为 XML。 | JSON |
AccessKeyId | String | 是 | 阿里云访问密钥 ID。您可以在RAM 控制台查看您的 AccessKeyId。如需创建 AccessKey,请参见创建AccessKey。 | yourAccessKeyId |
SignatureNonce | String | 是 | 签名唯一随机数。用于防止网络重放攻击,建议您每一次请求都使用不同的随机数,随机数位数无限制。 | 15215528852396 |
Timestamp | String | 是 | 按照ISO 8601标准表示的UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ,有效期为31分钟,即生成时间戳后需要在31分钟内发起请求。示例: | 2018-01-01T12:00:00Z |
SignatureMethod | String | 是 | 签名方式。目前为固定值 | HMAC-SHA1 |
SignatureVersion | String | 是 | 签名算法版本。目前为固定值 | 1.0 |
Signature | String | 是 | 请求签名,用户请求的身份验证。更多信息,请参见签名机制。 | Pc5WB8gokVn0xfeu%2FZV%2BiNM1dgI%3D |
签名机制
为了确保 API 的安全性,每个请求都需通过签名(Signature)进行身份验证。以下是签名计算的步骤:
步骤一:构造规范化请求字符串
1、将公共请求参数和接口自定义请求参数合并,并将合并后的参数按照参数首字母的字典顺序进行排序,排序时不包括公共请求参数中的Signature
参数。 伪代码如下:
// 合并公共请求参数和接口自定义参数,并根据key排序
params = merged(publicParams,apiReuqestParams)
sortParams = sorted(params.keys())
当请求参数信息包含
"in": "formData"
时,需要将这类参数按照固定格式拼接为一个字符串,拼接格式为:key1=value1&key2=value2&key3=value3
。若请求参数的请求类型同时是array、object时,需要将参数平铺为一个新的映射(map)。例如{"key":["value1","value2"]}
平铺后为{"key.1":"value1","key.2":"value2"}
。还需要在公共请求参数中添加content-type=application/x-www-form-urlencoded
。当请求参数信息包含
"in": "body"
时,需要在公共请求参数中添加content-type,content-type的值与请求内容类型有关。例如:请求内容类型为JSON数据时,content-type的值为
application/json
。请求内容类型为二进制文件流时,content-type的值为
application/octet-stream
。
2、使用 UTF-8 字符集按照RFC3986规范对请求参数及其值进行编码,并使用等号(=)将请求参数与参数值连接。
编码规则:
字符 A~Z、a~z、0~9 以及字符
-
、_
、.
、~
不编码。对其他 ASCII 码字符进行编码。编码格式为%加上16进制的 ASCII 码。例如半角双引号(
"
)将被编码为%22
。需要注意的是,部分特殊字符需要特殊处理,具体如下:编码前
编码后
空格( )
%20
星号(
*
)%2A
%7E
波浪号(
~
)
伪代码如下:
encodeURIComponentParam = encodeURIComponent(sortParams.key) + "=" + encodeURIComponent(sortParams.value)
3、将步骤2的结果通过&
连接,即可得到规范化请求字符串CanonicalizedQueryString
。请注意,参数的排序与第1步保持一致。伪代码如下:
CanonicalizedQueryString = encodeURIComponentParam1 + "&" + encodeURIComponentParam2 + ... + encodeURIComponentParamN
步骤二:构造签名字符串
构造待签名字符串 stringToSign
。该字符串构造规则的伪代码如下:
stringToSign =
HTTPMethod + "&" + // HTTPMethod:发送请求的 HTTP 方法,例如 GET。
encodeURIComponent("/") + "&" + // encodeURIComponent 为步骤一第2步的编码方法
encodeURIComponent(CanonicalizedQueryString) // CanonicalizedQueryString 为步骤一获取的规范化请求字符串。
步骤三:计算签名
按照RFC2104的定义,通过您传入的 AccessKeyId 对应的密钥 AccessSecret,使用 HMAC-SHA1
的签名算法,计算待签名字符串StringToSign
的签名。其中 Base64() 为编码计算函数,HMAC_SHA1() 为 HMAC_SHA1 签名函数,返回值为 HMAC_SHA1 加密后原始字节,而非16进制字符串,UTF_8_Encoding_Of() 是 UTF-8 字符编码函数,伪代码如下:
signature = Base64(HMAC_SHA1(AccessSecret + "&", UTF_8_Encoding_Of(stringToSign)))
签名示例
本示例以调用 ECS DescribeDedicatedHosts查询一台或多台专有宿主机的详细信息为例,根据假设的参数值,展示了签名机制中每个步骤所产生的正确输出内容。您可以在代码中使用本示例提供的假设参数值进行计算,并通过对比您的输出结果与本示例的内容,以验证签名过程的正确性。
所需参数名称 | 假设的参数值 |
Endpoint | ecs.cn-beijing.aliyuncs.com |
Action | DescribeDedicatedHosts |
Version | 2014-05-26 |
Format | JSON |
AccessKeyId | testid |
AccessKeySecret | testsecret |
SignatureNonce | edb2b34af0af9a6d14deaf7c1a5315eb |
Timestamp | 2023-03-13T08:34:30Z |
业务请求参数
所需参数名称 | 假设的参数值 |
RegionId | cn-beijing |
签名流程如下:
构造规范化请求字符串。
AccessKeyId=testid&Action=DescribeDedicatedHosts&Format=JSON&RegionId=cn-beijing&SignatureMethod=HMAC-SHA1&SignatureNonce=edb2b34af0af9a6d14deaf7c1a5315eb&SignatureVersion=1.0&Timestamp=2023-03-13T08%3A34%3A30Z&Version=2014-05-26
构造待签名字符串
stringToSign
。GET&%2F&AccessKeyId%3Dtestid%26Action%3DDescribeDedicatedHosts%26Format%3DJSON%26RegionId%3Dcn-beijing%26SignatureMethod%3DHMAC-SHA1%26SignatureNonce%3Dedb2b34af0af9a6d14deaf7c1a5315eb%26SignatureVersion%3D1.0%26Timestamp%3D2023-03-13T08%253A34%253A30Z%26Version%3D2014-05-26
计算签名值。根据
AccessKeySecret=testsecret
计算得到的签名值如下:9NaGiOspFP5UPcwX8Iwt2YJXXuk=
发起请求。根据接口URL组成规则
[协议][服务地址]?[公共参数][业务请求参数]
获取完整的请求URL:https://ecs.cn-beijing.aliyuncs.com/?AccessKeyId=testid&Action=DescribeDedicatedHosts&Format=JSON&Signature=9NaGiOspFP5UPcwX8Iwt2YJXXuk%3D&SignatureMethod=HMAC-SHA1&SignatureNonce=edb2b34af0af9a6d14deaf7c1a5315eb&SignatureVersion=1.0&Timestamp=2023-03-13T08%3A34%3A30Z&Version=2014-05-26&RegionId=cn-beijing
您可以使用curl或者wget等工具发起HTTP请求调用
DescribeDedicatedHosts
,查询一台或多台专有宿主机的详细信息。
示例代码的运行环境是Java 8,您可能需要根据具体情况对代码进行相应的调整。
运行Java示例,需要您在pom.xml中添加以下Maven依赖。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
public class Demo {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static final String ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
private static final String ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
public static class SignatureRequest {
public final String httpMethod;
public final String host;
public final String action;
public final String version;
public final String canonicalUri = "/";
public TreeMap<String, Object> headers = new TreeMap<>();
public TreeMap<String, Object> queryParams = new TreeMap<>();
public TreeMap<String, Object> body = new TreeMap<>();
public TreeMap<String, Object> allParams = new TreeMap<>();
public byte[] bodyByte;
public SignatureRequest(String httpMethod, String host, String action, String version) {
this.httpMethod = httpMethod;
this.host = host;
this.action = action;
this.version = version;
setExtendedHeaders();
}
public void setExtendedHeaders() {
headers.put("AccessKeyId", ACCESS_KEY_ID);
headers.put("Format", "JSON");
headers.put("SignatureMethod", "HMAC-SHA1");
headers.put("SignatureVersion", "1.0");
headers.put("SignatureNonce", UUID.randomUUID().toString());
DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "GMT"));
headers.put("Timestamp", DATE_FORMAT.format(new Date()));
headers.put("Action", action);
headers.put("Version", version);
}
public void getAllParams() {
allParams.putAll(headers);
if (!queryParams.isEmpty()) {
allParams.putAll(queryParams);
}
if (!body.isEmpty()) {
allParams.putAll(body);
}
}
}
public static void main(String[] args) throws IOException {
// 示例一:API请求参数无body
String httpMethod = "POST";
String endpoint = "dysmsapi.aliyuncs.com";
String action = "SendSms";
String version = "2017-05-25";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, endpoint, action, version);
signatureRequest.queryParams.put("PhoneNumbers", "123XXXXXXXX");
signatureRequest.queryParams.put("SignName", "XXXXXXX");
signatureRequest.queryParams.put("TemplateCode", "XXXXXXX");
signatureRequest.queryParams.put("TemplateParam", "XXXXXXX");
/*// 示例二:API请求参数有body
String httpMethod = "POST";
String endpoint = "mt.aliyuncs.com";
String action = "TranslateGeneral";
String version = "2018-10-12";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, endpoint, action, version);
TreeMap<String, Object> body = new TreeMap<>();
body.put("FormatType", "text");
body.put("SourceLanguage", "zh");
body.put("TargetLanguage", "en");
body.put("SourceText", "你好");
body.put("Scene", "general");
signatureRequest.body = body;
String formDataToString = formDataToString(body);
signatureRequest.bodyByte = formDataToString.getBytes(StandardCharsets.UTF_8);
signatureRequest.headers.put("content-type", "application/x-www-form-urlencoded");*/
/*// 示例三:API请求参数有body,body为二进制文件
String httpMethod = "POST";
String endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
String action = "RecognizeGeneral";
String version = "2021-07-07";
SignatureRequest signatureRequest = new SignatureRequest(httpMethod, endpoint, action, version);
signatureRequest.bodyByte = Files.readAllBytes(Paths.get("D:\\test.png"));
signatureRequest.headers.put("content-type", "application/octet-stream");*/
// 计算签名
calculateSignature(signatureRequest);
// 发起请求,验证签名是否正确
callApi(signatureRequest);
}
private static void calculateSignature(SignatureRequest signatureRequest) {
// 将header、queryParam、body合成一个map,用于构造规范化请求字符串
signatureRequest.getAllParams();
// 获取规范化请求字符串
StringBuilder canonicalQueryString = new StringBuilder();
signatureRequest.allParams.entrySet().stream().map(entry -> percentEncode(entry.getKey()) + "="
+ percentEncode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
if (canonicalQueryString.length() > 0) {
canonicalQueryString.append("&");
}
canonicalQueryString.append(queryPart);
});
System.out.println("canonicalQueryString:" + canonicalQueryString);
// 构造待签名字符串
String stringToSign = signatureRequest.httpMethod + "&" + percentEncode(signatureRequest.canonicalUri) + "&" + percentEncode(String.valueOf(canonicalQueryString));
System.out.println("stringToSign:" + stringToSign);
// 计算签名
String signature = generateSignature(ACCESS_KEY_SECRET, stringToSign);
System.out.println("signature:" + signature);
signatureRequest.allParams.put("Signature", signature);
}
private static void callApi(SignatureRequest signatureRequest) {
try {
String url = String.format("https://%s/", signatureRequest.host);
URIBuilder uriBuilder = new URIBuilder(url);
for (Map.Entry<String, Object> entry : signatureRequest.allParams.entrySet()) {
uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
}
HttpUriRequest httpRequest;
switch (signatureRequest.httpMethod) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (signatureRequest.bodyByte != null) {
httpPost.setEntity(new ByteArrayEntity(signatureRequest.bodyByte, ContentType.create((String) signatureRequest.headers.get("content-type"))));
}
httpRequest = httpPost;
break;
case "DELETE":
httpRequest = new HttpDelete(uriBuilder.build());
break;
default:
System.out.println("Unsupported HTTP method: " + signatureRequest.httpMethod);
throw new IllegalArgumentException("Unsupported HTTP method");
}
try (CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(httpRequest)) {
String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println(result);
} catch (IOException e) {
System.out.println("Failed to send request");
throw new RuntimeException(e);
}
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
private static String formDataToString(Map<String, Object> formData) {
Map<String, Object> tileMap = new HashMap<>();
processObject(tileMap, "", formData);
StringBuilder result = new StringBuilder();
boolean first = true;
String symbol = "&";
for (Map.Entry<String, Object> entry : tileMap.entrySet()) {
String value = String.valueOf(entry.getValue());
if (value != null && !value.isEmpty()) {
if (first) {
first = false;
} else {
result.append(symbol);
}
result.append(percentEncode(entry.getKey()));
result.append("=");
result.append(percentEncode(value));
}
}
return result.toString();
}
private static void processObject(Map<String, Object> map, String key, Object value) {
// 如果值为空,则无需进一步处理
if (value == null) {
return;
}
if (key == null) {
key = "";
}
// 当值为List类型时,遍历List中的每个元素,并递归处理
if (value instanceof List<?>) {
List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); ++i) {
processObject(map, key + "." + (i + 1), list.get(i));
}
} else if (value instanceof Map<?, ?>) {
// 当值为Map类型时,遍历Map中的每个键值对,并递归处理
Map<?, ?> subMap = (Map<?, ?>) value;
for (Map.Entry<?, ?> entry : subMap.entrySet()) {
processObject(map, key + "." + entry.getKey().toString(), entry.getValue());
}
} else {
// 对于以"."开头的键,移除开头的"."以保持键的连续性
if (key.startsWith(".")) {
key = key.substring(1);
}
// 对于byte[]类型的值,将其转换为UTF-8编码的字符串
if (value instanceof byte[]) {
map.put(key, new String((byte[]) value, StandardCharsets.UTF_8));
} else {
// 对于其他类型的值,直接转换为字符串
map.put(key, String.valueOf(value));
}
}
}
public static String generateSignature(String accessSecret, String stringToSign) {
try {
// 创建HMAC-SHA1密钥
SecretKeySpec signingKey = new SecretKeySpec((accessSecret + "&").getBytes(StandardCharsets.UTF_8), "HmacSHA1");
// 获取Mac实例并初始化
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
// 计算HMAC-SHA1签名
byte[] rawHmac = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(rawHmac);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
System.out.println("Failed to generate HMAC-SHA1 signature");
throw new RuntimeException(e);
}
}
public static String percentEncode(String str) {
if (str == null) {
throw new IllegalArgumentException("输入字符串不可为null");
}
try {
return URLEncoder.encode(str, StandardCharsets.UTF_8.name()).replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8编码不被支持", e);
}
}
}
相关文档
- 本页导读 (1)
- HTTP 请求结构
- 公共请求参数
- 签名机制
- 步骤一:构造规范化请求字符串
- 步骤二:构造签名字符串
- 步骤三:计算签名
- 签名示例
- 相关文档