すべてのプロダクト
Search
ドキュメントセンター

Alibaba Cloud SDK:V3 リクエストの構造と署名

最終更新日:Jun 04, 2026

Alibaba Cloud SDK を使用できない、または使用を希望しない場合は、HTTP リクエストに手動で署名して API オペレーションを呼び出すことができます。このトピックでは、V3 署名の構築とリクエストの署名について説明します。

注意事項

  • 署名メソッド V2 を使用している場合は、V3 に切り替えてください。

  • 署名 V3 は、OpenAPI Explorer で SDK を利用できる Alibaba Cloud サービスに適用されます。一部のサービスは、異なる認証を使用するセルフマネージドゲートウェイを使用しています。HTTP リクエストを送信する前に、サービス固有の署名ドキュメントを確認してください。

HTTP リクエスト構文

Alibaba Cloud API リクエストは、以下のコンポーネントで構成されます。

コンポーネント

必須

説明

プロトコル

はい

リクエストプロトコル: HTTP または HTTPS。セキュリティのため HTTPS を使用してください。有効な値: https:// および http://。サポートされているプロトコルについては、API リファレンスを確認してください。

https://

エンドポイント

はい

サービス API エンドポイント。リージョン固有のエンドポイントについては、API リファレンスを確認してください。

ecs.cn-shanghai.aliyuncs.com

resource_URI_parameters

はい

リソースを指定するリクエストパラメーター。

ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai

リクエストヘッダー

はい

API バージョン、エンドポイント、認証情報を含む共通リクエストヘッダー。以下の「リクエストヘッダー」セクションをご参照ください。

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

リクエストボディ

はい

リクエストボディのパラメーター。詳細については、「OpenAPI メタデータ」をご参照ください。

HTTP メソッド

はい

HTTP メソッド。メソッドについては、「OpenAPI メタデータ」をご参照ください。

POST

リクエストヘッダー

各 API リクエストには、以下のヘッダーを含める必要があります。

ヘッダー

タイプ

必須

説明

host

String

はい

Alibaba Cloud サービス API のエンドポイント。「HTTP リクエスト構文」をご参照ください。

ecs.cn-shanghai.aliyuncs.com

x-acs-action

String

はい

呼び出す API オペレーション。利用可能なオペレーションについては、「OpenAPI Portal」をご参照ください。

RunInstances

x-acs-content-sha256

String

はい

Base16 でエンコードされたリクエストボディのハッシュ。HashedRequestPayload の値と一致する必要があります。

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-date

String

はい

ISO 8601 形式 (yyyy-MM-ddTHH:mm:ssZ、 UTC) のリクエストタイムスタンプ。リクエストの送信時刻から 15 分以内である必要があります。

2023-10-26T10:22:32Z

x-acs-signature-nonce

String

はい

リプレイ攻撃を防ぐための一意のランダム番号。リクエストごとに異なる値を使用してください。HTTP にのみ適用されます。

3156853299f313e23d1673dc12e1703d

x-acs-version

String

はい

API バージョン番号。「x-acs-version の値を取得する方法」をご参照ください。

2014-05-26

Authorization

String

匿名リクエストでない場合に必須

リクエストの検証に用いる認証情報。形式: Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature

SignatureAlgorithm: 署名文字列の暗号化方式。値を ACS3-HMAC-SHA256 に設定します。

Credential: お客様の AccessKey ID。RAM コンソールで確認できます。AccessKey ペアを作成してください。

SignedHeaders: 署名計算に使用するリクエストヘッダー。Authorization を除くすべての共通リクエストヘッダーを使用します。

Signature: リクエスト署名。以下の「署名メソッド」セクションをご参照ください。

ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

x-acs-security-token

String

認証に Security Token Service (STS) を使用する場合は必須

STS トークン。AssumeRole レスポンスの SecurityToken 値を使用します。

署名方法

Alibaba Cloud は、お客様の AccessKey ID と AccessKey Secret を使用して各 HTTP/HTTPS リクエストに署名し、その署名をリクエスト内の署名と比較して、呼び出し元を認証します。

重要

すべてのリクエストとレスポンスは、UTF-8 でエンコードされます。

ステップ 1:正規リクエストの構築

以下の擬似コードを使用して、正規リクエストを構築します。

CanonicalRequest =
  HTTPRequestMethod + '\n' +    // 大文字の HTTP リクエストメソッド。
  CanonicalURI + '\n' +         // 正規化された Uniform Resource Identifier (URI)。
  CanonicalQueryString + '\n' + // 正規化されたクエリ文字列。
  CanonicalHeaders + '\n' +     // 正規ヘッダー。
  SignedHeaders + '\n' +        // 署名計算に使用されるリクエストヘッダー。
  HashedRequestPayload // リクエストボディのハッシュ値。

リクエストメソッド (HTTPRequestMethod)

GET や POST などの、大文字の HTTP リクエストメソッド。

正規化された URI (CanonicalURI)

エンドポイントとクエリ文字列の間の、URI エンコードされたリソースパス。先頭の / を含み、? を除外します。/ で区切られた各パスセグメントを、RFC 3986 に従って UTF-8 でエンコードします。エンコーディングルール:

  • 文字、数字、ハイフン (-) 、アンダースコア (_) 、ピリオド (.) 、チルダ (~) はエンコードする必要はありません。

    その他の文字は、「% + 文字の ASCII コードの 16 進数表記」という形式でパーセントエンコードする必要があります。たとえば、二重引用符 (") は %22 としてエンコードされます。次の表に、エンコード前後のいくつかの特殊文字を示します。これらの特殊文字に注意してください。

    エンコード前

    エンコード後

    スペース文字 ( )

    %20

    アスタリスク (*)

    %2A

    %7E

    Titles (~)

Java 標準ライブラリの java.net.URLEncoder を使用する場合、標準ライブラリに基づいて文字列を encode します。エンコードされた文字列で、プラス記号 (+) を %20 に、アスタリスク (*) を %2A に、%7E をチルダ (~) に置き換えます。これにより、前述のエンコーディングルールに一致するエンコードされた文字列を取得できます。

重要

API スタイルが RPC の場合、CanonicalURI パラメーターの値としてスラッシュ (/) を使用します。

API スタイルが ROA の場合、API のメタデータ内の path パラメーターの値をエンコードし、そのエンコードされた値を CanonicalURI パラメーターの値として使用します。例:/api/v1/clusters

正規化されたクエリ文字列 (CanonicalQueryString)

API メタデータで、API リクエストのリクエストパラメーターに "in": "query" の位置情報が含まれている場合、以下の方法に基づいてリクエストパラメーターを連結する必要があります。

  1. すべてのリクエストパラメーターをパラメーター名のアルファベット順にソートします。

  2. RFC 3986 に基づいて、パラメーター名と値を UTF-8 でエンコードします。エンコーディングルールは、正規化された URI の構築に使用されるものと同じです。

  3. 等号 (=) を使用して、各パラメーターのエンコードされた名前と値を連結します。値のないパラメーターについては、空の文字列をパラメーター値として使用します。

  4. 前の手順で取得した順序で、アンパサンド (&) を使用してエンコードされたパラメーターを連結します。

重要

例:

ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai

ハッシュ化されたリクエストペイロード (HashedRequestPayload)

ハッシュ関数を使用してリクエストボディを変換し、ハッシュ値を Base16 でエンコードして HashedRequestPayload を生成します。次に、リクエストヘッダー内の x-acs-content-sha256 の値を HashedRequestPayload の値に変更します。擬似コード:

HashedRequestPayload = HexEncode(Hash(RequestBody))
  • API メタデータで、リクエストパラメーターに "in": "body" または "in": "formData" の位置情報が含まれている場合は、リクエストボディでパラメーターを指定します。

    説明

    リクエストにボディがない場合、RequestBody の値を空の文字列に設定します。

    • リクエストパラメーターに "in": "formData" の位置情報が含まれている場合は、それらを key1=value1&key2=value2&key3=value3 の形式の文字列に連結し、Content-Type ヘッダーを追加して application/x-www-form-urlencoded に設定します。たとえば、リクエストパラメーターが配列またはオブジェクト型の場合、その値をインデックス付きキーと値のペアに変換します

    • リクエストパラメーターに "in": "body" の位置情報が含まれている場合は、リクエストに Content-Type ヘッダーを追加します。Content-Type ヘッダーの値は、リクエストコンテンツのタイプを指定します。例:

      • リクエストコンテンツが JSON 文字列の場合、Content-Type ヘッダーの値を application/json に設定します。

      • リクエストコンテンツがバイナリファイルストリームの場合、Content-Type ヘッダーの値を application/octet-stream に設定します。

  • Hash() はハッシュ関数です。SHA-256 アルゴリズムのみがサポートされています。

  • HexEncode() は、ハッシュ値を Base16 でエンコードする関数です。この関数は、エンコードされたハッシュ値を小文字の 16 進数形式で返します。

リクエストボディが空の場合のサンプル値:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

正規ヘッダー (CanonicalHeaders)

リクエストヘッダー内のパラメーターを以下の方法に基づいて連結します:

  1. x-acs- で始まるリクエストヘッダー、Host ヘッダー、および Content-Type ヘッダーなど、署名に含めるヘッダーを特定します。

  2. ヘッダー名を小文字に変換し、ヘッダーをアルファベット順にソートします。

  3. 各ヘッダーの値の前後のスペースを削除します。

  4. コロン (:) を使用してヘッダー名とヘッダー値を連結し、名前と値のペアの末尾にラインフィード (\n) を追加して、正規化されたヘッダーエントリを作成します。

  5. 複数の正規化されたヘッダーエントリを文字列に連結します。

説明

Authorization ヘッダーを除くすべてのリクエストヘッダーを署名計算に使用する必要があります。

擬似コード:

CanonicalHeaderEntry = Lowercase(HeaderName) + ':' + Trim(HeaderValue) + '\n'

CanonicalHeaders = 
    CanonicalHeaderEntry0 + CanonicalHeaderEntry1 + ... + CanonicalHeaderEntryN

例:

host:ecs.cn-shanghai.aliyuncs.com
x-acs-action:RunInstances
x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date:2023-10-26T10:22:32Z
x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
x-acs-version:2014-05-26

署名付きヘッダー (SignedHeaders)

署名計算に使用する、セミコロンで区切られた小文字のヘッダー名です。署名付きヘッダーは次のように構築します。

  • 正規ヘッダーの名前を小文字に変換します。

  • ヘッダーをアルファベット順にソートし、セミコロン (;) を使用してヘッダーを区切ります。

    擬似コード:

SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ... + Lowercase(HeaderNameN) 

例:

host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version

ステップ 2:署名対象の文字列の構築

以下の擬似コードを使用して、署名対象の文字列を構築します。

StringToSign =
    SignatureAlgorithm + '\n' +
    HashedCanonicalRequest
  • SignatureAlgorithm

    署名計算では、ACS3-HMAC-SHA256 アルゴリズムのみがサポートされています。

  • HashedCanonicalRequest

    正規リクエストのハッシュ値。以下の擬似コードは、ハッシュ値の生成方法を示しています:

    HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
    • Hash() はハッシュ関数です。SHA-256 アルゴリズムのみがサポートされています。

    • HexEncode() は、ハッシュ値を Base16 でエンコードする関数です。この関数は、エンコードされたハッシュ値を小文字の 16 進数形式で返します。

例:

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259

ステップ 3:署名文字列の計算

以下の擬似コードを使用して署名を計算します:

Signature = HexEncode(SignatureMethod(Secret, StringToSign))
  • StringToSign:ステップ 2 で構築した署名対象の文字列。値は UTF-8 でエンコードされます。

  • SignatureMethod:署名アルゴリズムとして HMAC-SHA256 を指定します。

  • Secret:AccessKey Secret。

  • HexEncode:値を Base16 でエンコードするために使用される関数。

例:

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

ステップ 4:リクエストへの署名文字列の追加

署名文字列を取得したら、リクエストの Authorization ヘッダーを次の形式で指定します:Authorization: <SignatureAlgorithm> Credential=<AccessKeyId>, SignedHeaders=<SignedHeaders>, Signature=<Signature>

例:

ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

シグネチャの例

前述の署名メソッドをご理解いただくため、本セクションでは、主要なプログラミング言語で署名メソッドを実装する完全なサンプルコードを紹介します。このサンプルコードは、あくまで署名メソッドの理解を助けるためのものであり、汎用的なものではありません。Alibaba Cloud は、複数のプログラミング言語と開発フレームワーク向けの SDK を提供しています。API リクエストの送信と署名の自動生成には、Alibaba Cloud SDK を使用することを推奨します。SDK を利用することで、手動で署名を計算することなく、Alibaba Cloud 上でアプリケーションを開発できます。

重要

リクエストに署名する前に、OpenAPI メタデータ、API リクエストメソッド、リクエストパラメーター、リクエストパラメーターの型、およびパラメーターの渡し方をよく読み、理解してください。そうしないと、リクエストの署名に失敗することがあります。

固定パラメーター値

この例では、サンプル値を使用して、各ステップで結果がどのように出力されるかを示します。サンプル値を使用して計算をシミュレートし、ご自身の結果をこの例の結果と比較することで、署名プロセスをテストできます。

パラメーター

アクセスキー ID

YourAccessKeyId

シークレットアクセスキー

YourAccessKeySecret

x-acs-signature-nonce

3156853299f313e23d1673dc12e1703d

x-acs-date

2023-10-26T10:22:32Z

x-acs-action

RunInstances

x-acs-version

2014-05-26

ホスト

ecs.cn-shanghai.aliyuncs.com

オペレーション固有のパラメーター

ImageId

win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd

RegionId

cn-shanghai

リクエストに署名するには、次のステップを実行します。

  1. 正規化されたリクエストを作成します。

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
  1. 署名対象の文字列を作成します。

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
  1. 署名文字列を計算します。

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
  1. 署名文字列をリクエストに追加します。

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-26T10:22:32Z
x-acs-version: 2014-05-26
x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-signature-nonce: 3156853299f313e23d1673dc12e1703d
user-agent: AlibabaCloud (Mac OS X; x86_64) Java/1.8.0_352-b08 tea-util/0.2.6 TeaDSL/1
accept: application/json

Java

説明

この例では、JDK 1.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.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

public class SignatureDemo {

    public static class SignatureRequest {
        // HTTP メソッド
        private final String httpMethod;
        // リクエストパス
        private final String canonicalUri;
        // エンドポイント
        private final String host;
        // API 名
        private final String xAcsAction;
        // API バージョン
        private final String xAcsVersion;
        // ヘッダー
        private final Map<String, String> headers = new TreeMap<>();
        // ボディ内のパラメーターのバイト配列
        private byte[] body;
        // クエリ文字列
        private final Map<String, Object> queryParam = new TreeMap<>();

        public SignatureRequest(String httpMethod, String canonicalUri, String host,
                                String xAcsAction, String xAcsVersion) {
            this.httpMethod = httpMethod;
            this.canonicalUri = canonicalUri;
            this.host = host;
            this.xAcsAction = xAcsAction;
            this.xAcsVersion = xAcsVersion;
            initHeader();
        }

        private void initHeader() {
            headers.put("host", host);
            headers.put("x-acs-action", xAcsAction);
            headers.put("x-acs-version", xAcsVersion);

            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
            headers.put("x-acs-date", sdf.format(new Date()));
            headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
        }

        public String getHttpMethod() {
            return httpMethod;
        }

        public String getCanonicalUri() {
            return canonicalUri;
        }

        public String getHost() {
            return host;
        }

        public Map<String, String> getHeaders() {
            return headers;
        }

        public byte[] getBody() {
            return body;
        }

        public Map<String, Object> getQueryParam() {
            return queryParam;
        }

        public void setBody(byte[] body) {
            this.body = body;
        }

        public void setQueryParam(String key, Object value) {
            this.queryParam.put(key, value);
        }

        public void setHeaders(String key, String value) {
            this.headers.put(key, value);
        }
    }

    public static class SignatureService {
        private static final String ALGORITHM = "ACS3-HMAC-SHA256";

        /**
         * 署名文字列を計算して生成します
         */
        public static void getAuthorization(SignatureRequest signatureRequest,
                                            String accessKeyId, String accessKeySecret, String securityToken) {
            try {
                // List 型と Map 型のクエリパラメーターをフラット化します。
                Map<String, Object> processedQueryParams = new TreeMap<>();
                processObject(processedQueryParams, "", signatureRequest.getQueryParam());
                signatureRequest.getQueryParam().clear();
                signatureRequest.getQueryParam().putAll(processedQueryParams);

                // ステップ 1:正規化されたリクエストを作成します。
                String canonicalQueryString = buildCanonicalQueryString(signatureRequest.getQueryParam());

                // リクエストボディのハッシュ値を計算します。
                String hashedRequestPayload = calculatePayloadHash(signatureRequest.getBody());
                signatureRequest.setHeaders("x-acs-content-sha256", hashedRequestPayload);

                // セキュリティトークンが存在する場合は追加します。
                if (securityToken != null && !securityToken.isEmpty()) {
                    signatureRequest.setHeaders("x-acs-security-token", securityToken);
                }

                // 正規ヘッダーと署名付きヘッダーを構築します。
                CanonicalHeadersResult canonicalHeadersResult = buildCanonicalHeaders(signatureRequest.getHeaders());

                // 正規化されたリクエストを構築します。
                String canonicalRequest = String.join("\n",
                        signatureRequest.getHttpMethod(),
                        signatureRequest.getCanonicalUri(),
                        canonicalQueryString,
                        canonicalHeadersResult.canonicalHeaders,
                        canonicalHeadersResult.signedHeaders,
                        hashedRequestPayload);

                System.out.println("canonicalRequest=========>\n" + canonicalRequest);

                // ステップ 2:署名対象文字列を作成します。
                String hashedCanonicalRequest = sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
                String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
                System.out.println("stringToSign=========>\n" + stringToSign);

                // ステップ 3:署名文字列を計算します。
                String signature = DatatypeConverter.printHexBinary(
                                hmac256(accessKeySecret.getBytes(StandardCharsets.UTF_8), stringToSign))
                        .toLowerCase();
                System.out.println("signature=========>" + signature);

                // ステップ 4:Authorization ヘッダーを指定します。
                String authorization = String.format("%s Credential=%s,SignedHeaders=%s,Signature=%s",
                        ALGORITHM, accessKeyId, canonicalHeadersResult.signedHeaders, signature);

                System.out.println("authorization=========>" + authorization);
                signatureRequest.getHeaders().put("Authorization", authorization);
            } catch (Exception e) {
                throw new RuntimeException("Failed to generate authorization", e);
            }
        }

        /**
         * formData 型のパラメーターを処理します。
         */
        private static String formDataToString(Map<String, Object> formData) {
            Map<String, Object> tileMap = new HashMap<>();
            processObject(tileMap, "", formData);
            StringBuilder result = new StringBuilder();
            boolean first = true;
            String symbol = "&";
            for (Map.Entry<String, Object> entry : tileMap.entrySet()) {
                String value = String.valueOf(entry.getValue());
                if (value != null && !value.isEmpty()) {
                    if (first) {
                        first = false;
                    } else {
                        result.append(symbol);
                    }
                    result.append(percentCode(entry.getKey()));
                    result.append("=");
                    result.append(percentCode(value));
                }
            }

            return result.toString();
        }

        /**
         * 正規化されたクエリ文字列を作成します。
         */
        private static String buildCanonicalQueryString(Map<String, Object> queryParams) {
            return queryParams.entrySet().stream()
                    .map(entry -> percentCode(entry.getKey()) + "=" +
                            percentCode(String.valueOf(entry.getValue())))
                    .collect(Collectors.joining("&"));
        }

        /**
         * リクエストボディのハッシュ値を計算します。
         */
        private static String calculatePayloadHash(byte[] body) throws Exception {
            if (body != null) {
                return sha256Hex(body);
            } else {
                return sha256Hex("".getBytes(StandardCharsets.UTF_8));
            }
        }

        /**
         * 正規ヘッダーを構築します。
         */
        private static CanonicalHeadersResult buildCanonicalHeaders(Map<String, String> headers) {
            List<Map.Entry<String, String>> signedHeaders = headers.entrySet().stream()
                    .filter(entry -> {
                        String key = entry.getKey().toLowerCase();
                        return key.startsWith("x-acs-") || "host".equals(key) || "content-type".equals(key);
                    })
                    .sorted(Map.Entry.comparingByKey())
                    .collect(Collectors.toList());

            StringBuilder canonicalHeaders = new StringBuilder();
            StringBuilder signedHeadersString = new StringBuilder();

            for (Map.Entry<String, String> entry : signedHeaders) {
                String lowerKey = entry.getKey().toLowerCase();
                String value = entry.getValue().trim();
                canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
                signedHeadersString.append(lowerKey).append(";");
            }

            if (signedHeadersString.length() > 0) {
                signedHeadersString.setLength(signedHeadersString.length() - 1); // 末尾のセミコロンを削除します。
            }

            return new CanonicalHeadersResult(canonicalHeaders.toString(), signedHeadersString.toString());
        }

        private static class CanonicalHeadersResult {
            final String canonicalHeaders;
            final String signedHeaders;

            CanonicalHeadersResult(String canonicalHeaders, String signedHeaders) {
                this.canonicalHeaders = canonicalHeaders;
                this.signedHeaders = signedHeaders;
            }
        }

        /**
         * 複雑なオブジェクトパラメーターを処理します。
         */
        private static void processObject(Map<String, Object> map, String key, Object value) {
            if (value == null) {
                return;
            }

            if (key == null) {
                key = "";
            }

            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<?, ?> 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);
                }

                if (value instanceof byte[]) {
                    map.put(key, new String((byte[]) value, StandardCharsets.UTF_8));
                } else {
                    map.put(key, String.valueOf(value));
                }
            }
        }

        /**
         * HMAC-SHA256 アルゴリズムを使用して計算します。
         */
        private static byte[] hmac256(byte[] secretKey, String str) throws Exception {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, mac.getAlgorithm());
            mac.init(secretKeySpec);
            return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
        }

        /**
         * SHA-256 アルゴリズムを使用してハッシュ値を計算します。
         */
        private static String sha256Hex(byte[] input) throws Exception {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(input);
            return DatatypeConverter.printHexBinary(digest).toLowerCase();
        }

        /**
         * URL エンコーディングを行います。
         */
        public static String percentCode(String str) {
            if (str == null) {
                return "";
            }
            try {
                return URLEncoder.encode(str, "UTF-8")
                        .replace("+", "%20")
                        .replace("*", "%2A")
                        .replace("%7E", "~");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException("UTF-8 encoding not supported", e);
            }
        }
    }

    /**
     * 署名サンプル。main() メソッドのパラメーターを調整する必要があります。
     * ROA スタイルの API オペレーションと RPC スタイルの API オペレーションは、canonicalUri 値のロジックのみが異なります。
     * <p>
     * リクエストメソッド (methods) 、リクエストパラメーター名 (name) 、リクエストパラメーターの型 (type) 、リクエストパラメーターの位置 (in) を取得し、その情報を署名リクエスト (SignatureRequest) にカプセル化します。
     * 1. API メタデータのリクエストパラメーターに "in":"query" の位置情報が含まれている場合は、クエリ文字列 (queryParam) でパラメーターを指定します。注:このタイプのパラメーターをボディで指定し、content-type を application/x-www-form-urlencoded に設定することもできます。詳細については、例 3 をご参照ください。
     * 2. API メタデータのリクエストパラメーターに "in": "body" の位置情報が含まれている場合は、ボディでパラメーターを指定し、MIME を application/octet-stream または application/json に設定します。注:RPC API の場合、application/json は推奨されません。代わりに例 3 のメソッドを使用してください。
     * 3. API メタデータのリクエストパラメーターに "in": "formData" の位置情報が含まれている場合は、ボディでパラメーターを指定し、MIME を application/x-www-form-urlencoded に設定します。
     */
    public static void main(String[] args) throws IOException {
        // 環境変数から AccessKey ID を取得します。
        String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
        String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
        String securityToken = System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN");

        if (accessKeyId == null || accessKeySecret == null) {
            System.err.println("ALIBABA_CLOUD_ACCESS_KEY_ID および ALIBABA_CLOUD_ACCESS_KEY_SECRET 環境変数を設定してください。");
            return;
        }

        // 例 1:RPC スタイルで Elastic Compute Service (ECS) の DescribeInstanceStatus オペレーションを呼び出します (パラメーターの位置:"in":"query")。
        SignatureRequest signatureRequest = new SignatureRequest(
                "POST",
                "/",
                "ecs.cn-hangzhou.aliyuncs.com",
                "DescribeInstanceStatus",
                "2014-05-26"
        );
        signatureRequest.setQueryParam("RegionId", "cn-hangzhou");
        signatureRequest.setQueryParam("InstanceId", Arrays.asList("i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"));

        /*// 例 2:OCR の RPC スタイルで RecognizeGeneral オペレーションを呼び出します (パラメーターの位置:"in":"body")。
        SignatureRequest signatureRequest = new SignatureRequest(
                "POST",
                "/",
                "ocr-api.cn-hangzhou.aliyuncs.com",
                "RecognizeGeneral",
                "2021-07-07");
        signatureRequest.setBody(Files.readAllBytes(Paths.get("D:\\test.jpeg")));
        signatureRequest.setHeaders("content-type", "application/octet-stream");*/

        /*// 例 3:RPC スタイルで Machine Translation の TranslateGeneral オペレーションを呼び出します (パラメーターの位置:"in": "formData" または "in":"body")。
        String httpMethod = "POST";
        String canonicalUri = "/";
        String host = "mt.aliyuncs.com";
        String xAcsAction = "TranslateGeneral";
        String xAcsVersion = "2018-10-12";
        SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        Map<String, Object> body = new HashMap<>();
        body.put("FormatType", "text");
        body.put("SourceLanguage", "zh");
        body.put("TargetLanguage", "en");
        body.put("SourceText", "Hello");
        body.put("Scene", "general");
        String formDataToString = SignatureService.formDataToString(body);
        signatureRequest.setBody(formDataToString.getBytes(StandardCharsets.UTF_8));
        signatureRequest.setHeaders("content-type", "application/x-www-form-urlencoded");*/

        /*// ROA スタイルで Container Service for Kubernetes (ACK) の CreateCluster オペレーションの POST リクエストを作成します。
        SignatureRequest signatureRequest = new SignatureRequest(
                "POST",
                "/clusters",
                "cs.cn-chengdu.aliyuncs.com",
                "CreateCluster",
                "2015-12-15");
        TreeMap<String, Object> body = new TreeMap<>();
        body.put("name", "Test");
        body.put("cluster_type", "ManagedKubernetes");
        body.put("kubernetes_version", "1.34.1-aliyun.1");
        body.put("region_id", "cn-chengdu");
        body.put("snat_entry", true);
        body.put("deletion_protection", true);
        body.put("proxy_mode", "ipvs");
        body.put("profile", "Default");
        body.put("timezone", "Asia/Shanghai");
        body.put("cluster_spec", "ack.pro.small");
        body.put("enable_rrsa", false);
        body.put("service_cidr", "192.168.0.0/16");
        body.put("zone_ids", Arrays.asList("cn-chengdu-b","cn-chengdu-b"));
        Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
        signatureRequest.setBody(gson.toJson(body).getBytes(StandardCharsets.UTF_8));
        signatureRequest.setHeaders("content-type", "application/json");*/

        /*// ROA スタイルで ACK の DescribeClusterResources オペレーションの GET リクエストを作成します。
        SignatureRequest signatureRequest = new SignatureRequest(
                "GET",
                "/clusters/" + SignatureService.percentCode("c299f90b63b************") + "/resources",
                "cs.cn-chengdu.aliyuncs.com",
                "DescribeClusterResources",
                "2015-12-15");
        signatureRequest.setQueryParam("with_addon_resources", true);*/

        /*// ROA スタイルで ACK の DeleteCluster オペレーションの DELETE リクエストを作成します。
        SignatureRequest signatureRequest = new SignatureRequest(
                "DELETE",
                "/clusters/" + SignatureService.percentCode("c299f90b63b************"),
                "cs.cn-chengdu.aliyuncs.com",
                "DeleteCluster",
                "2015-12-15");*/

        // 署名を生成します。
        SignatureService.getAuthorization(signatureRequest, accessKeyId, accessKeySecret, securityToken);

        // API を正常に呼び出せるかテストします
        callApi(signatureRequest);
    }

    /**
     * テスト専用
     */
    private static void callApi(SignatureRequest signatureRequest) {
        try {
            String url = "https://" + signatureRequest.getHost() + signatureRequest.getCanonicalUri();
            URIBuilder uriBuilder = new URIBuilder(url);

            // クエリパラメーターを指定します。
            for (Map.Entry<String, Object> entry : signatureRequest.getQueryParam().entrySet()) {
                uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
            }
            HttpUriRequest httpRequest;
            switch (signatureRequest.getHttpMethod()) {
                case "GET":
                    httpRequest = new HttpGet(uriBuilder.build());
                    break;
                case "POST":
                    HttpPost httpPost = new HttpPost(uriBuilder.build());
                    if (signatureRequest.getBody() != null) {
                        httpPost.setEntity(new ByteArrayEntity(signatureRequest.getBody(), ContentType.create(signatureRequest.getHeaders().get("content-type"))));
                    }
                    httpRequest = httpPost;
                    break;
                case "DELETE":
                    httpRequest = new HttpDelete(uriBuilder.build());
                    break;
                default:
                    System.out.println("サポートされていない HTTP メソッド:" + signatureRequest.getHttpMethod());
                    throw new IllegalArgumentException("Unsupported HTTP method");
            }

            // リクエストヘッダーを追加します。
            for (Map.Entry<String, String> entry : signatureRequest.getHeaders().entrySet()) {
                httpRequest.addHeader(entry.getKey(), entry.getValue());
            }

            // リクエストを送信します。
            try (CloseableHttpClient httpClient = HttpClients.createDefault();
                 CloseableHttpResponse response = httpClient.execute(httpRequest)) {
                String result = EntityUtils.toString(response.getEntity(), "UTF-8");
                System.out.println("API Response: " + result);
            }
        } catch (IOException | URISyntaxException e) {
            throw new RuntimeException("Failed to call API", e);
        }
    }
}

Python

説明

この例では、Python 3.12.3 のランタイム環境を使用します。ビジネス要件に基づいてパラメーターを調整してください。

pytz ライブラリと requests ライブラリは手動でインストールする必要があります。Python のバージョンに応じて、ターミナルで次のコマンドを実行してください:

Python 3

pip3 install pytz
pip3 install requests
import hashlib
import hmac
import json
import os
import uuid
from collections import OrderedDict
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from urllib.parse import quote_plus, urlencode

import pytz
import requests

class SignatureRequest:
    def __init__(
            self,
            http_method: str,
            canonical_uri: str,
            host: str,
            x_acs_action: str,
            x_acs_version: str
    ):
        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()  # type: Dict[str, Any]
        self.body = None  # type: Optional[bytes]

    def _init_headers(self) -> Dict[str, str]:
        current_time = datetime.now(pytz.timezone('Etc/GMT'))
        headers = OrderedDict([
            ('host', self.host),
            ('x-acs-action', self.x_acs_action),
            ('x-acs-version', self.x_acs_version),
            ('x-acs-date', current_time.strftime('%Y-%m-%dT%H:%M:%SZ')),
            ('x-acs-signature-nonce', str(uuid.uuid4())),
        ])
        return headers

    def sorted_query_params(self) -> None:
        """クエリパラメーターを名前のアルファベット順で並べ替えます。"""
        self.query_param = dict(sorted(self.query_param.items()))

    def sorted_headers(self) -> None:
        """リクエストヘッダーを名前のアルファベット順で並べ替えます。"""
        self.headers = dict(sorted(self.headers.items()))

def get_authorization(request: SignatureRequest) -> None:
    try:
        new_query_param = OrderedDict()
        process_object(new_query_param, '', request.query_param)
        request.query_param.clear()
        request.query_param.update(new_query_param)
        request.sorted_query_params()

        # Step 1: 正規リクエストを作成します。
        canonical_query_string = "&".join(
            f"{percent_code(quote_plus(k))}={percent_code(quote_plus(str(v)))}"
            for k, v in request.query_param.items()
        )
        hashed_request_payload = sha256_hex(request.body or b'')
        request.headers['x-acs-content-sha256'] = hashed_request_payload

        if SECURITY_TOKEN:
            signature_request.headers["x-acs-security-token"] = SECURITY_TOKEN
        request.sorted_headers()

        filtered_headers = OrderedDict()
        for k, v in request.headers.items():
            if k.lower().startswith("x-acs-") or k.lower() in ["host", "content-type"]:
                filtered_headers[k.lower()] = v

        canonical_headers = "\n".join(f"{k}:{v}" for k, v in filtered_headers.items()) + "\n"
        signed_headers = ";".join(filtered_headers.keys())

        canonical_request = (
            f"{request.http_method}\n{request.canonical_uri}\n{canonical_query_string}\n"
            f"{canonical_headers}\n{signed_headers}\n{hashed_request_payload}"
        )
        print(canonical_request)

        # Step 2: 署名対象文字列を作成します。
        hashed_canonical_request = sha256_hex(canonical_request.encode("utf-8"))
        string_to_sign = f"{ALGORITHM}\n{hashed_canonical_request}"
        print(string_to_sign)

        # Step 3: 署名を計算します。
        signature = hmac256(ACCESS_KEY_SECRET.encode("utf-8"), string_to_sign).hex().lower()

        # Step 4: Authorization ヘッダーに署名を追加します。
        authorization = f'{ALGORITHM} Credential={ACCESS_KEY_ID},SignedHeaders={signed_headers},Signature={signature}'
        request.headers["Authorization"] = authorization
    except Exception as e:
        print("Failed to get authorization")
        print(e)

def form_data_to_string(form_data: Dict[str, Any]) -> str:
    tile_map = OrderedDict()
    process_object(tile_map, "", form_data)
    return urlencode(tile_map)

def process_object(result_map: Dict[str, str], key: str, value: Any) -> None:
    if value is None:
        return

    if isinstance(value, (list, tuple)):
        for i, item in enumerate(value):
            process_object(result_map, f"{key}.{i + 1}", item)
    elif isinstance(value, dict):
        for sub_key, sub_value in value.items():
            process_object(result_map, f"{key}.{sub_key}", sub_value)
    else:
        key = key.lstrip(".")
        result_map[key] = value.decode("utf-8") if isinstance(value, bytes) else str(value)

def hmac256(key: bytes, msg: str) -> bytes:
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()

def sha256_hex(s: bytes) -> str:
    return hashlib.sha256(s).hexdigest()

def call_api(request: SignatureRequest) -> None:
    url = f"https://{request.host}{request.canonical_uri}"
    if request.query_param:
        url += "?" + urlencode(request.query_param, doseq=True, safe="*")

    headers = dict(request.headers)
    data = request.body

    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: str) -> str:
    return encoded_str.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")

# 環境変数からアクセスキー ID とシークレットアクセスキーを取得します。
ACCESS_KEY_ID = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID")
ACCESS_KEY_SECRET = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
SECURITY_TOKEN = os.environ.get("ALIBABA_CLOUD_SECURITY_TOKEN")

ALGORITHM = "ACS3-HMAC-SHA256"

"""
このスクリプトでは、API 署名の計算例を示します。テストを実行するには、main 関数内の例のコメントを解除し、値を変更してください。
ROA API と RPC API の主な違いは、`canonical_uri` の構築方法です。

API メタデータからリクエストメソッド、パラメーター名、型、位置を取得し、それらを使用して `SignatureRequest` オブジェクトを初期化します。
1. API メタデータでパラメーターの位置が "in": "query" の場合は、`query_param` ディクショナリに追加します。content-type を設定する必要はありません。注:RPC API の場合、content-type を `application/x-www-form-urlencoded` に設定して、これらのパラメーターをリクエストボディで渡すこともできます。例 3 を参照してください。
2. API メタデータでパラメーターの位置が "in": "body" の場合は、リクエストボディで渡します。ペイロード形式に応じて `content-type` ヘッダーを設定します。
3. API メタデータでパラメーターの位置が "in": "formData" の場合は、リクエストボディで渡し、`content-type` ヘッダーを `application/x-www-form-urlencoded` に設定します。
"""
if __name__ == "__main__":
    # 例 1:クエリにパラメーターを含む RPC API リクエスト
    http_method = "POST"  # リクエストメソッド。API メタデータから取得できます。POST の使用を推奨します。
    canonical_uri = "/"  # RPC API の場合、リソースパスは空です。`canonical_uri` にはスラッシュ (/) を使用します。
    host = "ecs.cn-hangzhou.aliyuncs.com"  # サービスエンドポイント。
    x_acs_action = "DescribeInstanceStatus"  # API オペレーション。
    x_acs_version = "2014-05-26"  # API バージョン。
    signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # DescribeInstanceStatus のリクエストパラメーターは次のとおりです。
    # API メタデータでは、`RegionId` はクエリ内で必須の String です。
    signature_request.query_param['RegionId'] = 'cn-hangzhou'
    # API メタデータでは、`InstanceId` はクエリ内の任意の配列です。
    signature_request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX",
                                                   "i-bp1incuofvzxXXXXXXXX"]

    # # 例 2:ボディにパラメーターを含む RPC API リクエスト (ファイルアップロードのシナリオ)
    # http_method = "POST"
    # canonical_uri = "/"
    # host = "ocr-api.cn-hangzhou.aliyuncs.com"
    # x_acs_action = "RecognizeGeneral"
    # x_acs_version = "2021-07-07"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # # API メタデータで "in": "body" として指定されているとおり、このパラメーターはリクエストボディで渡します。
    # file_path = "D:\\test.png"
    # with open(file_path, 'rb') as file:
    #     # 画像コンテンツをバイト配列として読み取ります。
    #     signature_request.body = file.read()
    #     signature_request.headers["content-type"] = "application/octet-stream"

    # # 例 3:"formData" またはボディにパラメーターを含む RPC API リクエスト (ファイルアップロード以外のシナリオ)
    # http_method = "POST"
    # canonical_uri = "/"
    # host = "mt.aliyuncs.com"
    # x_acs_action = "TranslateGeneral"
    # x_acs_version = "2018-10-12"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # # TranslateGeneral のリクエストパラメーターは次のとおりです。
    # # API メタデータでは、`Context` はクエリ内で任意の String です。
    # signature_request.query_param['Context'] = 'Morning'
    # # `FormatType`、`SourceLanguage`、`TargetLanguage` などのパラメーターは、API メタデータでは `formData` にあります。
    # form_data = OrderedDict()
    # form_data["FormatType"] = "text"
    # form_data["SourceLanguage"] = "zh"
    # form_data["TargetLanguage"] = "en"
    # form_data["SourceText"] = "Hello"
    # form_data["Scene"] = "general"
    # signature_request.body = bytes(form_data_to_string(form_data), 'utf-8')
    # signature_request.headers["content-type"] = "application/x-www-form-urlencoded"

    # # 例 4:ROA API POST リクエスト
    # http_method = "POST"
    # canonical_uri = "/clusters"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "CreateCluster"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # # API メタデータで "in": "body" として指定されているとおり、このパラメーターはリクエストボディで渡します。
    # body = OrderedDict()
    # body["name"] = "testDemo"
    # body["region_id"] = "cn-beijing"
    # body["cluster_type"] = "ExternalKubernetes"
    # body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
    # body["container_cidr"] = "172.16.1.0/20"
    # body["service_cidr"] = "10.2.0.0/24"
    # body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
    # body["vswitch_ids"] = ["vsw-2zei30dhfldu8XXXXXXXX"]
    # signature_request.body = bytes(json.dumps(body, separators=(',', ':')), 'utf-8')
    # signature_request.headers["content-type"] = "application/json; charset=utf-8"

    # # 例 5:ROA API GET リクエスト
    # http_method = "GET"
    # # `canonical_uri` にパスパラメーターが含まれる場合は、`percent_code()` 関数を使用してエンコードします。
    # cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}/resources"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DescribeClusterResources"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # signature_request.query_param['with_addon_resources'] = True

    # # 例 6:ROA API DELETE リクエスト
    # http_method = "DELETE"
    # # `canonical_uri` にパスパラメーターが含まれる場合は、`percent_code()` 関数を使用してエンコードします。
    # cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DeleteCluster"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)

    get_authorization(signature_request)
    call_api(signature_request)

Go

説明

この例では、go1.22.2 ランタイム環境を使用しています。ビジネス要件に基づいてパラメーターを調整してください。

ターミナルで次のコマンドを実行してください。

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.comcom/google/uuid"
)

type Request struct {
	httpMethod   string
	canonicalUri string
	host         string
	xAcsAction   string
	xAcsVersion  string
	headers      map[string]string
	body         []byte
	queryParam   map[string]interface{}
}

func NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion string) *Request {
	req := &Request{
		httpMethod:   httpMethod,
		canonicalUri: canonicalUri,
		host:         host,
		xAcsAction:   xAcsAction,
		xAcsVersion:  xAcsVersion,
		headers:      make(map[string]string),
		queryParam:   make(map[string]interface{}),
	}
	req.headers["host"] = host
	req.headers["x-acs-action"] = xAcsAction
	req.headers["x-acs-version"] = xAcsVersion
	req.headers["x-acs-date"] = time.Now().UTC().Format(time.RFC3339)
	req.headers["x-acs-signature-nonce"] = uuid.New().String()
	return req
}

// os.Getenv() は、環境変数から AccessKey ID と AccessKey Secret を取得します。
var (
	AccessKeyId     = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
	AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
	SecurityToken   = os.Getenv("ALIBABA_CLOUD_SECURITY_TOKEN")
	ALGORITHM       = "ACS3-HMAC-SHA256"
)

// サンプル署名。main() メソッドのパラメーターを調整してください。
// ROA スタイルの API オペレーションと RPC スタイルの API オペレーションは、canonicalUri 値のロジックのみが異なります。
// リクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、リクエストパラメーターの位置 (in) を取得し、その情報を署名リクエスト (SignatureRequest) にカプセル化します。
// 1. API メタデータのリクエストパラメーターに "in":"query" の位置情報が含まれている場合は、クエリ文字列 (queryParam) でパラメーターを指定します。注:このタイプのパラメーターをボディで指定し、content-type を application/x-www-form-urlencoded に設定することもできます。詳細については、「例 3」をご参照ください。
// 2. API メタデータのリクエストパラメーターに "in": "body" の位置情報が含まれている場合は、ボディでパラメーターを指定し、MIME を application/octet-stream または application/json に設定します。注:RPC API の場合、application/json は推奨されません。代わりに例 3 のメソッドを使用してください。
// 3. API メタデータのリクエストパラメーターに "in": "formData" の位置情報が含まれている場合は、ボディでパラメーターを指定し、MIME を application/x-www-form-urlencoded に設定します。
func main() {
	// 例 1: RPC スタイルで API オペレーションを呼び出します (パラメーターの位置:"in":"query")
	httpMethod := "POST"                   // HTTP リクエストメソッド。ほとんどの場合、POST または GET メソッドを使用して RPC スタイルで API オペレーションを呼び出すことができます。この例では、POST を使用します。
	canonicalUri := "/"                    // RPC API オペレーションのリソースパスは空です。したがって、CanonicalURI パラメーターの値としてスラッシュ (/) を使用します。
	host := "ecs.cn-hangzhou.aliyuncs.com" // クラウドサービスのエンドポイント。
	xAcsAction := "DescribeInstanceStatus" // 実行する API オペレーション。
	xAcsVersion := "2014-05-26"            // API のバージョン番号。
	req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// DescribeInstanceStatus オペレーションを呼び出すためのリクエストパラメーター:
	// RegionId は API メタデータで文字列として定義されています。これは必須パラメーターで、位置は "in":"query" です。
	req.queryParam["RegionId"] = "cn-hangzhou"
	// InstanceId は API メタデータで文字列として定義されています。これはオプションパラメーターで、位置は "in":"query" です。
	instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
	req.queryParam["InstanceId"] = instanceIds

	// // 例 2: RPC スタイルで API オペレーションを呼び出します (パラメーターの位置:"in":"body")
	// httpMethod := "POST"
	// canonicalUri := "/"
	// host := "ocr-api.cn-hangzhou.aliyuncs.com"
	// xAcsAction := "RecognizeGeneral"
	// xAcsVersion := "2021-07-07"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // 画像ファイルの内容を読み取ります。
	// filePath := "D:\\test.png"
	// bytes, err := os.ReadFile(filePath)
	// if err != nil {
	//     fmt.Println("Error reading file:", err)
	//     return
	// }
	// req.body = bytes
	// req.headers["content-type"] = "application/octet-stream"

	// // 例 3: RPC スタイルで API オペレーションを呼び出します (パラメーターの位置:"in": "formData" または "in":"body")
	// httpMethod := "POST"
	// canonicalUri := "/"
	// host := "mt.aliyuncs.com"
	// xAcsAction := "TranslateGeneral"
	// xAcsVersion := "2018-10-12"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // TranslateGeneral オペレーションを呼び出すためのリクエストパラメーター:
	// // Context は API メタデータで文字列として定義されています。これはオプションパラメーターで、位置は "in":"query" です。
	// req.queryParam["Context"] = "Morning"
	// // FormatType、SourceLanguage、TargetLanguage パラメーターは、メタデータで位置が "in":"formData" と定義されています。
	// body := make(map[string]interface{})
	// body["FormatType"] = "text"
	// body["SourceLanguage"] = "zh"
	// body["TargetLanguage"] = "en"
	// body["SourceText"] = "Hello"
	// body["Scene"] = "general"
	// str := formDataToString(body)
	// req.body = []byte(*str)
	// req.headers["content-type"] = "application/x-www-form-urlencoded"

	// // ROA スタイルの API オペレーションの POST リクエストを作成します。
	// httpMethod := "POST"
	// canonicalUri := "/clusters"
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "CreateCluster"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // リクエストパラメーターをカプセル化します。メタデータのリクエストパラメーターに "in": "body" の位置情報が含まれている場合は、ボディでパラメーターを指定します。
	// body := make(map[string]interface{})
	// body["name"] = "testDemo"
	// body["region_id"] = "cn-beijing"
	// body["cluster_type"] = "ExternalKubernetes"
	// body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
	// body["container_cidr"] = "10.0.0.0/8"
	// body["service_cidr"] = "172.16.1.0/20"
	// body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
	// vswitch_ids := []interface{}{"vsw-2zei30dhfldu8XXXXXXXX"}
	// body["vswitch_ids"] = vswitch_ids
	// jsonBytes, err := json.Marshal(body)
	// if err != nil {
	//     fmt.Println("Error marshaling to JSON:", err)
	//     return
	// }
	// req.body = []byte(jsonBytes)
	// req.headers["content-type"] = "application/json; charset=utf-8"

	// // ROA API オペレーションの GET リクエストを作成します。
	// httpMethod := "GET"
	// // canonicalUri パラメーターの値にパスパラメーターの値が含まれている場合は、percentCode({パスパラメーターの値}) を指定してパスパラメーターの値をエンコードします。
	// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66") + "/resources"
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "DescribeClusterResources"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// req.queryParam["with_addon_resources"] = "true"

	// // ROA API オペレーションの DELETE リクエストを作成します。
	// httpMethod := "DELETE"
	// // canonicalUri パラメーターの値にパスパラメーターの値が含まれている場合は、percentCode({パスパラメーターの値}) を指定してパスパラメーターの値をエンコードします。
	// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66")
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "DeleteCluster"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)

	// リクエストに署名します。
	getAuthorization(req)
	// API オペレーションを呼び出します。
	error := callAPI(req)
	if error != nil {
		println(error.Error())
	}
}

func callAPI(req *Request) error {
	urlStr := "https://" + req.host + req.canonicalUri
	q := url.Values{}
	keys := maps.Keys(req.queryParam)
	sort.Strings(keys)
	for _, k := range keys {
		v := req.queryParam[k]
		q.Set(k, fmt.Sprintf("%v", v))
	}
	urlStr += "?" + q.Encode()
	fmt.Println(urlStr)

	httpReq, err := http.NewRequest(req.httpMethod, urlStr, strings.NewReader(string(req.body)))
	if err != nil {
		return err
	}

	for key, value := range req.headers {
		httpReq.Header.Set(key, value)
	}

	client := &http.Client{}
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			return
		}
	}(resp.Body)
	var respBuffer bytes.Buffer
	_, err = io.Copy(&respBuffer, resp.Body)
	if err != nil {
		return err
	}
	respBytes := respBuffer.Bytes()
	fmt.Println(string(respBytes))
	return nil
}

func getAuthorization(req *Request) {
	// List 型と Map 型のクエリパラメーターをフラット化します。
	newQueryParams := make(map[string]interface{})
	processObject(newQueryParams, "", req.queryParam)
	req.queryParam = newQueryParams
	// ステップ 1: 正規化されたリクエストを作成します。
	canonicalQueryString := ""
	keys := maps.Keys(req.queryParam)
	sort.Strings(keys)
	for _, k := range keys {
		v := req.queryParam[k]
		canonicalQueryString += percentCode(url.QueryEscape(k)) + "=" + percentCode(url.QueryEscape(fmt.Sprintf("%v", v))) + "&"
	}
	canonicalQueryString = strings.TrimSuffix(canonicalQueryString, "&")
	fmt.Printf("canonicalQueryString========>%s\n", canonicalQueryString)

	var bodyContent []byte
	if req.body == nil {
		bodyContent = []byte("")
	} else {
		bodyContent = req.body
	}
	hashedRequestPayload := sha256Hex(bodyContent)
	req.headers["x-acs-content-sha256"] = hashedRequestPayload

	if SecurityToken != "" {
		req.headers["x-acs-security-token"] = SecurityToken
	}

	canonicalHeaders := ""
	signedHeaders := ""
	HeadersKeys := maps.Keys(req.headers)
	sort.Strings(HeadersKeys)
	for _, k := range HeadersKeys {
		lowerKey := strings.ToLower(k)
		if lowerKey == "host" || strings.HasPrefix(lowerKey, "x-acs-") || lowerKey == "content-type" {
			canonicalHeaders += lowerKey + ":" + req.headers[k] + "\n"
			signedHeaders += lowerKey + ";"
		}
	}
	signedHeaders = strings.TrimSuffix(signedHeaders, ";")

	canonicalRequest := req.httpMethod + "\n" + req.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
	fmt.Printf("canonicalRequest========>\n%s\n", canonicalRequest)

	// ステップ 2: 署名対象の文字列を作成します。
	hashedCanonicalRequest := sha256Hex([]byte(canonicalRequest))
	stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
	fmt.Printf("stringToSign========>\n%s\n", stringToSign)

	// ステップ 3: 署名文字列を計算します。
	byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
	if err != nil {
		fmt.Println(err)
		panic(err)
	}
	signature := strings.ToLower(hex.EncodeToString(byteData))

	// ステップ 4: Authorization ヘッダーを指定します。
	authorization := ALGORITHM + " Credential=" + AccessKeyId + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
	req.headers["Authorization"] = authorization
}

func hmac256(key []byte, toSignString string) ([]byte, error) {
	// HMAC-SHA256 アルゴリズムが実装されているオブジェクトを取得します。
	h := hmac.New(sha256.New, key)
	// 署名対象の文字列を作成します。
	_, err := h.Write([]byte(toSignString))
	if err != nil {
		return nil, err
	}
	// 署名対象の文字列のハッシュ値を計算し、ハッシュ値を返します。
	return h.Sum(nil), nil
}

func sha256Hex(byteArray []byte) string {
	// SHA-256 アルゴリズムが実装されているオブジェクトを取得します。
	hash := sha256.New()
	// バイト配列をハッシュ関数に書き込みます。
	_, _ = hash.Write(byteArray)
	// SHA-256 アルゴリズムを使用してバイト配列のハッシュ値を計算し、小文字の 16 進数形式の文字列としてハッシュ値を返します。
	hexString := hex.EncodeToString(hash.Sum(nil))

	return hexString
}

func percentCode(str string) string {
	// 特定の文字を置き換えます。
	str = strings.ReplaceAll(str, "+", "%20")
	str = strings.ReplaceAll(str, "*", "%2A")
	str = strings.ReplaceAll(str, "%7E", "~")
	return str
}

func formDataToString(formData map[string]interface{}) *string {
	tmp := make(map[string]interface{})
	processObject(tmp, "", formData)
	res := ""
	urlEncoder := url.Values{}
	for key, value := range tmp {
		v := fmt.Sprintf("%v", value)
		urlEncoder.Add(key, v)
	}
	res = urlEncoder.Encode()
	return &res
}

// processObject: マップやリストなどの複雑なオブジェクトを再帰的にキーと値のペアに分解します。
func processObject(mapResult map[string]interface{}, key string, value interface{}) {
	if value == nil {
		return
	}

	switch v := value.(type) {
	case []interface{}:
		for i, item := range v {
			processObject(mapResult, fmt.Sprintf("%s.%d", key, i+1), item)
		}
	case map[string]interface{}:
		for subKey, subValue := range v {
			processObject(mapResult, fmt.Sprintf("%s.%s", key, subKey), subValue)
		}
	default:
		if strings.HasPrefix(key, ".") {
			key = key[1:]
		}
		if b, ok := v.([]byte); ok {
			mapResult[key] = string(b)
		} else {
			mapResult[key] = fmt.Sprintf("%v", v)
		}
	}
}

Node.js

説明

この例では、Node.js v20.13.1 ランタイム環境を使用しています。ビジネス要件に基づいてパラメーターを調整してください。

この例では、Node.js を使用します。

const crypto = require('crypto');
const fs = require('fs');

class Request {
    constructor(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion) {
        this.httpMethod = httpMethod;
        this.canonicalUri = canonicalUri || '/';
        this.host = host;
        this.xAcsAction = xAcsAction;
        this.xAcsVersion = xAcsVersion;
        this.headers = {};
        this.body = null;
        this.queryParam = {};
        this.initHeader();
    }

    initHeader() {
        const date = new Date();
        this.headers = {
            'host': this.host,
            'x-acs-action': this.xAcsAction,
            'x-acs-version': this.xAcsVersion,
            'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
            'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
        }
    }
}

const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
const securityToken = process.env.ALIBABA_CLOUD_SECURITY_TOKEN;
const encoder = new TextEncoder()

if (!accessKeyId || !accessKeySecret) {
    console.error('ALIBABA_CLOUD_ACCESS_KEY_ID と ALIBABA_CLOUD_ACCESS_KEY_SECRET 環境変数を設定する必要があります。');
    process.exit(1);
}

function getAuthorization(signRequest) {
    try {
        newQueryParam = {};
        processObject(newQueryParam, "", signRequest.queryParam);
        signRequest.queryParam = newQueryParam;
        // ステップ 1:正規リクエストを作成します。
        const canonicalQueryString = Object.entries(signRequest.queryParam)
            .sort(([a], [b]) => a.localeCompare(b))
            .map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
            .join('&');

        // リクエストペイロード。GET リクエストなどでリクエストボディが空の場合は、ペイロードに空の文字列を使用します。
        const requestPayload = signRequest.body || encoder.encode('');
        const hashedRequestPayload = sha256Hex(requestPayload);
        signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;
        if (securityToken) {
            signRequest.headers['x-acs-security-token'] = securityToken;
        }

        // すべてのヘッダー名を小文字に変換します。
        signRequest.headers = Object.fromEntries(
            Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
        );

        const sortedKeys = Object.keys(signRequest.headers)
            .filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
            .sort();
        // 署名付きヘッダーのリスト。ヘッダー名は小文字で、アルファベット順にソートされ、セミコロン (;) で区切られています。
        const signedHeaders = sortedKeys.join(";")
        // 正規ヘッダー。複数の正規化されたヘッダーを、小文字のヘッダー名でアルファベット順にソートし、連結します。
        const canonicalHeaders = sortedKeys.map(key => `${key}:${signRequest.headers[key]}`).join('\n') + '\n';

        const canonicalRequest = [
            signRequest.httpMethod,
            signRequest.canonicalUri,
            canonicalQueryString,
            canonicalHeaders,
            signedHeaders,
            hashedRequestPayload
        ].join('\n');
        console.log('canonicalRequest=========>\n', canonicalRequest);

        // ステップ 2:署名対象文字列を作成します。
        const hashedCanonicalRequest = sha256Hex(encoder.encode(canonicalRequest));
        const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
        console.log('stringToSign=========>', stringToSign);

        // ステップ 3:署名を計算します。
        const signature = hmac256(accessKeySecret, stringToSign);
        console.log('signature=========>', signature);

        // ステップ 4:オーソリゼーションヘッダーを作成します。
        const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
        console.log('authorization=========>', authorization);
        signRequest.headers['Authorization'] = authorization;
    } catch (error) {
        console.error('Failed to get authorization');
        console.error(error);
    }
}

async function callApi(signRequest) {
    try {
        let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
        // リクエストパラメーターを追加します。
        if (signRequest.queryParam) {
            const query = new URLSearchParams(signRequest.queryParam);
            url += '?' + query.toString();
        }
        console.log('url=========>', url);

        // リクエストオプションを設定します。
        let options = {
            method: signRequest.httpMethod.toUpperCase(),
            headers: signRequest.headers
        };

        // リクエストボディを処理します。
        if (signRequest.body && ['POST', 'PUT'].includes(signRequest.httpMethod.toUpperCase())) {
            options.body = signRequest.body;
        }
        return (await fetch(url, options)).text();
    } catch (error) {
        console.error('Failed to send request:', error);
    }
}

function percentCode(str) {
    return encodeURIComponent(str)
        .replace(/\+/g, '%20')
        .replace(/\*/g, '%2A')
        .replace(/~/g, '%7E');
}

function hmac256(key, data) {
    const hmac = crypto.createHmac('sha256', key);
    hmac.update(data, 'utf8');
    return hmac.digest('hex').toLowerCase();
}

function sha256Hex(bytes) {
    const hash = crypto.createHash('sha256');
    const digest = hash.update(bytes).digest('hex');
    return digest.toLowerCase();
}

function formDataToString(formData) {
    const tmp = {};
    processObject(tmp, "", formData);
    let queryString = '';
    for (let [key, value] of Object.entries(tmp)) {
        if (queryString !== '') {
            queryString += '&';
        }
        queryString += encodeURIComponent(key) + '=' + encodeURIComponent(value);
    }
    return queryString;
}

function processObject(map, key, value) {
    // 値が null の場合、それ以上の処理は必要ありません。
    if (value === null) {
        return;
    }
    if (key === null) {
        key = "";
    }

    // 値が配列の場合、その要素を反復処理し、再帰的に処理します。
    if (Array.isArray(value)) {
        value.forEach((item, index) => {
            processObject(map, `${key}.${index + 1}`, item);
        });
    } else if (typeof value === 'object' && value !== null) {
        // 値がオブジェクトの場合、そのキーと値のペアを反復処理し、再帰的に処理します。
        Object.entries(value).forEach(([subKey, subValue]) => {
            processObject(map, `${key}.${subKey}`, subValue);
        });
    } else {
        // キーがピリオド (.) で始まる場合は、それを削除してキーパスを連続させます。
        if (key.startsWith('.')) {
            key = key.slice(1);
        }
        map[key] = String(value);
    }
}

/**
 * これは署名の例です。main 関数のサンプルパラメーターを実際の値に置き換える必要があります。
 * ROA API と RPC API は、canonicalUri を構築するロジックのみが異なります。残りのプロセスは同じです。
 *
 * API メタデータからリクエストメソッド (method)、パラメーター名 (name)、パラメーターの型 (type)、パラメーターの位置 (in) を取得し、これらのパラメーターを Request オブジェクトにカプセル化します。
 * 1. API メタデータでパラメーターの位置が「in」:「query」の場合は、queryParam を使用して渡します。注:RPC API の場合、Content-Type を application/x-www-form-urlencoded にしてリクエストボディでこれらのパラメーターを渡すこともできます。例 3 をご参照ください。
 * 2. パラメーターの位置が「in」:「body」の場合は、Content-Type を application/octet-stream または application/json にしてリクエストボディで渡します。RPC API の場合、application/json は推奨されません。代わりに、例 3 に示す方法を使用してください。
 * 3. パラメーターの位置が「in」:「formData」の場合は、Content-Type を application/x-www-form-urlencoded にしてリクエストボディで渡します。
 */
// 例 1:「query」にパラメーターを含む RPC API リクエスト
const httpMethod = 'POST'; // リクエストメソッド。ほとんどの RPC API は POST と GET の両方をサポートしています。この例では POST を使用します。
const canonicalUri = '/'; // RPC API にはリソースパスがないため、canonicalUri としてスラッシュ (/) を使用します。
const host = 'ecs.cn-hangzhou.aliyuncs.com'; // エンドポイント
const xAcsAction = 'DescribeInstanceStatus'; // API 名
const xAcsVersion = '2014-05-26'; // API バージョン
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// DescribeInstanceStatus のリクエストパラメーターは次のとおりです。
signRequest.queryParam = {
    // API メタデータでは、RegionId は String で、「query」にあり、必須です。
    RegionId: 'cn-hangzhou',
    // API メタデータでは、InstanceId は Array で、「query」にあり、オプションです。
    InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}

// // 例 2:「body」にパラメーターを含む RPC API リクエスト (ファイルアップロードシナリオ)
// const httpMethod = 'POST';
// const canonicalUri = '/';
// const host = 'ocr-api.cn-hangzhou.aliyuncs.com';
// const xAcsAction = 'RecognizeGeneral';
// const xAcsVersion = '2021-07-07';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// const filePath = 'D:\\test.png';
// const bytes = fs.readFileSync(filePath);
// // "in": "body" は、パラメーターがリクエストボディで渡されることを意味します。
// signRequest.body = bytes;
// signRequest.headers['content-type'] = 'application/octet-stream';

// // 例 3:「formData」または「body」にパラメーターを含む RPC API リクエスト (ファイルアップロード以外のシナリオ)
// const httpMethod = 'POST'; // リクエストメソッド。ほとんどの RPC API は POST と GET の両方をサポートしています。この例では POST を使用します。
// const canonicalUri = '/'; // RPC API にはリソースパスがないため、canonicalUri としてスラッシュ (/) を使用します。
// const host = 'mt.aliyuncs.com'; // エンドポイント
// const xAcsAction = 'TranslateGeneral'; // API 名
// const xAcsVersion = '2018-10-12'; // API バージョン
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// // TranslateGeneral のリクエストパラメーターは次のとおりです。
// // API メタデータでは、Context は String で、「query」にあり、オプションです。
// signRequest.queryParam["Context"] = "Morning";
// // API メタデータでは、FormatType、SourceLanguage、TargetLanguage などのパラメーターは「formData」にあります。
// const formData = {
//     SourceLanguage: "zh",
//     TargetLanguage: "en",
//     FormatType: "text",
//     Scene: "general",
//     SourceText: 'Hello'
// }
// const str = formDataToString(formData)
// signRequest.body = encoder.encode(str);
// signRequest.headers['content-type'] = 'application/x-www-form-urlencoded';

// // 例:ROA API 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);
// // "in": "body" は、パラメーターがリクエストボディで渡されることを意味します。
// const body = {
//     name: 'testDemo',
//     region_id: 'cn-beijing',
//     cluster_type: 'ExternalKubernetes',
//     vpcid: 'vpc-2zeou1uod4ylaf35teei9',
//     container_cidr: '10.0.0.0/8',
//     service_cidr: '172.16.3.0/20',
//     security_group_id: 'sg-2ze1a0rlgeo7dj37dd1q',
//     vswitch_ids: [
//         'vsw-2zei30dhfldu8ytmtarro'
//     ],
// }
// signRequest.body = encoder.encode(JSON.stringify(body));
// signRequest.headers['content-type'] = 'application/json';

// // 例:ROA API GET リクエスト
// const httpMethod = 'GET';
// // canonicalUri にパスパラメーターが含まれている場合は、percentCode() 関数を使用してパーセントエンコードする必要があります。
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96") + '/resources';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DescribeClusterResources';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
// signRequest.queryParam = {
//     with_addon_resources: true,
// }

// // 例:ROA API DELETE リクエスト
// const httpMethod = 'DELETE';
// // canonicalUri にパスパラメーターが含まれている場合は、percentCode() 関数を使用してパーセントエンコードする必要があります。
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96");
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DeleteCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

getAuthorization(signRequest);
// API を呼び出します。
callApi(signRequest).then(r => {
    console.log(r);
}).catch(error => {
    console.error(error);
});

PHP

説明

この例では、PHP 7.4.33 のランタイム環境を使用します。ビジネス要件に基づいてパラメーターを調整してください。

<?php

class SignatureDemo
{
    // 暗号化アルゴリズム
    private $ALGORITHM;
    // アクセスキー ID
    private $AccessKeyId;
    // シークレットアクセスキー
    private $AccessKeySecret;

    private $SecurityToken;

    public function __construct()
    {
        date_default_timezone_set('UTC'); // タイムゾーンを UTC に設定します。
        $this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // 環境変数から RAM ユーザーのアクセスキー ID を取得します。
        $this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // 環境変数から RAM ユーザーのシークレットアクセスキーを取得します。
        $this->SecurityToken = getenv('ALIBABA_CLOUD_SECURITY_TOKEN');
        $this->ALGORITHM = 'ACS3-HMAC-SHA256'; // 暗号化アルゴリズムを設定します。
    }

    /**
     * この例では、API 署名の方法を説明します。main メソッド内のサンプルパラメーターは実際の値に置き換える必要があります。
     * ROA API と RPC API の違いは、canonicalUri の値のロジックのみです。それ以外のプロセスは同様です。
     *
     * API メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、パラメーターの型 (type)、パラメーターの位置 (in) を取得し、パラメーターを SignatureRequest にカプセル化します。
     * 1. API メタデータでリクエストパラメーターの位置が "in":"query" と定義されている場合は、queryParam を使用して渡します。注:RPC API の場合、このタイプのパラメーターは、`content-type` を `application/x-www-form-urlencoded` に設定し、ボディで渡すこともできます。詳細については、「例 3」をご参照ください。
     * 2. API メタデータでリクエストパラメーターの位置が "in": "body" と定義されている場合は、MIME タイプを `application/octet-stream` または `application/json` に設定し、ボディで渡します。RPC API の場合、application/json は推奨されません。代わりに「例 3」のメソッドを使用してください。
     * 3. API メタデータでリクエストパラメーターの位置が "in": "formData" と定義されている場合は、MIME タイプを `application/x-www-form-urlencoded` に設定し、ボディで渡します。
     */
    public function main()
    {
        // 例 1:"in": "query" のパラメーターを含む RPC API リクエスト。
        $request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
        // DescribeInstanceStatus のリクエストパラメーターは次のとおりです。
        $request['queryParam'] = [
            // API メタデータでは、RegionId はクエリ内の必須の String です。
            'RegionId' => 'cn-hangzhou',
            // API メタデータでは、InstanceId はクエリ内の任意の配列です。
            'InstanceId' => ["i-bp11ht4h2kdXXXXXXXX", "i-bp16maz3h3xgXXXXXXXX", "i-bp10r67hmslXXXXXXXX"]
        ];

        // // 例 2:"in": "body" のパラメーターを含む RPC API リクエスト (ファイルのアップロードのシナリオ)。
        // $request = $this->createRequest('POST', '/', 'ocr-api.cn-hangzhou.aliyuncs.com', 'RecognizeGeneral', '2021-07-07');
        // // API メタデータでリクエストパラメーターが "in": "body" として指定されています。パラメーターをボディで渡します。
        // $filePath = 'D:\\test.png';
        // // ファイルリソースを使用してバイナリファイルを渡します。
        // $fileResource = fopen($filePath, 'rb');
        // $request['body'] = stream_get_contents($fileResource); // ファイルリソースの内容を読み取ります。
        // $request['headers']['content-type'] = 'application/octet-stream'; // Content-Type を application/octet-stream に設定します。
        // // ファイルリソースを閉じます。
        // fclose($fileResource);

        // // 例 3:"in": "formData" または "in": "body" のパラメーターを含む RPC API リクエスト (ファイルのアップロード以外のシナリオ)。
        // $request = $this->createRequest('POST', '/', 'mt.aliyuncs.com', 'TranslateGeneral', '2018-10-12');
        // // TranslateGeneral のリクエストパラメーターは次のとおりです。
        // $request['queryParam'] = [
        //     // API メタデータでは、Context はクエリ内の任意の String です。
        //     'Context' => 'Morning',
        // ];
        // $formData = [
        //     'FormatType' => 'text',
        //     'SourceLanguage' => 'zh',
        //     'TargetLanguage' => 'en',
        //     'SourceText' => 'Hello',
        //     'Scene' => 'general',
        // ];
        // $str = self::formDataToString($formData);
        // $request['body'] = $str;
        // $request['headers']['content-type'] = 'application/x-www-form-urlencoded';

        // // ROA API POST リクエスト
        // $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
        // $bodyData = [
        //     'name' => 'Test Cluster',
        //     'region_id' => 'cn-beijing',
        //     'cluster_type' => 'ExternalKubernetes',
        //     'vpcid' => 'vpc-2zeou1uod4ylaXXXXXXXX',
        //     'service_cidr' => '10.2.0.0/24',
        //     'security_group_id' => 'sg-2ze1a0rlgeo7XXXXXXXX',
        //     "vswitch_ids" => [
        //         "vsw-2zei30dhfldu8XXXXXXXX"
        //     ]
        // ];
        // $request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
        // $request['headers']['content-type'] = 'application/json; charset=utf-8'; 

        // // ROA API GET リクエスト
        // // canonicalUri にパスパラメーターが含まれる場合は、rawurlencode({path_parameter}) を使用してパスパラメーターをエンコードします。
        // $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
        // $canonicalUri = sprintf("/clusters/%s", rawurlencode($cluster_id));
        // $request = $this->createRequest('GET', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DescribeClusterResources', '2015-12-15');
        // $request['queryParam'] = [
        //     'with_addon_resources' => true,
        // ];

        // // ROA API DELETE リクエスト
        // $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
        // $canonicalUri = sprintf("/clusters/%s", rawurlencode($cluster_id));
        // $request = $this->createRequest('DELETE', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DeleteCluster', '2015-12-15');

        $this->getAuthorization($request);
        // API を呼び出します。
        $this->callApi($request);
    }

    private function createRequest($httpMethod, $canonicalUri, $host, $xAcsAction, $xAcsVersion)
    {
        $headers = [
            'host' => $host,
            'x-acs-action' => $xAcsAction,
            'x-acs-version' => $xAcsVersion,
            'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'),
            'x-acs-signature-nonce' => bin2hex(random_bytes(16)),
        ];
        return [
            'httpMethod' => $httpMethod,
            'canonicalUri' => $canonicalUri,
            'host' => $host,
            'headers' => $headers,
            'queryParam' => [],
            'body' => null,
        ];
    }

    private function getAuthorization(&$request)
    {
        $request['queryParam'] = $this->processObject($request['queryParam']);
        $canonicalQueryString = $this->buildCanonicalQueryString($request['queryParam']);
        $hashedRequestPayload = hash('sha256', $request['body'] ?? '');
        $request['headers']['x-acs-content-sha256'] = $hashedRequestPayload;

        if($this->SecurityToken){
            $request['headers']['x-acs-security-token'] = $this->SecurityToken;
        }

        $canonicalHeaders = $this->buildCanonicalHeaders($request['headers']);
        $signedHeaders = $this->buildSignedHeaders($request['headers']);

        $canonicalRequest = implode("\n", [
            $request['httpMethod'],
            $request['canonicalUri'],
            $canonicalQueryString,
            $canonicalHeaders,
            $signedHeaders,
            $hashedRequestPayload,
        ]);

        $hashedCanonicalRequest = hash('sha256', $canonicalRequest);
        $stringToSign = "{$this->ALGORITHM}\n$hashedCanonicalRequest";

        $signature = strtolower(bin2hex(hash_hmac('sha256', $stringToSign, $this->AccessKeySecret, true)));
        $authorization = "{$this->ALGORITHM} Credential={$this->AccessKeyId},SignedHeaders=$signedHeaders,Signature=$signature";

        $request['headers']['Authorization'] = $authorization;
    }

    private function callApi($request)
    {
        try {
            // cURL を使用してリクエストを送信します。
            $url = "https://" . $request['host'] . $request['canonicalUri'];

            // URL にリクエストパラメーターを追加します。
            if (!empty($request['queryParam'])) {
                $url .= '?' . http_build_query($request['queryParam']);
            }

            echo $url;
            // cURL セッションを初期化します。
            $ch = curl_init();

            // cURL オプションを設定します。
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // SSL 証明書の検証を無効にします。この設定は安全ではないため、本番環境では推奨されません。
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // レスポンスを直接出力するのではなく、文字列として返します。
            curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // リクエストヘッダーを追加します。

            // リクエストタイプに基づいて cURL オプションを設定します。
            switch ($request['httpMethod']) {
                case "GET":
                    break;
                case "POST":
                    curl_setopt($ch, CURLOPT_POST, true);
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
                    break;
                case "DELETE":
                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
                    break;
                default:
                    echo "Unsupported HTTP method: " . $request['httpMethod'];
                    throw new Exception("Unsupported HTTP method");
            }

            // リクエストを送信します。
            $result = curl_exec($ch);

            // エラーを確認します。
            if (curl_errno($ch)) {
                echo "Failed to send request: " . curl_error($ch);
            } else {
                echo $result;
            }

        } catch (Exception $e) {
            echo "Error: " . $e->getMessage();
        } finally {
            // cURL セッションを閉じます。
            curl_close($ch);
        }
    }

    function formDataToString($formData)
    {
        $res = self::processObject($formData);
        return http_build_query($res);
    }

    function processObject($value)
    {
        // 値が null の場合は返します。
        if ($value === null) {
            return;
        }
        $tmp = [];
        foreach ($value as $k => $v) {
            if (0 !== strpos($k, '_')) {
                $tmp[$k] = $v;
            }
        }
        return self::flatten($tmp);
    }

    private static function flatten($items = [], $delimiter = '.', $prepend = '')
    {
        $flatten = [];
        foreach ($items as $key => $value) {
            $pos = \is_int($key) ? $key + 1 : $key;

            if (\is_object($value)) {
                $value = get_object_vars($value);
            }

            if (\is_array($value) && !empty($value)) {
                $flatten = array_merge(
                    $flatten,
                    self::flatten($value, $delimiter, $prepend . $pos . $delimiter)
                );
            } else {
                if (\is_bool($value)) {
                    $value = true === $value ? 'true' : 'false';
                }
                $flatten["$prepend$pos"] = $value;
            }
        }
        return $flatten;
    }

    private function convertHeadersToArray($headers)
    {
        $headerArray = [];
        foreach ($headers as $key => $value) {
            $headerArray[] = "$key: $value";
        }
        return $headerArray;
    }

    private function buildCanonicalQueryString($queryParams)
    {

        ksort($queryParams);
        // クエリパラメーターを構築し、エンコードします。
        $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)
    {
        // キーでヘッダーを並べ替え、連結します。
        uksort($headers, 'strcasecmp');
        $canonicalHeaders = '';
        foreach ($headers as $key => $value) {
            $canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n";
        }
        return $canonicalHeaders;
    }

    private function buildSignedHeaders($headers)
    {
        // 署名ヘッダーの文字列を構築します。
        $signedHeaders = array_keys($headers);
        sort($signedHeaders, SORT_STRING | SORT_FLAG_CASE);
        return implode(';', array_map('strtolower', $signedHeaders));
    }
}

$demo = new SignatureDemo();
$demo->main();

.NET

説明

この例では、.NET 8.0.302 ランタイム環境を使用しています。ビジネス要件に応じてパラメーターを調整してください。

using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using Newtonsoft.Json;

namespace SignatureDemo
{
    public class Request
    {
        public string HttpMethod { get; private set; }
        public string CanonicalUri { get; private set; }
        public string Host { get; private set; }
        public string XAcsAction { get; private set; }
        public string XAcsVersion { get; private set; }
        public SortedDictionary<string, object> Headers { get; private set; }
        public byte[]? Body { get; set; }
        public Dictionary<string, object> QueryParam { get; set; }

        public Request(string httpMethod, string canonicalUri, string host, string xAcsAction, string xAcsVersion)
        {
            HttpMethod = httpMethod;
            CanonicalUri = canonicalUri;
            Host = host;
            XAcsAction = xAcsAction;
            XAcsVersion = xAcsVersion;
            Headers = [];
            QueryParam = [];
            Body = null;
            InitHeader();
        }

        private void InitHeader()
        {
            Headers["host"] = Host;
            Headers["x-acs-action"] = XAcsAction;
            Headers["x-acs-version"] = XAcsVersion;
            DateTime utcNow = DateTime.UtcNow;
            Headers["x-acs-date"] = utcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
            Headers["x-acs-signature-nonce"] = Guid.NewGuid().ToString();
        }
    }

    public class Program
    {
        private static readonly string AccessKeyId = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID") ?? throw new InvalidOperationException("ALIBABA_CLOUD_ACCESS_KEY_ID 環境変数が設定されていません。");
        private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ?? throw new InvalidOperationException("ALIBABA_CLOUD_ACCESS_KEY_SECRET 環境変数が設定されていません。");
        private static readonly string? SecurityToken = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_SECURITY_TOKEN");
        private const string Algorithm = "ACS3-HMAC-SHA256";
        private const string ContentType = "content-type";

        /**
        * この例では、リクエスト署名の作成方法を説明します。main メソッドのサンプルパラメーターは、実際の値に置き換える必要があります。
        * ROA API と RPC API では、canonicalUri の値を決定するロジックのみが異なり、残りのプロセスは同様です。
        *
        * API メタデータから、リクエストメソッド、各リクエストパラメーターの名前、型、および位置 (in) を取得します。次に、この情報を Request オブジェクトにカプセル化します。
        * 1. メタデータでリクエストパラメーターの位置 (in) が query の場合は、QueryParam を使用して渡します。注:RPC API の場合、content-type に application/x-www-form-urlencoded を指定し、これらのパラメーターをリクエストボディで渡すこともできます。詳細は例 3 をご参照ください。
        * 2. メタデータでリクエストパラメーターの位置 (in) が body の場合は、application/octet-stream または application/json としてリクエストボディで渡します。RPC API の場合、application/json の使用は推奨されません。代わりに application/x-www-form-urlencoded (例 3 をご参照ください) を使用してください。
        * 3. メタデータでリクエストパラメーターの位置 (in) が formData の場合は、application/x-www-form-urlencoded としてリクエストボディで渡します。
        */
        public static void Main(string[] args)
        {
            // 例 1:パラメーターをクエリで渡す RPC API リクエスト
            string httpMethod = "POST"; // リクエストメソッド。ほとんどの RPC API は POST と GET の両方をサポートしています。この例では POST を使用します。
            string canonicalUri = "/"; // RPC API にはリソースパスがないため、CanonicalUri にはスラッシュ (/) を使用します。
            string host = "ecs.cn-hangzhou.aliyuncs.com"; // クラウドサービスのエンドポイント。
            string xAcsAction = "DescribeInstanceStatus"; // API オペレーション。
            string xAcsVersion = "2014-05-26"; // API バージョン。
            var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // DescribeInstanceStatus のリクエストパラメーター:
            // RegionId は必須のクエリパラメーターです (文字列型)。
            request.QueryParam["RegionId"] = "cn-hangzhou"; 
            // InstanceId はオプションのクエリパラメーターです (配列型)。
            List<string> instanceIds = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"];
            request.QueryParam["InstanceId"] = instanceIds; 

            // // 例 2:パラメーターをボディで渡す RPC API リクエスト (ファイルアップロード)
            // string httpMethod = "POST"; 
            // string canonicalUri = "/"; 
            // string host = "ocr-api.cn-hangzhou.aliyuncs.com"; 
            // string xAcsAction = "RecognizeGeneral"; 
            // string xAcsVersion = "2021-07-07"; 
            // var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // メタデータでは、リクエストパラメーターは body にあります。パラメーターをリクエストボディで渡します。
            // request.Body = File.ReadAllBytes(@"D:\test.png");
            // request.Headers["content-type"] = "application/octet-stream";

            // // 例 3:パラメーターをフォームデータで渡す RPC API リクエスト (ファイルアップロードではない)
            // string httpMethod = "POST"; 
            // string canonicalUri = "/"; 
            // string host = "mt.aliyuncs.com"; 
            // string xAcsAction = "TranslateGeneral"; 
            // string xAcsVersion = "2018-10-12"; 
            // var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // TranslateGeneral のリクエストパラメーターは次のとおりです:
            // // メタデータでは、Context はオプションのクエリパラメーター (string 型) です。
            // request.QueryParam["Context"] = "Morning"; 
            // // メタデータでは、FormatType、SourceLanguage、TargetLanguage などのパラメーターはフォームデータ内にあります。
            // var body = new Dictionary<string, object>
            // {
            //     { "FormatType", "text" },
            //     { "SourceLanguage", "zh" },
            //     { "TargetLanguage", "en" },
            //     { "SourceText", "Hello" },
            //     { "Scene", "general" },
            // };
            // var str = FormDataToString(body);
            // request.Body = Encoding.UTF8.GetBytes(str);
            // request.Headers[ContentType] = "application/x-www-form-urlencoded";

            // // 例:ROA API POST リクエスト
            // String httpMethod = "POST";
            // String canonicalUri = "/clusters";
            // String host = "cs.cn-beijing.aliyuncs.com";
            // String xAcsAction = "CreateCluster"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // リクエストボディ。JsonConvert を使用してボディを JSON 文字列に変換します。
            // var body = new SortedDictionary<string, object>
            // {
            //     { "name", "testDemo" },
            //     { "region_id", "cn-beijing" },
            //     { "cluster_type", "ExternalKubernetes" },
            //     { "vpcid", "vpc-2zeou1uod4ylaXXXXXXXX" },
            //     { "container_cidr", "10.0.0.0/8" },
            //     { "service_cidr", "172.16.1.0/20" },
            //     { "security_group_id", "sg-2ze1a0rlgeo7XXXXXXXX" },
            //     { "vswitch_ids", new List<string>{"vsw-2zei30dhfldu8XXXXXXXX"} },
            // };
            // string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
            // request.Body = Encoding.UTF8.GetBytes(jsonBody);
            // request.Headers[ContentType] = "application/json; charset=utf-8";

            // // 例:ROA API GET リクエスト
            // String httpMethod = "GET";
            // // canonicalUri にパスパラメーターが含まれる場合は、percentCode({path_parameter}) を使用してパスパラメーターをエンコードする必要があります。
            // String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX") + "/resources";
            // String host = "cs.cn-beijing.aliyuncs.com"; 
            // String xAcsAction = "DescribeClusterResources"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // request.QueryParam["with_addon_resources"]=true;

            // // 例:ROA API DELETE リクエスト
            // String httpMethod = "DELETE";
            // // canonicalUri にパスパラメーターが含まれる場合は、percentCode({path_parameter}) を使用してパスパラメーターをエンコードする必要があります。
            // String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX");
            // String host = "cs.cn-beijing.aliyuncs.com"; 
            // String xAcsAction = "DeleteCluster"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

            GetAuthorization(request);
            // API を呼び出します。
            var result = CallApiAsync(request);
            Console.WriteLine($"result:{result.Result}");
        }

        private static async Task<string?> CallApiAsync(Request request)
        {
            try
            {
                // HttpClient インスタンスを宣言します。
                using var httpClient = new HttpClient();

                // URL を構築します。
                string url = $"https://{request.Host}{request.CanonicalUri}";
                var uriBuilder = new UriBuilder(url);
                var query = new List<string>();

                // リクエストパラメーターを追加します。
                foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
                {
                    string value = entry.Value?.ToString() ?? "";
                    query.Add($"{entry.Key}={Uri.EscapeDataString(value)}");
                }

                uriBuilder.Query = string.Join("&", query);
                Console.WriteLine(uriBuilder.Uri);
                var requestMessage = new HttpRequestMessage
                {
                    Method = new HttpMethod(request.HttpMethod),
                    RequestUri = uriBuilder.Uri,
                };

                // リクエストヘッダーを設定します。
                foreach (var entry in request.Headers)
                {
                    if (entry.Key == "Authorization")
                    {
                        requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString());
                    }
                    else if (entry.Key == ContentType) // content-type ヘッダーは、後でリクエストボディのコンテンツと一緒に設定します。
                    {
                        continue;
                    }
                    else
                    {
                        requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
                    }
                }

                if (request.Body != null)
                {
                    HttpContent content = new ByteArrayContent(request.Body);
                    string contentType = request.Headers["content-type"].ToString();
                    content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
                    requestMessage.Content = content;
                }
                
                // リクエストを送信します。
                HttpResponseMessage response = await httpClient.SendAsync(requestMessage);
                // レスポンスコンテンツを読み取ります。
                string result = await response.Content.ReadAsStringAsync();
                return result;
            }
            catch (UriFormatException e)
            {
                Console.WriteLine("無効な URI 構文");
                Console.WriteLine(e.Message);
                return null;
            }
            catch (Exception e)
            {
                Console.WriteLine("リクエストの送信に失敗しました");
                Console.WriteLine(e);
                return null;
            }
        }

        private static void GetAuthorization(Request request)
        {
            try
            {
                // QueryParam の List または Dictionary パラメーターをフラット化します。
                request.QueryParam = FlattenDictionary(request.QueryParam);

                // ステップ 1:正準リクエストを構築します。
                StringBuilder canonicalQueryString = new();
                foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
                {
                    if (canonicalQueryString.Length > 0)
                    {
                        canonicalQueryString.Append('&');
                    }
                    canonicalQueryString.Append($"{PercentCode(entry.Key)}={PercentCode(entry.Value?.ToString() ?? "")}");
                }

                byte[] requestPayload = request.Body ?? Encoding.UTF8.GetBytes("");
                string hashedRequestPayload = Sha256Hash(requestPayload);
                request.Headers["x-acs-content-sha256"] = hashedRequestPayload;
                if (!string.IsNullOrEmpty(SecurityToken))
                {
                    request.Headers["x-acs-security-token"] = SecurityToken;
                }

                StringBuilder canonicalHeaders = new();
                StringBuilder signedHeadersSb = new();
                foreach (var entry in request.Headers.OrderBy(e => e.Key.ToLower()))
                {
                    if (entry.Key.StartsWith("x-acs-", StringComparison.CurrentCultureIgnoreCase) || entry.Key.Equals("host", StringComparison.OrdinalIgnoreCase) || entry.Key.Equals(ContentType, StringComparison.OrdinalIgnoreCase))
                    {
                        string lowerKey = entry.Key.ToLower();
                        string value = (entry.Value?.ToString() ?? "").Trim();
                        canonicalHeaders.Append($"{lowerKey}:{value}\n");
                        signedHeadersSb.Append($"{lowerKey};");
                    }
                }
                string signedHeaders = signedHeadersSb.ToString().TrimEnd(';');
                string canonicalRequest = $"{request.HttpMethod}\n{request.CanonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
                Console.WriteLine($"canonicalRequest:{canonicalRequest}");

                // ステップ 2:署名対象文字列を構築します。
                string hashedCanonicalRequest = Sha256Hash(Encoding.UTF8.GetBytes(canonicalRequest));
                string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
                Console.WriteLine($"stringToSign:{stringToSign}");

                // ステップ 3:署名を計算します。
                string signature = HmacSha256(AccessKeySecret, stringToSign);

                // ステップ 4:Authorization ヘッダーを構築します。
                string authorization = $"{Algorithm} Credential={AccessKeyId},SignedHeaders={signedHeaders},Signature={signature}";
                request.Headers["Authorization"] = authorization;
                Console.WriteLine($"authorization:{authorization}");
            }
            catch (Exception ex)
            {
                Console.WriteLine("認可の取得に失敗しました");
                Console.WriteLine(ex.Message);
            }
        }

        private static string FormDataToString(Dictionary<string, object> formData)
        {
            Dictionary<string, object> tileMap = FlattenDictionary(formData);
            
            StringBuilder result = new StringBuilder();
            bool first = true;
            string symbol = "&";

            foreach (var entry in tileMap)
            {
                string value = entry.Value?.ToString() ?? "";
                if (!string.IsNullOrEmpty(value))
                {
                    if (!first)
                    {
                        result.Append(symbol);
                    }
                    first = false;
                    result.Append(PercentCode(entry.Key));
                    result.Append("=");
                    result.Append(PercentCode(value));
                }
            }
            return result.ToString();
        }

        private static Dictionary<string, object> FlattenDictionary(Dictionary<string, object> dictionary, string prefix = "")
        {
            var result = new Dictionary<string, object>();
            foreach (var kvp in dictionary)
            {
                string key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";

                if (kvp.Value is Dictionary<string, object> nestedDict)
                {
                    var nestedResult = FlattenDictionary(nestedDict, key);
                    foreach (var nestedKvp in nestedResult)
                    {
                        result[nestedKvp.Key] = nestedKvp.Value;
                    }
                }
                else if (kvp.Value is List<string> list)
                {
                    for (int i = 0; i < list.Count; i++)
                    {
                        result[$"{key}.{i + 1}"] = list[i];
                    }
                }
                else
                {
                    result[key] = kvp.Value;
                }
            }
            return result;
        }

        private static string HmacSha256(string key, string message)
        {
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
            {
                byte[] hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
                return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
            }
        }

        private static string Sha256Hash(byte[] input)
        {
            byte[] hashBytes = SHA256.HashData(input);
            return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        }

        private static string PercentCode(string str)
        {
            if (string.IsNullOrEmpty(str))
            {
                throw new ArgumentException("入力文字列に null または空文字列は指定できません。");
            }
            return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
        }
    }
}

Rust

説明

In this example, the rustc 1.82.0 runtime environment is used. Adjust the parameters based on your business requirements.

To use the signature method in Rust, you must add the following dependencies to the Cargo.toml file:

[dependencies]
serde = { version = "1.0" }
serde_json = "1.0"
rand = "0.8"
base64 = "0.21"
sha2 = "0.10"
chrono = "0.4"
hmac = "0.12"
hex = "0.4"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
percent-encoding = "2.1"
use core::str;
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::time::{SystemTime, SystemTimeError};
use chrono::DateTime;
use hmac::{Hmac, Mac};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use rand::Rng;
use serde_json::{json, Value}; 
use std::borrow::Cow;    
use reqwest::{
    Client,
    header::{HeaderMap, HeaderValue}, Method, Response, StatusCode,
};
use sha2::{Digest, Sha256};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;

// Generates the x-acs-date timestamp.
pub fn current_timestamp() -> Result<u64, SystemTimeError> {
    Ok(SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs())
}
// Percent-encodes a string.
pub fn percent_code(encode_str: &str) -> Cow<'_, str> {
    let encoded = utf8_percent_encode(encode_str, NON_ALPHANUMERIC)
        .to_string()
        .replace("+", "20%")
        .replace("%5F", "_")
        .replace("%2D", "-")
        .replace("%2E", ".")
        .replace("%7E", "~");
        
    Cow::Owned(encoded) // Returns a Cow<str> that can hold either an owned String or a borrowed &str. } fn flatten_target_ops( targets: Vec<HashMap<&str, &str>>, base_key: &str, ) -> Vec<(&'static str, &'static str)> { let mut result = Vec::new(); for (idx, item) in targets.iter().enumerate() { let prefix = format!("{}.{}", base_key, idx + 1); for (&k, &v) in item { let key = format!("{}.{}", prefix, k); let key_static: &'static str = Box::leak(key.into_boxed_str()); let value_static: &'static str = Box::leak(v.to_string().into_boxed_str()); result.push((key_static, value_static)); } } result } /// Calculates the SHA-256 hash of a message. pub fn sha256_hex(message: &str) -> String { let mut hasher = Sha256::new(); hasher.update(message); format!("{:x}", hasher.finalize()).to_lowercase() } // Computes the HMAC-SHA256 signature. pub fn hmac256(key: &[u8], message: &str) -> Result<Vec<u8>, String> { let mut mac = Hmac::<Sha256>::new_from_slice(key) .map_err(|e| format!("use data key on sha256 fail:{}", e))?; mac.update(message.as_bytes()); let signature = mac.finalize(); Ok(signature.into_bytes().to_vec()) } // Generates a nonce. pub fn generate_random_string(length: usize) -> String { const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; let mut rng = rand::thread_rng(); (0..length) .map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char) .collect() } pub fn generate_nonce() -> String { generate_random_string(32) } // Builds the canonical query string from sorted and encoded parameters. pub fn build_sored_encoded_query_string(query_params: &[(&str, &str)]) -> String { let sorted_query_params: BTreeMap<_, _> = query_params.iter().copied().collect(); let encoded_params: Vec<String> = sorted_query_params .into_iter() .map(|(k, v)| { let encoded_key = percent_code(k); let encoded_value = percent_code(v); format!("{}={}", encoded_key, encoded_value) }) .collect(); encoded_params.join("&") } // Reads the response body into a string. pub async fn read_response(result: Response) -> Result<(StatusCode, String), String> { let status = result.status(); let data = result.bytes().await.map_err(|e| format!("Read response body failed: {}", e))?; let res = match str::from_utf8(&data) { Ok(s) => s.to_string(), Err(_) => return Err("Body contains non UTF-8 characters".to_string()), }; Ok((status, res)) } // Defines value types for FormData. #[derive(Debug, Clone)] pub enum FormValue { String(String), Vec(Vec<String>), HashMap(HashMap<String, String>), } // Defines an enum for different request body types: JSON, binary, or FormData. pub enum RequestBody { Json(HashMap<String, Value>), // JSON Binary(Vec<u8>), // Binary FormData(HashMap<String, FormValue>), // FormData None, } // Builds and sends a signed API request. pub async fn call_api( client: Client, method: Method, host: &str, canonical_uri: &str, query_params: &[(&str, &str)], action: &str, version: &str, body: RequestBody, access_key_id: &str, access_key_secret: &str, ) -> Result<String, String> { // Processes the request body according to its type. let body_content = match &body { RequestBody::Json(body_map) => json!(body_map).to_string(), RequestBody::Binary(binary_data) => { STANDARD.encode(binary_data) }, RequestBody::FormData(form_data) => { let params: Vec<String> = form_data .iter() .flat_map(|(k, v)| { match v { FormValue::String(s) => { vec![format!("{}={}", percent_code(k), percent_code(&s))] }, FormValue::Vec(vec) => { vec.iter() .map(|s| format!("{}={}", percent_code(k), percent_code(s))) .collect::<Vec<_>>() }, FormValue::HashMap(map) => { map.iter() .map(|(sk, sv)| format!("{}={}", percent_code(sk), percent_code(sv))) .collect::<Vec<_>>() }, } }) .collect(); params.join("&") }, RequestBody::None => String::new(), }; // Calculate hashes and prepare values for signing. let hashed_request_payload = if body_content.is_empty() { sha256_hex("") } else { sha256_hex(&body_content) }; // x-acs-date in ISO8601 format. let now_time = current_timestamp().map_err(|e| format!("Get current timestamp failed: {}", e))?; let datetime = DateTime::from_timestamp(now_time as i64, 0).ok_or_else(|| format!("Get datetime from timestamp failed: {}", now_time))?; let datetime_str = datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string(); // x-acs-signature-nonce: The unique nonce for the request. let signature_nonce = generate_nonce(); println!("Signature Nonce: {}", signature_nonce); // The headers to include in the signature. let sign_header_arr = &[ "host", "x-acs-action", "x-acs-content-sha256", "x-acs-date", "x-acs-signature-nonce", "x-acs-version", ]; let sign_headers = sign_header_arr.join(";"); // 1. Construct the canonical headers. let mut headers = HeaderMap::new(); headers.insert("Host", HeaderValue::from_str(host).unwrap()); headers.insert("x-acs-action", HeaderValue::from_str(action).unwrap()); headers.insert("x-acs-version", HeaderValue::from_str(version).unwrap()); headers.insert("x-acs-date", HeaderValue::from_str(&datetime_str).unwrap()); headers.insert("x-acs-signature-nonce", HeaderValue::from_str(&signature_nonce).unwrap()); headers.insert("x-acs-content-sha256", HeaderValue::from_str(&hashed_request_payload).unwrap()); // 2. Construct the canonical request string. let canonical_query_string = build_sored_encoded_query_string(query_params); println!("CanonicalQueryString: {}", canonical_query_string); let canonical_request = format!( "{}\n{}\n{}\n{}\n\n{}\n{}", method.as_str(), canonical_uri, canonical_query_string, sign_header_arr.iter().map(|&header| format!("{}:{}", header, headers[header].to_str().unwrap())).collect::<Vec<_>>().join("\n"), sign_headers, hashed_request_payload ); println!("Canonical Request: {}", canonical_request); // 3. Calculate the SHA-256 hash of the canonical request. let result = sha256_hex(&canonical_request); // 4. Construct the string to sign. let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", result); // 5. Calculate the signature. let signature = hmac256(access_key_secret.as_bytes(), &string_to_sign)?; let data_sign = hex::encode(&signature); let auth_data = format!( "ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}", access_key_id, sign_headers, data_sign ); // 6. Build the Authorization header. headers.insert("Authorization", HeaderValue::from_str(&auth_data).unwrap()); // Construct the URL with query parameters. let url: String; if !query_params.is_empty() { url = format!("https://{}{}?{}", host, canonical_uri,canonical_query_string); } else { url = format!("https://{}{}", host, canonical_uri); } // Send the request. let response = send_request( &client, method, &url, headers, query_params, &body, &body_content, ) .await?; // Read the response. let (_, res) = read_response(response).await?; Ok(res) } /// Sends the HTTP request. async fn send_request( client: &Client, method: Method, url: &str, headers: HeaderMap, query_params: &[(&str, &str)], // Request parameters in the query string. body: &RequestBody, // The request body type. body_content: &str, // The serialized request body. ) -> Result<Response, String> { let mut request_builder = client.request(method.clone(), url); // Add request headers. for (k, v) in headers.iter() { request_builder = request_builder.header(k, v.clone()); } // Add the request body. match body { RequestBody::Binary(_) => { request_builder = request_builder.header("Content-Type", "application/octet-stream"); request_builder = request_builder.body(body_content.to_string()); // Move this value. } RequestBody::Json(_) => { // Sets the Content-Type header for non-empty JSON bodies. if !body_content.is_empty() { request_builder = request_builder.body(body_content.to_string()); request_builder = request_builder.header("Content-Type", "application/json; charset=utf-8"); } } RequestBody::FormData(_) => { // For form-data payloads, set the content type. if !body_content.is_empty() { request_builder = request_builder.header("Content-Type", "application/x-www-form-urlencoded"); request_builder = request_builder.body(body_content.to_string()); } } RequestBody::None => { request_builder = request_builder.body(String::new()); } } // Build the request. let request = request_builder .build() .map_err(|e| format!("build request fail: {}", e))?; // Send the request. let response = client .execute(request) .await .map_err(|e| format!("execute request fail: {}", e))?; // Return the response. Ok(response) } /** * * This is a signature example. You must replace the sample parameters in the main function with your actual values. * <p> * You can get the request method, parameter name, type, and location (in) from the API metadata. * 1. If a request parameter is in the query ("in":"query") in the metadata, pass it via query_params. For RPC APIs, you can also pass this type of parameter in the request body with a Content-Type of application/x-www-form-urlencoded. For more information, see Example 3. * 2. If a request parameter is in the body ("in": "body"), pass it as the request body with a MIME type of application/octet-stream or application/json. For RPC APIs, application/json is not recommended. Use the method in Example 3 instead. * 3. If a request parameter is in form data ("in": "formData"), pass it in the request body with a MIME type of application/x-www-form-urlencoded. */ #[tokio::main] async fn main() { // Create an HTTP client. let client = Client::new(); // Retrieve the AccessKey ID and AccessKey secret from environment variables. let access_key_id = env::var("ALIBABA_CLOUD_ACCESS_KEY_ID").expect("Cannot get access key id."); let access_key_secret = env::var("ALIBABA_CLOUD_ACCESS_KEY_SECRET").expect("Cannot get access key id."); let access_key_id: &str = &access_key_id; let access_key_secret: &str = &access_key_secret; // RPC API example 1: Request with "in":"query" parameters (POST) let method = Method::POST; let host = "ecs.cn-hangzhou.aliyuncs.com"; // endpoint for ECS let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI. let action = "DescribeInstanceStatus"; let version = "2014-05-26"; let region_id = "cn-hangzhou"; let instance_ids = vec![ "i-bp11ht4XXXXXXXX", "i-bp16mazXXXXXXXX", ]; let mut query: Vec<(&str, &str)> = Vec::new(); query.push(("RegionId", region_id)); for (index, instance_id) in instance_ids.iter().enumerate() { let key = format!("InstanceId.{}", index + 1); query.push((Box::leak(key.into_boxed_str()), instance_id)); } // The query parameters. let query_params: &[(&str, &str)] = &query; // The request body is empty. let body = RequestBody:: None; // RPC API example: Request with complex "in":"query" parameters (POST) // let method = Method::POST; // let host = "tds.cn-shanghai.aliyuncs.com"; // let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI. // let action = "AddAssetSelectionCriteria"; // let version = "2018-12-03"; // // Define parameters. // let mut target_op = HashMap::new(); // target_op.insert("Operation", "add"); // target_op.insert("Target", "i-2ze1j7ocdXXXXXXXX"); // // Define the TargetOperationList parameter by passing a map into the collection. // let target_operation_list = vec![target_op]; // // Flatten parameters. // let mut query = flatten_target_ops(target_operation_list, "TargetOperationList"); // // Add other parameters. // query.push(("SelectionKey", "85a561b7-27d5-47ad-a0ec-XXXXXXXX")); // let query_params: &[(&str, &str)] = &query; // let body = RequestBody:: None; // RPC API example 2: Request with "in":"body" parameter for a file upload (POST) // let method = Method::POST; // let host = "ocr-api.cn-hangzhou.aliyuncs.com"; // let canonical_uri = "/"; // let action = "RecognizeGeneral"; // let version = "2021-07-07"; // // An "in":"body" parameter for a binary file upload. // let binary_data = std::fs::read("<FILE_PATH>").expect("Failed to read the file."); // Replace  with the actual file path. // // When the body is a binary type. // let body = RequestBody::Binary(binary_data); // // The query parameters are empty. // let query_params = &[]; // RPC API example 3: Request with "in":"formData" or "in":"body" parameters for a non-file upload (POST) // let method = Method::POST; // let host = "mt.aliyuncs.com"; // let canonical_uri = "/"; // let action = "TranslateGeneral"; // let version = "2018-10-12"; // // Parameters such as FormatType, SourceLanguage, and TargetLanguage are specified as "in":"formData" in the API metadata. // let mut form_data = HashMap::new(); // The body type is FormData(HashMap<String, FormValue>). FormValue supports Vec<String>, HashMap<String, String>, etc. You can add more types to the FormValue enum as needed. // form_data.insert(String::from("FormatType"),FormValue::String(String::from("text"))); // form_data.insert(String::from("SourceLanguage"),FormValue::String(String::from("zh"))); // form_data.insert(String::from("TargetLanguage"),FormValue::String(String::from("en"))); // form_data.insert(String::from("SourceText"),FormValue::String(String::from("Hello"))); // form_data.insert(String::from("Scene"),FormValue::String(String::from("general"))); // let query_params = &[("Context", "morning")]; // let body = RequestBody::FormData(form_data); // ROA API POST request: CreateCluster // // Define API request details. // let method = Method::POST; // let host = "cs.cn-hangzhou.aliyuncs.com"; // let canonical_uri = "/clusters"; // let action = "CreateCluster"; // let version = "2015-12-15"; // // Set the request body parameters. // let mut body_json = HashMap::new(); // The body type is Json(HashMap<String, Value>). Value supports types like String, Number, Boolean, Null, Array, and nested JSON objects. // body_json.insert(String::from("name"),json!("Test-Cluster")); // body_json.insert(String::from("region_id"),json!("cn-hangzhou")); // body_json.insert(String::from("cluster_type"),json!("ExternalKubernetes")); // body_json.insert(String::from("vpcid"),json!("vpc-2zeou1uodXXXXXXXX")); // body_json.insert(String::from("container_cidr"),json!("10.X.X.X/X")); // body_json.insert(String::from("service_cidr"),json!("10.X.X.X/X")); // body_json.insert(String::from("security_group_id"),json!("sg-2ze1a0rlgXXXXXXXX")); // body_json.insert( // String::from("vswitch_ids"), // Value::Array(vec![ // Value::from("vsw-2zei30dhflXXXXXXXX"), // Value::from("vsw-2zei30dhflXXXXXXXX"), // Value::from("vsw-2zei30dhflXXXXXXXX"), // ]), // ); // // The query parameters are empty. // let query_params = &[]; // let body = RequestBody::Json(body_json); // ROA API GET request: DescribeClusterResources // let method = Method::GET; // let host = "cs.cn-hangzhou.aliyuncs.com"; // // Construct the resource path. // let uri = format!("/clusters/{}/resources", percent_code("ce196d21571a64be9XXXXXXXX").as_ref()); // let canonical_uri = uri.as_str(); // The resource path as an &str type. // let action = "DescribeClusterResources"; // let version = "2015-12-15"; // // Set the query parameters. // let query_params = &[("with_addon_resources", if true { "true" } else { "false" })]; // Can be "true" or "false". // // Set the request body to empty. // let body = RequestBody:: None; // ROA API DELETE request: Delete a pay-as-you-go cluster // let method = Method::DELETE; // let host = "cs.cn-hangzhou.aliyuncs.com"; // let uri = format!("/clusters/{}", percent_code("ce0138ff31ad044f8XXXXXXXX").as_ref()); // let canonical_uri = uri.as_str(); // The resource path as an &str type. // let action = "DeleteCluster"; // let version = "2015-12-15"; // // The query parameters are empty. // let query_params = &[]; // // The request body is empty. // let body = RequestBody:: None; // RPC API example: SendSms // let method = Method::POST; // let host = "dysmsapi.aliyuncs.com"; // let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI. // let action = "SendSms"; // let version = "2017-05-25"; // let mut query: Vec<(&str, &str)> = Vec::new(); // query.push(("PhoneNumbers", "<YOUR_PHONENUMBERS>")); // query.push(("TemplateCode", "<YOUR_TEMPLATECODE>")); // query.push(("SignName", "<YOUR_SIGNNAME>")); // query.push(("TemplateParam", "<YOUR_TEMPLATEPARAM>")); // // The query parameters. // let query_params: &[(&str, &str)] = &query; // // The request body is empty. // let body = RequestBody:: None; // Send the request. match call_api( client.clone(), method, // request method: POST, GET, or DELETE host, canonical_uri, query_params, // query parameters ("in":"query") action, version, body, // request body ("in":"body"), supports JSON, FormData, and binary types access_key_id, access_key_secret, ) .await { Ok(response) => println!("Response: {}", response), Err(error) => eprintln!("Error: {}", error), } } 

シェル

#!/bin/bash

accessKey_id="<YOUR-ACCESSKEY-ID>"
accessKey_secret="<YOUR-ACCESSKEY-SECRET>"
algorithm="ACS3-HMAC-SHA256"

# リクエストパラメーター:ビジネス要件に応じてリクエストパラメーターを調整してください。
httpMethod="POST"
host="dns.aliyuncs.com"
queryParam=("DomainName=example.com" "RRKeyWord=@")
action="DescribeDomainRecords"
version="2015-01-09"
canonicalURI="/"
# body または formData 型のパラメーターは body で指定できます。
# パラメーターが body の場合、body の値は JSON 文字列です:"{'key1':'value1','key2':'value2'}"。署名計算に使用するヘッダーに content-type:application/json; charset=utf-8 を追加してください。
# パラメーターがバイナリファイルの場合、body を変更する必要はありません。署名計算に使用するヘッダーに content-type:application/octet-stream を追加し、curl_command 変数に --data-binary を追加します。
# パラメーターが formData の場合、body の値を "key1=value1&key2=value2" の形式で指定し、署名計算に使用するヘッダーに content-type:application/x-www-form-urlencoded を追加します。
body=""

# UTC 時刻を ISO 8601 形式で指定します。
utc_timestamp=$(date +%s)
utc_date=$(date -u -d @${utc_timestamp} +"%Y-%m-%dT%H:%M:%SZ") 
# x-acs-signature-nonce ヘッダーにランダムな文字列を設定します。
random=$(uuidgen | sed 's/-//g') 

# 署名計算に使用するリクエストヘッダーを追加します。
headers="host:${host}
x-acs-action:${action}
x-acs-version:${version}
x-acs-date:${utc_date}
x-acs-signature-nonce:${random}"

# URL エンコード関数。
urlencode() {
    local string="${1}"
    local strlen=${#string}
    local encoded=""
    local pos c o

    for (( pos=0 ; pos<strlen ; pos++ )); do
        c=${string:$pos:1}
        case "$c" in
            [-_.~a-zA-Z0-9] ) o="${c}" ;;
            * )               printf -v o '%%%02X' "'$c"
        esac
        encoded+="${o}"
    done
    echo "${encoded}"
}

# ステップ 1:正規化されたリクエストを構築します。
# すべてのクエリパラメーターをフラット化します。
newQueryParam=()

# 元のクエリパラメーターを走査します。
for param in "${queryParam[@]}"; do
    # クエリパラメーターの値に等号 (=) が含まれているか確認します。含まれている場合、そのパラメーターはキーと値のペアです。
    if [[ "$param" == *"="* ]]; then
        # パラメーターをキーと値に分割します。
        IFS='=' read -r key value <<< "$param"

        # 値を URL エンコードします。
        value=$(urlencode "$value")

        # 値がリストかどうかを確認します。リストの場合、値は括弧 () で囲まれています。
        if [[ "$value" =~ ^\(.+\)$ ]]; then
            # 括弧 () を削除します。
            value="${value:1:-1}"

            # 内部フィールドセパレーター (IFS) を使用して値を分割します。
            IFS=' ' read -ra values <<< "$value"

            # 各値にインデックスを追加します。
            index=1
            for val in "${values[@]}"; do
                # 二重引用符 (") を削除します。
                val="${val%\"}"
                val="${val#\"}"

                # クエリパラメーターを新しい配列に追加します。
                newQueryParam+=("$key.$index=$val")
                ((index++))
            done
        else
            # 値がリストでない場合、クエリパラメーターを新しい配列に追加します。
            newQueryParam+=("$key=$value")
        fi
    else
        # パラメーターに等号 (=) が含まれない場合、変更せずに新しい配列に追加します。
        newQueryParam+=("$param")
    fi
done

# 新しいクエリパラメーターを処理してソートします。
sortedParams=()
declare -A paramsMap
for param in "${newQueryParam[@]}"; do
    IFS='=' read -r key value <<< "$param"
    paramsMap["$key"]="$value"
done
# クエリパラメーターをキーでソートします。
for key in $(echo ${!paramsMap[@]} | tr ' ' '\n' | LC_ALL=C sort); do
    sortedParams+=("$key=${paramsMap[$key]}")
done

# 1.1 正規化されたクエリ文字列を構築します。
canonicalQueryString=""
first=true
for item in "${sortedParams[@]}"; do
    [ "$first" = true ] && first=false || canonicalQueryString+="&"
    # 等号 (=) が存在するかどうかを確認します。
    if [[ "$item" == *=* ]]; then
        canonicalQueryString+="$item"
    else
        canonicalQueryString+="$item="
    fi
done

# 1.2 リクエストボディを処理します。
hashedRequestPayload=$(echo -n "$body" | openssl dgst -sha256 | awk '{print $2}')
headers="${headers}
x-acs-content-sha256:$hashedRequestPayload"

# 1.3 正規化されたヘッダーを構築します。
canonicalHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
    key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
    value=$(echo "$line" | cut -d':' -f2-)
    echo "${key}:${value}"
done | sort | tr '\n' '\n')

signedHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
    key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
    echo "$key"
done | sort | tr '\n' ';' | sed 's/;$//')

# 1.4 正規化されたリクエストを構築します。
canonicalRequest="${httpMethod}\n${canonicalURI}\n${canonicalQueryString}\n${canonicalHeaders}\n\n${signedHeaders}\n${hashedRequestPayload}"
echo -e "canonicalRequest=${canonicalRequest}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

str=$(echo "$canonicalRequest" | sed 's/%/%%/g')
hashedCanonicalRequest=$(printf "${str}" | openssl sha256 -hex | awk '{print $2}')
# ステップ 2:署名文字列を構築します。
stringToSign="${algorithm}\n${hashedCanonicalRequest}"
echo -e "stringToSign=$stringToSign"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# ステップ 3:署名を計算します。
signature=$(printf "${stringToSign}" | openssl dgst -sha256 -hmac "${accessKey_secret}" | sed 's/^.* //')
echo -e "signature=${signature}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# ステップ 4:Authorization ヘッダーを構築します。
authorization="${algorithm} Credential=${accessKey_id},SignedHeaders=${signedHeaders},Signature=${signature}"
echo -e "authorization=${authorization}"

# cURL コマンドを構築します。
url="https://$host$canonicalURI"
curl_command="curl -X $httpMethod '$url?$canonicalQueryString'"

# Authorization ヘッダーを追加します。
IFS=$'\n'  # 改行 (\n) を新しい IFS として使用します。
for header in $headers; do
    curl_command="$curl_command -H '$header'"
done
curl_command+=" -H 'Authorization:$authorization'"
# body がバイナリファイルの場合、次の行のコードをコメントアウトしてください。
curl_command+=" -d '$body'"
# body がバイナリファイルの場合、次の行のコードのコメントを解除してください。
#curl_command+=" --data-binary @"/root/001.png" "

echo "$curl_command"
# cURL コマンドを実行します。
eval "$curl_command"

C言語

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>
#include <stdint.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/rand.h>
#include <curl/curl.h>

// 環境変数からアクセスキー ID とシークレットアクセスキーを取得します。
#define ACCESS_KEY_ID getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
#define ACCESS_KEY_SECRET getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
#define ALGORITHM "ACS3-HMAC-SHA256"
#define BUFFER_SIZE 4096

// ソート用のキーと値のペア。
typedef struct {
    char key[256];
    char value[256];
} KeyValuePair;

// キーを辞書順にソートするための比較関数。
int compare_pairs(const void *a, const void *b) {
    return strcmp(((const KeyValuePair *)a)->key, ((const KeyValuePair *)b)->key);
}

// 文字列を URL エンコードします。
char* percentEncode(const char* str) {
    if (str == NULL) {
        fprintf(stderr, "The input string cannot be null\n");
        return NULL;
    }
    size_t len = strlen(str);
    char* encoded = (char*)malloc(len * 3 + 1);
    if (encoded == NULL) {
        fprintf(stderr, "Failed to allocate memory\n");
        free(encoded); 
        return NULL;
    }
    char* ptr = encoded;
    for (size_t i = 0; i < len; i++) {
        unsigned char c = (unsigned char)str[i];
        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
            *ptr++ = c;
        } else {
            ptr += sprintf(ptr, "%%%02X", c);
        }
    }
    *ptr = '\0'; 
    char* finalEncoded = malloc(strlen(encoded) + 1);
    if (finalEncoded) {
        char* fptr = finalEncoded;
        for (size_t j = 0; j < strlen(encoded); j++) {
            if (encoded[j] == '+') {
                strcpy(fptr, "%20");
                fptr += 3; 
            } else if (encoded[j] == '*') {
                strcpy(fptr, "%2A");
                fptr += 3;
            } else if (encoded[j] == '~') {
                *fptr++ = '~';
            } else {
                *fptr++ = encoded[j];
            }
        }
        *fptr = '\0'; 
    }

    free(encoded); 
    return finalEncoded;
}

/**
 * @brief クエリパラメーターを URL エンコードしてソートし、正規化されたクエリ文字列を生成します。
 * @param query_params 元のクエリパラメーター文字列。例:"key1=value1&key2=value2"。
 * @return char* 正規化されたクエリ文字列へのポインター。呼び出し元は割り当てられたメモリを解放する必要があります。
 */
char* generate_sorted_encoded_query(const char* query_params) {
    if (query_params == NULL || strlen(query_params) == 0) {
        return strdup(""); // パラメーターが空の場合は、空文字列を返します。
    }

    KeyValuePair pairs[100]; // 最大 100 個のキーと値のペアをサポートします。
    int pair_count = 0;

    char* copy = strdup(query_params);
    if (!copy) {
        fprintf(stderr, "Failed to allocate memory\n");
        return NULL;
    }

    char* token = NULL;
    char* saveptr = NULL;
    token = strtok_r(copy, "&", &saveptr);

    while (token != NULL && pair_count < 100) {
        char* eq = strchr(token, '=');
        if (eq) {
            size_t key_len = eq - token;
            char key[256], value[256];

            strncpy(key, token, key_len);
            key[key_len] = '\0';

            const char* val = eq + 1;
            strncpy(value, val, sizeof(value) - 1);
            value[sizeof(value) - 1] = '\0';

            char* encoded_key = percentEncode(key);
            char* encoded_value = percentEncode(value);

            strncpy(pairs[pair_count].key, encoded_key, sizeof(pairs[pair_count].key));
            strncpy(pairs[pair_count].value, encoded_value, sizeof(pairs[pair_count].value));
            pair_count++;

            free(encoded_key);
            free(encoded_value);
        }
        token = strtok_r(NULL, "&", &saveptr);
    }

    free(copy);

    // キーでソートします。
    qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);

    // ソート済みのクエリパラメーターを連結します。
    char* query_sorted = malloc(BUFFER_SIZE);
    if (!query_sorted) {
        fprintf(stderr, "Failed to allocate memory\n");
        return NULL;
    }
    query_sorted[0] = '\0';

    for (int i = 0; i < pair_count; ++i) {
        if (i == 0) {
            snprintf(query_sorted, BUFFER_SIZE, "%s=%s", pairs[i].key, pairs[i].value);
        } else {
            char temp[512];
            snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
            strncat(query_sorted, temp, BUFFER_SIZE - strlen(query_sorted) - 1);
        }
    }

    return query_sorted;
}

// HMAC-SHA256 ハッシュを計算します。
void hmac256(const char *key, const char *message, char *output) {
    unsigned char hmac[SHA256_DIGEST_LENGTH];
    unsigned int result_len;
    HMAC(EVP_sha256(), key, strlen(key), (unsigned char *)message, strlen(message), hmac, &result_len);
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        sprintf(output + (i * 2), "%02x", hmac[i]);
    }
    output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// SHA-256 ハッシュを計算します。
void sha256_hex(const char *input, char *output) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((unsigned char *)input, strlen(input), hash);
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        sprintf(output + (i * 2), "%02x", hash[i]);
    }
    output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// x-acs-signature-nonce を生成します。
void generate_uuid(char *uuid, size_t size) {
    if (size < 37) { 
        fprintf(stderr, "Buffer size too small for UUID\n");
        return;
    }
    unsigned char random_bytes[16];
    RAND_bytes(random_bytes, sizeof(random_bytes));
    random_bytes[6] &= 0x0f; // 上位4ビットをクリアします。
    random_bytes[6] |= 0x40; // バージョンを 4 に設定します。
    random_bytes[8] &= 0x3f; // 上位2ビットをクリアします。
    random_bytes[8] |= 0x80; // バリアントを 10xx に設定します。
    snprintf(uuid, size,
             "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
             random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
             random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
             random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
             random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
}
// ファイルを読み取ります。
size_t read_file(const char *file_path, char **buffer) {
    FILE *file = fopen(file_path, "rb");
    if (!file) {
        fprintf(stderr, "Cannot open file %s\n", file_path);
        return 0; // 読み取りに失敗しました。
    }
    fseek(file, 0, SEEK_END);
    size_t file_size = ftell(file);
    fseek(file, 0, SEEK_SET);

    *buffer = (char *)malloc(file_size);
    if (!*buffer) {
        fprintf(stderr, "Failed to allocate memory for file buffer\n");
        fclose(file);
        return 0; // 読み取りに失敗しました。
    }
    fread(*buffer, 1, file_size, file);
    fclose(file);
    return file_size; // 読み取ったバイト数を返します。
}
// Authorization ヘッダーを計算します。
char* get_authorization(const char *http_method, const char *canonical_uri, const char *host,
                       const char *x_acs_action, const char *x_acs_version, const char *query_params,
                       const char *body, char *authorization_header,
                        char *hashed_payload, char *x_acs_date, char *uuid) {
    // x-acs-signature-nonce、x-acs-date、x-acs-content-sha256、および署名対象文字列を準備します。
    generate_uuid(uuid, 37);
    // x-acs-date の形式は yyyy-MM-ddTHH:mm:ssZ です。例:2025-04-17T07:19:10Z。
    time_t now = time(NULL);
    struct tm *utc_time = gmtime(&now);
    strftime(x_acs_date, 64, "%Y-%m-%dT%H:%M:%SZ", utc_time);
    // 署名対象文字列。
    char signed_headers[] = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
    // x-acs-content-sha256 
    sha256_hex(body ? body : "", hashed_payload);
    printf("Generated x-acs-content-sha256: %s\n", hashed_payload);
    // 1. 正規化されたリクエストヘッダーを作成します。
    char canonical_headers[BUFFER_SIZE];
    snprintf(canonical_headers, sizeof(canonical_headers),
             "host:%s\nx-acs-action:%s\nx-acs-content-sha256:%s\nx-acs-date:%s\nx-acs-signature-nonce:%s\nx-acs-version:%s",
              host, x_acs_action, hashed_payload, x_acs_date, uuid, x_acs_version);
    printf("Canonical Headers:\n%s\n", canonical_headers);

    // 2. 正規化されたリクエストを作成します。
    // クエリパラメーターをソートしてエンコードします。
    char* sorted_query_params = generate_sorted_encoded_query(query_params);
    if (!sorted_query_params) {
      fprintf(stderr, "Failed to generate the sorted query string\n");
      return NULL;
    }
    char canonical_request[BUFFER_SIZE];
    snprintf(canonical_request, sizeof(canonical_request),
         "%s\n%s\n%s\n%s\n\n%s\n%s",
         http_method,
         canonical_uri,
         sorted_query_params ? sorted_query_params : "",
         canonical_headers,
         signed_headers,
         hashed_payload);
    printf("Canonical Request:\n%s\n", canonical_request);

    // 3. 正規化されたリクエストの SHA-256 ハッシュを計算します。
    char hashed_canonical_request[SHA256_DIGEST_LENGTH * 2 + 1];
    sha256_hex(canonical_request, hashed_canonical_request);
    printf("hashedCanonicalRequest: %s\n", hashed_canonical_request);
    // 4. 署名対象文字列を作成します。
    char string_to_sign[BUFFER_SIZE];
    snprintf(string_to_sign, sizeof(string_to_sign), "%s\n%s", ALGORITHM, hashed_canonical_request);
    printf("stringToSign:\n%s\n", string_to_sign);
    // 5. 署名を計算します。
    char signature[SHA256_DIGEST_LENGTH * 2 + 1];
    hmac256(ACCESS_KEY_SECRET, string_to_sign, signature);
    printf("Signature: %s\n", signature);
    // 6. Authorization ヘッダーを作成します。
    snprintf(authorization_header, BUFFER_SIZE,
             "%s Credential=%s,SignedHeaders=%s,Signature=%s",
             ALGORITHM, ACCESS_KEY_ID, signed_headers, signature);
    printf("Authorization: %s\n", authorization_header);

    return sorted_query_params;
}
// API リクエストを送信します。
void call_api(const char *http_method, const char *canonical_uri, const char *host,
              const char *x_acs_action, const char *x_acs_version, const char *query_params,
              const char *body,const char *content_type, size_t body_length) {
    // 署名の計算用変数。
    char authorization_header[BUFFER_SIZE];
    char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
    char x_acs_date[64];
    char uuid[37];
    // 1. cURL を初期化します。
    CURL *curl = curl_easy_init();
    if (!curl) {
        fprintf(stderr, "curl_easy_init() failed\n");
        goto cleanup;
    }
    // 2. 署名を計算し、正規化されたクエリ文字列を取得します。
    char *signed_query_params = get_authorization(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, authorization_header, hashed_payload, x_acs_date, uuid);
    // 3. リクエスト URL を作成します。
    char url[BUFFER_SIZE];
    if (signed_query_params && strlen(signed_query_params) > 0) {
        snprintf(url, sizeof(url), "https://%s%s?%s", host, canonical_uri, signed_query_params);
    } else {
        snprintf(url, sizeof(url), "https://%s%s", host, canonical_uri);
    }
    printf("Request URL: %s\n", url);
    // クエリ文字列に割り当てられたメモリを解放します。
    if (signed_query_params) {
        free(signed_query_params);
    }

    // 4. リクエストヘッダーを追加します。
    struct curl_slist *headers = NULL;
    char header_value[BUFFER_SIZE];
    snprintf(header_value, sizeof(header_value), "Content-Type: %s", content_type);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "Authorization: %s", authorization_header);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "host: %s", host);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-action: %s", x_acs_action);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-content-sha256: %s", hashed_payload);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-date: %s", x_acs_date);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-signature-nonce: %s", uuid);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-version: %s", x_acs_version);
    headers = curl_slist_append(headers, header_value);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method);
    curl_easy_setopt(curl, CURLOPT_URL, url);
    // その他の cURL 設定:SSL 検証を無効にし、デバッグ情報を追加します。
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    // 5. リクエストボディを追加します。
    if (body) {
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_length); 
        if (strcmp(content_type, "application/octet-stream") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        } else if (strcmp(content_type, "application/x-www-form-urlencoded") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        } else if (strcmp(content_type, "application/json; charset=utf-8") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        }
    }
    printf("RequestBody: %s\n",body);
    // 6. リクエストを送信します。
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        goto cleanup;
    }
cleanup:
    if (headers) curl_slist_free_all(headers);
    if (curl) curl_easy_cleanup(curl);
}
/**
*
     * これは署名の計算例です。main 関数内のサンプルパラメーターは実際の値に置き換える必要があります。
     * <p>
     * リクエストメソッド (methods)、パラメーター名 (name)、パラメーターの型 (type)、パラメーターの位置 (in) については、API メタデータを参照してください。
     * 1. API メタデータでリクエストパラメーターが "in": "query" として指定されている場合は、`query_params` 文字列を使用して渡します。注:RPC API の場合、このタイプのパラメーターは `Content-Type` を `application/x-www-form-urlencoded` に設定してリクエストボディで渡すこともできます。詳細については、"formData" の例をご参照ください。
     * 2. API メタデータでリクエストパラメーターが "in": "body" として指定されている場合は、`application/octet-stream` または `application/json` の MIME タイプでリクエストボディに渡します。RPC API の場合、`application/json` は推奨しません。代わりに、"formData" の例に示すメソッドを使用してください。
     * 3. API メタデータでリクエストパラメーターが "in": "formData" として指定されている場合は、`application/x-www-form-urlencoded` の MIME タイプでリクエストボディに渡します。
*/
int main() {
    // コンソール出力のエンコーディングを UTF-8 に設定します。
    SetConsoleOutputCP(CP_UTF8);
    srand((unsigned int)time(NULL));

    /**
      * RPC API リクエストの例:複雑なクエリパラメーターを含む "in": "query"
    */   
    const char *http_method = "POST";
    const char *canonical_uri = "/";
    const char *host = "tds.cn-shanghai.aliyuncs.com";
    const char *x_acs_action = "AddAssetSelectionCriteria";
    const char *x_acs_version = "2018-12-03";

    // 文字列パラメーター SelectionKey を定義します。
    const char *selection_key = "85a561b7-27d5-47ad-a0ec-XXXXXXXX";
    // TargetOperationList パラメーターを定義します。ターゲットオブジェクトの拡張可能なリストです。
    struct {
        const char *operation;
        const char *target;
    } targetOperation_list[] = {
        {"add", "i-2ze1j7ocdg9XXXXXXXX"},
        // エントリを追加できます。
        // {"add", "i-abc123xyzXXXXX"},
    };

    int count = sizeof(targetOperation_list) / sizeof(targetOperation_list[0]);
    KeyValuePair pairs[100]; // 元のキーと値を格納します。
    int pair_count = 0;

    for (int i = 0; i < count; ++i) {
      char op_key[128], target_key[128];
      snprintf(op_key, sizeof(op_key), "TargetOperationList.%d.Operation", i + 1);
      snprintf(target_key, sizeof(target_key), "TargetOperationList.%d.Target", i + 1);

      strncpy(pairs[pair_count].key, op_key, sizeof(pairs[pair_count].key));
      strncpy(pairs[pair_count].value, targetOperation_list[i].operation, sizeof(pairs[pair_count].value));
      pair_count++;

      strncpy(pairs[pair_count].key, target_key, sizeof(pairs[pair_count].key));
      strncpy(pairs[pair_count].value, targetOperation_list[i].target, sizeof(pairs[pair_count].value));
      pair_count++;
}
    // SelectionKey パラメーターを追加します。
    snprintf(pairs[pair_count].key, sizeof(pairs[pair_count].key), "SelectionKey");
    snprintf(pairs[pair_count].value, sizeof(pairs[pair_count].value), "%s", selection_key);
    pair_count++;

    // ソートとエンコードは get_authorization() 関数内で実行されます。
    qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);

    // 元の (エンコード前の) クエリ文字列を作成します。
    char query_params[BUFFER_SIZE] = {0};
    for (int i = 0; i < pair_count; ++i) {
      if (i == 0) {
        snprintf(query_params, sizeof(query_params), "%s=%s", pairs[i].key, pairs[i].value);
     } else {
        char temp[512];
        snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
        strncat(query_params, temp, sizeof(query_params) - strlen(query_params) - 1);
    }
}
    const char *body = ""; // リクエストボディは空です。
    const char *content_type = "application/json; charset=utf-8";
    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

    /**
      * RPC API リクエストの例:リクエストパラメーター "in": "query"。
    */
    // API リクエストパラメーターを定義します。
    // const char *http_method = "POST";
    // const char *canonical_uri = "/";
    // const char *host = "ecs.cn-hangzhou.aliyuncs.com";
    // const char *x_acs_action = "DescribeInstanceStatus";
    // const char *x_acs_version = "2014-05-26";
    // // InstanceId パラメーターを定義します。オプションの配列です。
    // const char *instance_ids[] = {
    //     "i-bp11ht4hXXXXXXXX",
    //     "i-bp16maz3XXXXXXXX"
    // };
    // // InstanceId 配列を連結します。
    // char InstanceId[BUFFER_SIZE];
    // snprintf(InstanceId, sizeof(InstanceId),
    //          "InstanceId.1=%s&InstanceId.2=%s",
    //         instance_ids[0],
    //         instance_ids[1]);
    // // クエリパラメーターを設定します。RegionId パラメーターは必須です。
    // char query_params[BUFFER_SIZE];
    // snprintf(query_params, sizeof(query_params),
    //          "%s&RegionId=cn-hangzhou", InstanceId);
    // const char *body = "";
    // const char *content_type = "application/json; charset=utf-8";
    // call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * RPC API リクエストの例:リクエストパラメーター "in": "body" (ファイルのアップロードのシナリオ)。
      */
    // ファイルの内容を格納するポインターを宣言します。
    // char *body = NULL;
    // size_t body_length = read_file("<YOUR_FILE_PATH>", &body);
    // if (body_length > 0) {
    //   const char *http_method = "POST";
    //   const char *canonical_uri = "/";
    //   const char *host = "ocr-api.cn-hangzhou.aliyuncs.com";
    //   const char *x_acs_action = "RecognizeGeneral";
    //   const char *x_acs_version = "2021-07-07";
    //   const char *query_params = "";
    //   const char *content_type = "application/octet-stream";
    //   call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, body_length);
    //   free(body);
    // } else {
    //   fprintf(stderr, "File read error\n");
    // }

      /**
       * RPC API リクエストの例:リクエストパラメーター "in": "formData" または "in": "body" (ファイルのアップロード以外のシナリオ)。
       */
    // const char *http_method = "POST";
    // const char *canonical_uri = "/";
    // const char *host = "mt.aliyuncs.com";
    // const char *x_acs_action = "TranslateGeneral";
    // const char *x_acs_version = "2018-10-12";
    // char query_params[BUFFER_SIZE];
    // snprintf(query_params, sizeof(query_params), "Context=%s", "Morning");
    // const char *format_type = "text";
    // const char *source_language = "zh";
    // const char *target_language = "en";
    // const char *source_text = "Hello";
    // const char *scene = "general";
    // char body[BUFFER_SIZE];
    // snprintf(body, sizeof(body),
    // "FormatType=%s&SourceLanguage=%s&TargetLanguage=%s&SourceText=%s&Scene=%s",
    // percentEncode(format_type), percentEncode(source_language), percentEncode(target_language),
    // percentEncode(source_text), percentEncode(scene));
    // const char *content_type = "application/x-www-form-urlencoded";
    // printf("formdata_body: %s\n", body);
    // call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

   /**
    * RPC API リクエストの例:リクエストパラメーター "in": "formData"
    */
//    const char *http_method = "POST";
//    const char *canonical_uri = "/";
//    const char *host = "sasti.aliyuncs.com";
//    const char *x_acs_action = "AskTextToTextMsg";
//    const char *x_acs_version = "2020-05-12";
//    // クエリ
//    const char *query_params = "";
//    // ボディ
//    const char *Memory = "false";
//    const char *Stream = "true";
//    const char *ProductCode = "sddp_pre";
//    const char *Feature = "{}";
//    const char *Model = "yunsec-llm-latest";
//    const char *Type = "Chat";
//    const char *TopP = "0.9";
//    const char *Temperature = "0.01";
//    const char *Prompt = "Who are you";
//    const char *Application = "sddp_pre";
//    char body[BUFFER_SIZE];
//    snprintf(body, sizeof(body),
//            "Memory=%s&Stream=%s&ProductCode=%s&Feature=%s&Model=%s&Type=%s&TopP=%s&Temperature=%s&Prompt=%s&Application=%s",
//            Memory, Stream, ProductCode, Feature, Model, Type, TopP, Temperature, Prompt, Application);
//    const char *content_type = "application/x-www-form-urlencoded";
//    printf("formdata_body: %s\n", body);
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * ROA API POST リクエスト:パラメーター "in": "body"。
      */
//    const char *http_method = "POST";
//    const char *canonical_uri = "/clusters";
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "CreateCluster";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "";
//    char body[BUFFER_SIZE];
//    snprintf(body, sizeof(body),
//             "{\"name\":\"%s\",\"region_id\":\"%s\",\"cluster_type\":\"%s\","
//             "\"vpcid\":\"%s\",\"container_cidr\":\"%s\","
//             "\"service_cidr\":\"%s\",\"security_group_id\":\"%s\","
//             "\"vswitch_ids\":[\"%s\"]}",
//             "Test Cluster", "cn-beijing", "ExternalKubernetes",
//             "vpc-2zeou1uod4yXXXXXXXX", "10.X.X.X/XX",
//             "10.X.X.X/XX", "sg-2ze1a0rlgeXXXXXXXX",
//             "vsw-2zei30dhflXXXXXXXX");
//    const char *content_type = "application/json; charset=utf-8";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * ROA API GET リクエスト。
      */
//    const char *http_method = "GET";
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s/resources", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "DescribeClusterResources";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "with_addon_resources=true";
//    const char *body = "";
//    const char *content_type = "";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        *  ROA API DELETE リクエスト。
      */
//    const char *http_method = "DELETE";
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "DeleteCluster";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "";
//    const char *body = "";
//    const char *content_type = "";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

    // 生成した値を格納する変数。
    char authorization_header[BUFFER_SIZE];
    char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
    char x_acs_date[64];
    char uuid[37];
    return 0;
}

よくある質問

「Specified signature does not match our calculation」または「The request signature does not conform to Aliyun standards」エラーの対処法

通常、署名検証は署名計算プロセスのエラーが原因で失敗します。

一般的な原因:

  • AccessKey の設定エラー。

  • パラメーターの位置が正しくない場合。例えば、クエリパラメーターをリクエストボディで渡している場合などです。

  • 正規化されたクエリ文字列 (CanonicalQueryString) のパラメーターがアルファベット順にソートされていないこと。

  • 署名計算に使用されるヘッダー (署名付きヘッダー) がアルファベット順にソートされていないこと。

  • スペース文字は %20 にエンコードされません。

  • パラメーターが複数回 URL エンコードされました。署名計算プロセスでは、URL エンコードはパスパラメーターと正規化されたクエリ文字列に 1 回だけ適用する必要があります。たとえば、エラーメッセージに複数の %25 文字が表示される場合、それは % 文字自体が誤ってエンコードされたことを示します。

解決策:

説明

固定パラメーター値を使用して、ローカルで署名計算結果を検証してください。

  1. ご自身の正規リクエストの値と期待される結果を比較してください。異なる場合は、「ステップ 1:正規化されたリクエストの構築」を見直し、不一致箇所を見つけてください。

  2. 正規リクエストが一致する場合は、署名対象の文字列の値を比較してください。不一致はハッシュアルゴリズムのエラーを示します。

  3. 正規リクエストと署名対象の文字列の両方が一致する場合、問題は AccessKey Secret が正しくないか、暗号化アルゴリズムの実装に誤りがある可能性が高いです。

  4. 問題が解決しない場合は、テクニカルサポートにお問い合わせください

Postman を使用したデバッグ方法

Postman で API 呼び出しをデバッグするには、次の手順に従います。

  1. コードまたはスクリプトを使用して署名を計算し、Authorization ヘッダー値を生成します。

  2. CanonicalHeaders からすべてのリクエストヘッダー (先ほど生成した Authorization ヘッダーを含む) を Postman の Headers タブにコピーします。 例:

    キー

    値の例

    host

    dysmsapi.aliyuncs.com

    x-acs-action

    SendSms

    x-acs-content-sha256

    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

    x-acs-date

    2025-04-16T07:45:55Z

    x-acs-signature-nonce

    315484d3-b129-4966-974a-699b7ee56647

    x-acs-version

    2017-05-25

    Authorization

    ACS3-HMAC-SHA256 Credential=testAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=b37aac99faa507472778256374366b7a47ba48adbc484a53ad789db194658a2d

  3. API パラメーターをそのタイプに応じて Postman に追加します。

    説明

    パラメーターの順序は、署名を計算したときに使用した順序と同一である必要があります。

    • クエリパラメーターの場合は、[Params] タブに追加します。

    • ボディパラメーターの場合は、[Body] タブに追加します。

リクエストパラメーターの渡し方

OpenAPI メタデータでは、in フィールドが各パラメーターの場所を指定し、それによってリクエストでパラメーターがどのように渡されるかが決まります。

位置

説明

content-type

"in": "query"

クエリパラメーターは、 URL のクエスチョンマーク (?) の後に表示されます。各 キーと値のペア はアンパサンド (&) で区切られます。

任意。指定する場合、値は application/json にする必要があります。

"in": "formData"

フォームパラメーターはリクエストボディで送信され、key1=value1&key2=value2&key3=value3 という形式の文字列に連結する必要があります。注: arrayobject のような複雑な型の場合、value をインデックス付きキー/値ペアに「フラット化」する必要があります。たとえば、object のような {"key":["value1","value2"]}{"key.1":"value1","key.2":"value2"} に変換する必要があります。

必須。値は application/x-www-form-urlencoded である必要があります。

"in": "body"

ボディパラメーターはリクエストボディで直接渡されます。

必須。Content-Type の値は、送信されるコンテンツのタイプによって異なります。例:

  • リクエスト内容が JSON 文字列の場合、Content-Type ヘッダーの値を application/json に設定します。

  • リクエストコンテンツがバイナリファイルストリームの場合、Content-Type ヘッダーの値を application/octet-stream に設定します。

API メタデータによると、style の値は RPC または ROA です。

RPC または ROA は、CanonicalURI の値にのみ影響します。 style パラメーターが RPC または ROA 以外のタイプに設定されている場合、CanonicalURI の値は API メタデータ内の path パラメーターによって決定されます。 path パラメーターに値がある場合、CanonicalURIpath の値に設定されます。 path パラメーターに値がない場合、CanonicalURI はスラッシュ (/) に設定されます。 たとえば、ACK クラスターをクエリするための API メタデータでは、path パラメーターの値は /api/v1/clusters であり、CanonicalURI の値も /api/v1/clusters です。

{
  "code": 0,
  "data": {
    "summary": "Queries the details about Container Service for Kubernetes (A...",
    "path": "/api/v1/clusters",
    "methods": [
      "get"
    ],
    "schemes": [
      "http",
      "https"
    ],
    "security": [
      {
        "AK": []
      }
    ]
  }
}

配列またはオブジェクトパラメーターの渡し方

配列やオブジェクトのような複雑なデータ型を渡す場合は、インデックス付きのキーと値のペアに変換する必要があります。

例 1: {"InstanceId":["i-bp10igfmnyttXXXXXXXX","i-bp1incuofvzxXXXXXXXX","i-bp1incuofvzxXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX"]} は、次のように変換されます。

{
    "InstanceId.1": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.10": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.11": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.12": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.2": "i-bp1incuofvzxXXXXXXXX",
    "InstanceId.3": "i-bp1incuofvzxXXXXXXXX",
    "InstanceId.4": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.5": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.6": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.7": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.8": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.9": "i-bp10igfmnyttXXXXXXXX"
}

例 2: {"ImageId":"win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd","RegionId":"cn-shanghai","Tag":[{"tag1":"value1","tag2":"value2"}]} は、次のように変換する必要があります。

{
    "ImageId":"win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd",
    "RegionId":"cn-shanghai",
    "Tag.1.tag1":"value1",
    "Tag.1.tag2":"value2"
}

x-acs-version の値を取得する方法

  1. Alibaba Cloud OpenAPI 開発者ポータルに移動し、呼び出したい API のクラウド製品を選択します。この例では ECS を使用します。ポータルの上部メニューで [Select Cloud Product] をクリックして製品のホームページに移動し、推奨される API バージョンを確認します。

  2. 製品のホームページで推奨される API バージョンを確認します。例えば、ECS の推奨 API バージョンは 2014-05-26 です。

    ページの下部には、[Service Area List] と [API Overview] の 2 つのタブがあります。API バージョンのドロップダウンリストを使用して特定のバージョンに切り替え、その詳細を表示できます。

API 呼び出しで GET メソッドと POST メソッドを切り替えることはできますか?

  • ほとんどの場合、POST または GET メソッドを使用して RPC スタイルの API オペレーションを呼び出すことができます。

  • ただし、ROA スタイルの API オペレーションを呼び出すには、指定されたメソッドのみを使用できます。

サポートされているメソッドは API メタデータで確認してください。

「You are not authorized to do this operation.」エラーの対処法

原因:RAM ユーザーの AccessKey ペアに必要な権限が不足しているためです。

解決策:「code 403. You are not authorized to do this operation. Action: xxxx」エラーメッセージが返された場合の対処方法」。

AccessKey ペアを取得する方法

AccessKey ペアは、AccessKey ID と AccessKey Secret から成る永続的なアクセス認証情報です。Alibaba Cloud は AccessKey ペアを使用して API 呼び出し元を認証します。RAM ユーザーの AccessKey を作成する

お問い合わせ

署名に関するご質問は、DingTalk グループ (ID : 147535001692) にご参加ください。

説明

その他のご質問については、このグループへの参加はご遠慮ください。