本文为您介绍如何通过ROA风格的自签名方式发起HTTP请求,调用阿里云OpenAPI。
不再推荐使用该访问方式,请移步参考V3版本请求体&签名机制。
HTTP 请求结构
一个完整的阿里云 OpenAPI 请求,包含以下部分。
名称 | 是否必选 | 描述 | 示例值 |
协议 | 是 | 请求协议,您可以通过调用OpenAPI元数据查看API支持的请求协议。若API同时支持 | https:// |
服务地址 | 是 | 即 Endpoint。您可以查阅不同云产品的服务接入地址文档,查阅不同服务区域下的服务地址。 | bailian.cn-beijing.aliyuncs.com |
resource_URI_parameters | 是 | 接口URL,包括接口路径和位置在 path、 query的接口请求参数。 | /{WorkspaceId}/datacenter/category |
RequestHeader | 是 | 请求头信息,通常包含API的版本、Host、Authorization等信息。后文将详细说明,请参见RequestHeader(请求头) | Accept:application/json Authorization:acs YourAccessKeyId:s8ZMF/eAIvvPJwehLLha0bVNFJ0= Content-MD5:LP54yxk8n7KqF1PPgbJizw== Content-Type:application/json Date:Wed, 16 Apr 2025 03:44:46 GMT Host:bailian.cn-beijing.aliyuncs.com x-acs-signature-method:HMAC-SHA1 x-acs-signature-nonce:ef34aae7-7bd2-413d-a541-680cd2c48538 x-acs-signature-version:1.0 x-acs-version:2023-12-29 |
RequestBody | 是 | 定义在 body 中的业务请求参数,建议您在阿里云 OpenAPI 开发者门户进行试用。 | {"CategoryName":"test","CategoryType":"UNSTRUCTURED"} |
HTTPMethod | 是 | 请求方法,请求方法包括PUT、POST、GET、DELETE。接口的请求方法可通过对应的API文档获取。 | POST |
RequestHeader(请求头)
一个完整的阿里云 OpenAPI 请求,包含以下部分。
名称 | 类型 | 是否必选 | 描述 | 示例值 |
Accept | String | 否 | 指定接口返回数据的格式,该参数未传时,以XML形式返回数据。ROA 只支持固定值 | application/json |
Content-MD5 | String | 否 | RequestBody的128-bit MD5散列值转换成BASE64编码的结果。 | LP54yxk8n7KqF1PPgbJizw== |
Date | String | 是 | 当前时间戳,有效期为15分钟,即生成时间戳后需要在15分钟内发起请求。HTTP 1.1协议中规定的 GMT 时间,例如:Tue 9 Apr 2019 07:35:29 GMT。 | Wed, 16 Apr 2025 03:44:46 GMT |
Host | String | 是 | 即服务地址,参见HTTP 请求结构中的服务地址。 | bailian.cn-beijing.aliyuncs.com |
x-acs-signature-method | String | 非匿名请求必须 | 签名方式。目前为固定值 | HMAC-SHA1 |
x-acs-signature-nonce | String | 是 | 签名唯一随机数。用于防止网络重放攻击,建议您每次请求都使用不同的随机数。 | ef34aae7-7bd2-413d-a541-680cd2c48538 |
x-acs-signature-version | String | 否 | 签名版本。目前为固定值 | 1.0 |
x-acs-version | String | 是 | API 版本。您可以访问阿里云 OpenAPI 开发者门户或者通过OpenAPI元数据获取OpenAPI 对应的 API 版本。 | 2023-12-29 |
Authorization | String | 非匿名请求必须 | 用于验证请求合法性的认证信息,格式为 Signature为请求签名,取值参见签名机制。 | acs YourAccessKeyId:D9uFJAJgLL+dryjBfQK+YeqGtoY= |
签名机制
为确保API的安全调用,所有HTTP或HTTPS请求均需在请求中包含签名信息。当请求到达阿里云API网关时,网关将根据请求中的参数及请求头重新计算签名,并与请求中包含的签名信息进行对比,以验证请求者的身份,并确保传输数据的完整性与安全性。以下是签名计算的步骤:
请求及返回结果都使用UTF-8字符集进行编码。
步骤一:构造规范化请求头
阿里云规范化请求头(CanonicalizedHeaders)是非标准HTTP头部信息。它是指请求中出现的以x-acs-
为前缀的参数信息,构造方法如下:
将RequestHeader(请求头)中以
x-acs-
为前缀的请求头名称转换成小写字母,并按照字典顺序进行升序排列。对请求头的名称和值分别进行去除首尾空格操作。
在每一个请求头后添加一个
\n
分隔符(包括最后一个请求头),然后将所有请求头拼接在一起即获得CanonicalizedHeaders。
示例值:
x-acs-signature-method:HMAC-SHA1
x-acs-signature-nonce:ef34aae7-7bd2-413d-a541-680cd2c48538
x-acs-signature-version:1.0
x-acs-version:2023-12-29
步骤二:构造规范化资源
规范化资源(CanonicalizedResource) 表示您想要访问资源的规范描述,具体构造方式如下:
将请求的查询字符串(queryString)中的参数按照参数名称的字典序重新排序,并以
&
分隔符连接生成已排序查询字符串。将请求资源路径(指URL中host与查询字符串之间的部分,包含host之后的
/
但不包含查询字符串前的?
)与已排序查询字符串以?
拼接,得到规范化资源。当查询字符串不存在时,直接用请求资源路径作为规范化资源。
示例值:
/llm-p2e4XXXXXXXXsvtn/datacenter/category
步骤三:构造待签名字符串
按照以下伪代码构造待签名字符串(stringToSign):
String stringToSign =
HTTPMethod + "\n" +
Accept + "\n" +
ContentMD5 + "\n" +
ContentType + "\n" +
Date + "\n" +
CanonicalizedHeaders +
CanonicalizedResource
参数 | 描述 |
HTTPMethod | 大写的HTTP方法名,例如POST、GET。 |
Accept | Accept请求头的值,当请求头不存在时,使用空字符串代替。 |
ContentMD5 | Content-MD5请求头的值,当请求头不存在时,使用空字符串代替。 |
ContentType | Content-Type请求头的值,当请求头不存在时,使用空字符串代替。 说明 RequestBody对应的MIME类型。 |
Date | Date请求头的值。 |
CanonicalizedHeaders | 步骤一:构造规范化请求头中获得的规范化请求头。 |
CanonicalizedResource | 步骤二:构造规范化资源中获得的规范化资源。 |
示例值:
POST
application/json
LP54yxk8n7KqF1PPgbJizw==
application/json
Wed, 16 Apr 2025 03:44:46 GMT
x-acs-signature-method:HMAC-SHA1
x-acs-signature-nonce:ef34aae7-7bd2-413d-a541-680cd2c48538
x-acs-signature-version:1.0
x-acs-version:2023-12-29
/llm-p2e4XXXXXXXXsvtn/datacenter/category
步骤四:计算签名
根据RFC2104的定义,按照HMAC-SHA1算法对上一步生成的待签名字符串进行签名计算,并以Base64编码规则将计算结果编码成字符串,即得到最终的签名值(signature)。
String signature = Base64(HMAC_SHA1(SigningKey, stringToSign))
计算签名时使用的SigningKey值就是您的AccessKey Secret。更多信息,请参见创建AccessKey。
示例值:
s8ZMF/eAIvvPJwehLLha0bVNFJ0=
步骤五:构造Authorization
计算出签名结果signature后,按照以下规则拼接字符串,并将结果作为Authorization请求头的值。
String Authorization = "acs " + AccessKeyId + ":" + signature
示例值:
acs YourAccessKeyId:s8ZMF/eAIvvPJwehLLha0bVNFJ0=
步骤六:将签名添加到请求中并发起请求
POST https://bailian.cn-beijing.aliyuncs.com/llm-p2e4XXXXXXXXsvtn/datacenter/category HTTP/1.1
Accept:application/json
Authorization:acs YourAccessKeyId:r8Y9ZqVhTrYGl4nieqk7CW0Pwow=
Content-MD5:LP54yxk8n7KqF1PPgbJizw==
Content-Type:application/json
Date:Wed, 16 Apr 2025 06:47:10 GMT
Host:bailian.cn-beijing.aliyuncs.com
x-acs-signature-method:HMAC-SHA1
x-acs-signature-nonce:e3d8efa7-b1d8-42f3-9733-4fe2691e15dc
x-acs-signature-version:1.0
x-acs-version:2023-12-29
{"CategoryName":"test","CategoryType":"UNSTRUCTURED"}
签名示例代码
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.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 javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
public class SignatureDemo {
public static class SignatureRequest {
private final String httpMethod;
private final String host;
private final String version;
private final String canonicalUri;
public TreeMap<String, String> headers = new TreeMap<>();
public TreeMap<String, Object> queryParams = new TreeMap<>();
public byte[] bodyByteArray;
public SignatureRequest(String httpMethod, String host, String version, String canonicalUri) {
this.httpMethod = httpMethod;
this.host = host;
this.version = version;
this.canonicalUri = canonicalUri;
initHeaders();
}
private void initHeaders() {
headers.put("Host", host);
headers.put("x-acs-version", version);
headers.put("x-acs-signature-version", "1.0");
headers.put("Accept", "application/json");
headers.put("x-acs-signature-nonce", java.util.UUID.randomUUID().toString());
headers.put("Date", getGmtDate());
headers.put("x-acs-signature-method", "HMAC-SHA1");
}
private String getGmtDate() {
SimpleDateFormat gmtFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH);
gmtFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
return gmtFormat.format(new Date());
}
public void setBody(Map<String, String> body, ContentType contentType) {
Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
this.bodyByteArray = gson.toJson(body).getBytes(StandardCharsets.UTF_8);
headers.put("Content-MD5", md5Sum(this.bodyByteArray));
headers.put("Content-Type", contentType.getMimeType());
}
}
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");
private static final String ALGORITHM_NAME = "HmacSHA1";
public static void main(String[] args) {
// 示例一:POST请求
String method = "POST";
String host = "bailian.cn-beijing.aliyuncs.com";
String version = "2023-12-29";
String canonicalUri = "/llm-p2e4XXXXXXXXsvtn/datacenter/category";
SignatureRequest signatureRequest = new SignatureRequest(method, host, version, canonicalUri);
Map<String, String> body = new HashMap<>();
body.put("CategoryName", "test");
body.put("CategoryType", "UNSTRUCTURED");
System.out.println(new Gson().toJson(body));
signatureRequest.setBody(body, ContentType.APPLICATION_JSON);
/*// 示例二:GET请求
String method = "GET";
String host = "bailian.cn-beijing.aliyuncs.com";
String version = "2023-12-29";
String canonicalUri = "/llm-p2e4XXXXXXXXsvtn/datacenter/files";
SignatureRequest signatureRequest = new SignatureRequest(method, host, version, canonicalUri);
signatureRequest.queryParams.put("CategoryId", "cate_a946*********************_10045991");*/
/*// 示例三:DELETE请求
String method = "DELETE";
String host = "bailian.cn-beijing.aliyuncs.com";
String version = "2023-12-29";
String canonicalUri = "/llm-p2e4XXXXXXXXsvtn/datacenter/category/cate_a946*********************_10045991";
SignatureRequest signatureRequest = new SignatureRequest(method, host, version, canonicalUri);*/
// 生成Authorization
generateSignature(signatureRequest);
// 通过HTTPClient发送请求
callApi(signatureRequest);
}
private static void generateSignature(SignatureRequest signatureRequest) {
try {
// 步骤一:构造规范化请求头
String canonicalHeaders = buildCanonicalHeaders(signatureRequest);
// 步骤二:构造规范化资源
String canonicalQueryString = buildQueryString(signatureRequest);
// 步骤三:构造规范化请求字符串
String stringToSign = buildStringToSign(signatureRequest, canonicalQueryString, canonicalHeaders);
// 步骤四:构造签名字符串
String signature = signString(stringToSign);
// 步骤五:构造Authorization
String authorization = "acs " + ACCESS_KEY_ID + ":" + signature;
signatureRequest.headers.put("Authorization", authorization);
} catch (Exception ex) {
throw new IllegalArgumentException(ex.toString());
}
}
private static void callApi(SignatureRequest signatureRequest) {
try {
// 通过HttpClient发送请求
String url = "https://" + signatureRequest.host + signatureRequest.canonicalUri;
URIBuilder uriBuilder = new URIBuilder(url);
// 添加请求参数
signatureRequest.queryParams.forEach((key, value) -> uriBuilder.addParameter(key, String.valueOf(value)));
HttpUriRequest httpRequest;
switch (signatureRequest.httpMethod) {
case "GET":
httpRequest = new HttpGet(uriBuilder.build());
break;
case "POST":
HttpPost httpPost = new HttpPost(uriBuilder.build());
if (signatureRequest.bodyByteArray != null) {
httpPost.setEntity(new ByteArrayEntity(signatureRequest.bodyByteArray, ContentType.create(signatureRequest.headers.get("Content-Type"))));
}
httpRequest = httpPost;
break;
case "PUT":
HttpPut httpPut = new HttpPut(uriBuilder.build());
if (signatureRequest.bodyByteArray != null) {
httpPut.setEntity(new ByteArrayEntity(signatureRequest.bodyByteArray, ContentType.create(signatureRequest.headers.get("Content-Type"))));
}
httpRequest = httpPut;
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请求头
signatureRequest.headers.forEach(httpRequest::addHeader);
// 发送请求
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");
throw new IllegalArgumentException(e.toString());
}
} catch (URISyntaxException e) {
// 异常处理
System.out.println("Invalid URI syntax");
throw new IllegalArgumentException(e.toString());
}
}
private static String buildCanonicalHeaders(SignatureRequest signatureRequest) {
StringBuilder canonicalHeaders = new StringBuilder();
signatureRequest.headers.entrySet().stream()
.filter(entry -> entry.getKey().startsWith("x-acs-"))
.forEach(entry -> canonicalHeaders.append(entry.getKey().toLowerCase().trim()).append(":").append(entry.getValue().trim()).append("\n"));
return canonicalHeaders.toString();
}
private static String buildQueryString(SignatureRequest signatureRequest) {
StringBuilder queryBuilder = new StringBuilder(signatureRequest.canonicalUri);
if (!signatureRequest.queryParams.isEmpty()) {
queryBuilder.append("?");
}
for (Map.Entry<String, Object> entry : signatureRequest.queryParams.entrySet()) {
queryBuilder.append(entry.getKey());
String value = (String) entry.getValue();
if (value != null && !value.isEmpty()) {
queryBuilder.append("=").append(value).append("&");
} else {
queryBuilder.append("&");
}
}
String queryString = queryBuilder.toString();
if (queryString.endsWith("&")) {
queryString = queryString.substring(0, queryString.length() - 1);
}
return queryString;
}
private static String buildStringToSign(SignatureRequest signatureRequest, String canonicalQueryString, String canonicalHeaders) {
StringBuilder sb = new StringBuilder();
sb.append(signatureRequest.httpMethod).append("\n");
appendIfPresent(sb, signatureRequest.headers.get("Accept"));
appendIfPresent(sb, signatureRequest.headers.get("Content-MD5"));
appendIfPresent(sb, signatureRequest.headers.get("Content-Type"));
appendIfPresent(sb, signatureRequest.headers.get("Date"));
sb.append(canonicalHeaders);
sb.append(canonicalQueryString);
return sb.toString();
}
private static void appendIfPresent(StringBuilder sb, String value) {
if (value != null) {
sb.append(value).append("\n");
} else {
sb.append("\n");
}
}
private static String signString(String stringToSign) {
try {
Mac mac = Mac.getInstance(ALGORITHM_NAME);
mac.init(new SecretKeySpec(ACCESS_KEY_SECRET.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME));
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return DatatypeConverter.printBase64Binary(signData);
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new IllegalArgumentException(ex.toString());
}
}
public static String md5Sum(byte[] buff) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] md5Bytes = md.digest(buff);
return DatatypeConverter.printBase64Binary(md5Bytes);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalArgumentException(ex.toString());
}
}
}
相关文档
您可以通过以下文档详细了解两种API风格的区别,具体请参见区分ROA风格和RPC风格。