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

更新时间:2025-02-21 05:51:21

本文将为您介绍如何通过自签名方式发起HTTP请求,调用阿里云RPC风格的OpenAPI。

重要

不再推荐使用该访问方式,请移步参考V3版本请求体&签名机制

HTTP 请求结构

一个完整的阿里云 RPC 请求由以下部分组成:

名称

是否必选

描述

示例值

名称

是否必选

描述

示例值

协议

请求协议,您可以在OpenAPI元数据中查看API支持的请求协议。若API同时支持HTTPHTTPS时,为了确保更高的安全性,建议您使用HTTPS协议发送请求。

https://

服务地址

即 Endpoint。您可以查阅不同云产品的服务接入地址文档获取Endpoint。

ecs.cn-hangzhou.aliyuncs.com

公共请求参数

阿里云OpenAPI的公共请求参数,更多信息,请参见下文公共请求参数

Action

接口自定义请求参数

API的请求参数,您可以在OpenAPI元数据中查看API定义的请求参数,或者在阿里云 OpenAPI 开发者门户查看。

RegionId

HTTPMethod

请求方式,您可以在OpenAPI元数据中查看API支持的请求方式。

GET

公共请求参数

每个OpenAPI请求都需包含以下参数:

名称

类型

是否必选

描述

示例值

名称

类型

是否必选

描述

示例值

Action

String

API 的名称。您可以访问阿里云 OpenAPI 开发者门户,搜索您想调用的API 。

CreateInstance

Version

String

API 版本。您可以访问阿里云 OpenAPI 开发者门户,查看云产品的API版本。例如短信服务产品,您可以通过查看云产品主页中看到API 版本为 2017-05-25。

2014-05-26

Format

String

指定接口返回数据格式,可选 JSON 或 XML,默认为 XML。

JSON

AccessKeyId

String

阿里云访问密钥 ID。您可以在RAM 控制台查看您的 AccessKeyId。如需创建 AccessKey,请参见创建AccessKey

yourAccessKeyId

SignatureNonce

String

签名唯一随机数。用于防止网络重放攻击,建议您每一次请求都使用不同的随机数,随机数位数无限制。

15215528852396

Timestamp

String

按照ISO 8601标准表示的UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ,有效期为31分钟,即生成时间戳后需要在31分钟内发起请求。示例:2018-01-01T12:00:00Z表示北京时间20180101200000秒。

2018-01-01T12:00:00Z

SignatureMethod

String

签名方式。目前为固定值 HMAC-SHA1

HMAC-SHA1

SignatureVersion

String

签名算法版本。目前为固定值 1.0

1.0

Signature

String

请求签名,用户请求的身份验证。更多信息,请参见签名机制

Pc5WB8gokVn0xfeu%2FZV%2BiNM1dgI%3D

签名机制

为了确保 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)))

签名示例

固定参数示例
Java签名示例

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

所需参数名称

假设的参数值

Endpoint

ecs.cn-beijing.aliyuncs.com

Action

DescribeDedicatedHosts

Version

2014-05-26

Format

JSON

AccessKeyId

testid

AccessKeySecret

testsecret

SignatureNonce

edb2b34af0af9a6d14deaf7c1a5315eb

Timestamp

2023-03-13T08:34:30Z

业务请求参数

所需参数名称

假设的参数值

RegionId

cn-beijing

签名流程如下:

  1. 构造规范化请求字符串。

    AccessKeyId=testid&Action=DescribeDedicatedHosts&Format=JSON&RegionId=cn-beijing&SignatureMethod=HMAC-SHA1&SignatureNonce=edb2b34af0af9a6d14deaf7c1a5315eb&SignatureVersion=1.0&Timestamp=2023-03-13T08%3A34%3A30Z&Version=2014-05-26
  2. 构造待签名字符串stringToSign

    GET&%2F&AccessKeyId%3Dtestid%26Action%3DDescribeDedicatedHosts%26Format%3DJSON%26RegionId%3Dcn-beijing%26SignatureMethod%3DHMAC-SHA1%26SignatureNonce%3Dedb2b34af0af9a6d14deaf7c1a5315eb%26SignatureVersion%3D1.0%26Timestamp%3D2023-03-13T08%253A34%253A30Z%26Version%3D2014-05-26
  3. 计算签名值。根据 AccessKeySecret=testsecret计算得到的签名值如下:

    9NaGiOspFP5UPcwX8Iwt2YJXXuk=
  4. 发起请求。根据接口URL组成规则[协议][服务地址]?[公共参数][业务请求参数]获取完整的请求URL:

    https://ecs.cn-beijing.aliyuncs.com/?AccessKeyId=testid&Action=DescribeDedicatedHosts&Format=JSON&Signature=9NaGiOspFP5UPcwX8Iwt2YJXXuk%3D&SignatureMethod=HMAC-SHA1&SignatureNonce=edb2b34af0af9a6d14deaf7c1a5315eb&SignatureVersion=1.0&Timestamp=2023-03-13T08%3A34%3A30Z&Version=2014-05-26&RegionId=cn-beijing

    您可以使用curl或者wget等工具发起HTTP请求调用DescribeDedicatedHosts,查询一台或多台专有宿主机的详细信息。

说明

示例代码的运行环境是Java 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);
        }
    }
}

相关文档

区分ROA风格和RPC风格

  • 本页导读 (1)
  • HTTP 请求结构
  • 公共请求参数
  • 签名机制
  • 步骤一:构造规范化请求字符串
  • 步骤二:构造签名字符串
  • 步骤三:计算签名
  • 签名示例
  • 相关文档
AI助理

点击开启售前

在线咨询服务

你好,我是AI助理

可以解答问题、推荐解决方案等