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

Alibaba Cloud SDK:リクエスト構文と署名方式 V3

最終更新日:Aug 29, 2026

署名方式 V3 は、Alibaba Cloud API Gateway が Alibaba Cloud API オペレーションへの HTTP または HTTPS リクエストを認証するために使用するメカニズムです。SDK を使用して Alibaba Cloud API オペレーションを呼び出したくない場合、またはご利用の実行環境が SDK をサポートしていない場合は、ご自身でリクエストに署名する必要があります。

注意事項

  • V2 からの移行 — 現在、署名方式 V2 を使用して API オペレーションを呼び出している場合、直接署名方式 V3 に切り替えることができます。

  • SDK のカバー率 — OpenAPI Explorer は Alibaba Cloud サービス向けの SDK を提供しており、これらのサービスの API オペレーションは署名方式 V3 をサポートしています。

  • 自己管理ゲートウェイを持つサービス — 一部の Alibaba Cloud サービスは自己管理ゲートウェイを使用しており、その認証メカニズムはこのトピックで説明されているメカニズムとは異なります。これらのサービスに HTTP リクエストを送信する前に、対応するサービスの署名方式に関するトピックをお読みください。

  • Simple Log Service (SLS) — SLS の署名方式の詳細については、「リクエスト署名」をご参照ください。

  • Object Storage Service (OSS) — OSS の署名方式の詳細については、「署名方式」をご参照ください。

HTTP リクエスト構文

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

名前必須説明例
プロトコルはい各 Alibaba Cloud サービスの API リファレンスで構成の詳細を確認できます。リクエストは HTTP または HTTPS プロトコルで送信できます。セキュリティを強化するため、HTTPS でリクエストを送信することを推奨します。有効な値: https:// および http://。https://
エンドポイントはいサービスのエンドポイント。各 Alibaba Cloud サービスのエンドポイントドキュメントを参照して、さまざまなリージョンのエンドポイントを確認できます。ecs.cn-shanghai.aliyuncs.com
resource_URI_parametersはいリクエスト URI。API パスと、パスまたはリクエストのクエリ文字列に含まれるリクエストパラメーターを含みます。ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
RequestHeaderはい共通リクエストヘッダー。ほとんどの場合、API バージョン、Host、Authorization などの情報が含まれます。詳細については、次のセクションをご参照ください。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
RequestBodyはいボディで定義されるビジネスリクエストパラメーター。「OpenAPI メタデータ」から取得できます。
HTTPMethodはいリクエストメソッド。「OpenAPI メタデータ」から取得できます。POST

RequestHeader

Alibaba Cloud API オペレーションを呼び出す際、共通リクエストヘッダーには以下の情報を含める必要があります。

名前型必須説明例
hostStringはいサービスエンドポイント。詳細については、「」をご参照ください。ecs.cn-shanghai.aliyuncs.com
x-acs-actionStringはいAPI オペレーションの名前。Alibaba Cloud OpenAPI Developer Portal にアクセスし、呼び出したい API オペレーションを検索できます。RunInstances
x-acs-content-sha256Stringはいリクエストボディをハッシュ化し、そのハッシュ値を Base16 でエンコードした結果。この値は HashedRequestPayload の値と同じです。e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-dateStringはいISO 8601 標準の UTC 時刻。フォーマットは yyyy-MM-ddTHH:mm:ssZ です (例:2018-01-01T12:00:00Z)。この値は、リクエスト送信前の 15 分以内の時刻である必要があります。2023-10-26T10:22:32Z
x-acs-signature-nonceStringはい署名ノンス。ノンスはネットワークリプレイ攻撃を防ぎます。リクエストごとに異なるノンスを使用する必要があります。このメカニズムは HTTP プロトコルにのみ適用されます。3156853299f313e23d1673dc12e1703d
x-acs-versionStringはいAPI のバージョン番号。バージョン番号の取得方法の詳細については、「API バージョン (x-acs-version) の取得方法」をご参照ください。2014-05-26
AuthorizationString非匿名リクエストの場合は必須リクエストの有効性を検証するために使用される認証情報。フォーマットは `Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature` です。SignatureAlgorithm は署名暗号化アルゴリズムで、`ACS3-HMAC-SHA256` である必要があります。Credential はユーザーの AccessKey ID です。Resource Access Management (RAM) コンソールで AccessKey ID を確認できます。AccessKey ペアを作成するには、「AccessKey ペアの作成」をご参照ください。SignedHeaders は署名計算に使用されるリクエストヘッダー名のリストです。注:セキュリティを向上させるため、Authorization を除くすべての共通リクエストヘッダーに基づいて署名を計算することを推奨します。Signature はリクエストの署名です。値の詳細については、「署名方式」セクションをご参照ください。ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
x-acs-security-tokenStringSTS 認証の場合は必須AssumeRole オペレーションの応答に含まれる SecurityToken の値。

署名を計算する前に

署名方式 V3 は、各リクエストに AccessKey ペア (AccessKey ID と AccessKey Secret) を使用して署名します。以下の制約は、署名対象と送信内容を決定します。署名コードを記述する前に、すべての制約を確認してください。

  • 文字セット — リクエストと応答は UTF-8 文字セットを使用してエンコードされます。

  • タイムウィンドウ — x-acs-date の値は、リクエスト送信前の 15 分以内の時刻である必要があります。

  • ノンスの一意性 — リクエストごとに異なる x-acs-signature-nonce 値を使用してください。ノンスはネットワークリプレイ攻撃を防ぎます。このメカニズムは HTTP プロトコルにのみ適用されます。

  • 認証情報 — 長期的な認証情報の場合、ご利用の AccessKey ペアでリクエストに署名します。Security Token Service (STS) によって発行された一時的な認証情報の場合、x-acs-security-token ヘッダーも追加します。このヘッダーの値は、AssumeRole オペレーションの応答に含まれる SecurityToken の値です。

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

RPC スタイルと ROA スタイルの API

API メタデータの style フィールドは、リクエストの CanonicalURI と、API がサポートするリクエストメソッドを決定します。

  • CanonicalURI — RPC スタイルの API の場合、CanonicalURI はスラッシュ (/) です。ROA スタイルの API の場合、CanonicalURI は API メタデータの path の値です (例:/api/v1/clusters)。

  • リクエストメソッド — RPC スタイルの API は通常、GET と POST の両方のリクエストをサポートします。ROA スタイルの API は 1 つのリクエストメソッドのみをサポートします。API がサポートするリクエストメソッドの取得方法の詳細については、「OpenAPI メタデータ」をご参照ください。

  • style のその他の値 — RPC と ROA は CanonicalURI の値にのみ影響します。style が RPC または ROA 以外の値に設定されている場合、API メタデータのサマリー情報で path パラメーターに値があるかどうかを確認してください。値が存在する場合は、CanonicalURI を path の値に設定します。値が存在しない場合は、CanonicalURI をスラッシュ (/) に設定します。

    以下の Container Service for Kubernetes (ACK) の ACK クラスターリストをクエリするオペレーションの API メタデータでは、path の値は /api/v1/clusters です。この場合、CanonicalURI の値は /api/v1/clusters です。
{
  "code": 0,
  "data": {
    "summary": "Container Service for Kubernetes (A... の詳細をクエリします",
    "path": "/api/v1/clusters",
    "methods": [
      "get"
    ],
    "schemes": [
      "http",
      "https"
    ],
    "security": [
      {
        "AK": []
      }
    ]
  }
}

パラメーターの位置

API メタデータでは、in フィールドで各パラメーターの位置を定義します。この位置によって、パラメーターの渡し方と、リクエストに必要な content-type が決まります。

パラメーターの位置説明content-type
"in": "query"クエリパラメーター。リクエスト URL の末尾にある疑問符 (?) の後に表示されます。異なる name=value ペアはアンパサンド (&) で区切られます。任意。指定する場合は、値を application/json に設定します。
"in": "formData"フォームパラメーター。key1=value1&key2=value2&key3=value3 形式の文字列に連結し、リクエストボディで渡す必要があります。さらに、リクエストパラメーターが array または object 型の場合、value をインデックス付きのキーと値のペアに変換します。例えば、object 値 {"key":["value1","value2"]} は {"key.1":"value1","key.2":"value2"} に変換する必要があります。必須。値を content-type=application/x-www-form-urlencoded に設定します。
"in": "body"ボディパラメーター。リクエストボディで渡されます。必須。content-type の値はリクエストのコンテンツタイプによって異なります。リクエストコンテンツが JSON データの場合は、content-type を application/json に設定します。リクエストコンテンツがバイナリファイルストリームなどのバイナリ形式の場合は、content-type を `application/octet-stream` に設定します。

配列およびオブジェクトパラメーター

リクエストパラメーターが複雑なデータ構造である場合、パラメーター値をインデックス付きのキーと値のペアに変換します。

例 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"
}

署名方式

リクエストは AccessKey ペア (AccessKey ID と AccessKey Secret) を使用して署名および認証されます。HTTP または HTTPS リクエストごとに、Alibaba Cloud API Gateway はリクエストパラメーターに基づいて署名を再計算し、計算された署名とリクエストに含まれる署名を比較します。これにより、リクエスターの ID が検証され、送信されるデータの整合性とセキュリティが確保されます。

以下の 4 つのステップで、Authorization リクエストヘッダーの値を生成します。

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

以下の擬似コードは、正規化リクエスト (CanonicalRequest) の構築方法を示しています:

CanonicalRequest =
  HTTPRequestMethod + '\n' +    // HTTP リクエストメソッド (大文字)
  CanonicalURI + '\n' +         // 正規化 URI
  CanonicalQueryString + '\n' + // 正規化クエリ文字列
  CanonicalHeaders + '\n' +     // 正規化ヘッダー
  SignedHeaders + '\n' +        // 署名対象ヘッダー
  HashedRequestPayload      // リクエストボディのハッシュ値

HTTPRequestMethod (リクエストメソッド):GET や POST などの HTTP メソッド名 (大文字)。

CanonicalURI (正規化 URI):URL のエンコードされたリソースパス。リソースパスは、URL のホストとクエリ文字列の間の部分です。ホストに続くスラッシュ (/) を含みますが、クエリ文字列の前の疑問符 (?) は含みません。リクエストを送信する際は、正規化 URI を使用します。URI の各部分 (スラッシュ (/) で区切られた各文字列) を、RFC3986 のルールに基づいて UTF-8 文字セットでエンコードします:

  • 文字 A~Z、a~z、0~9、および文字 -、_、.、~ はエンコードされません。

    その他の文字は、パーセント記号 (%) の後にその文字の 16 進数 ASCII コードを続けた形式でエンコードされます。例えば、二重引用符 (") は %22 とエンコードされます。以下のシーケンスは特別な処理が必要です:
シーケンス必須の結果
スペース ( )%20
アスタリスク (*)%2A
%7Eチルダ (~)

Java 標準ライブラリの java.net.URLEncoder を使用する場合、まず標準ライブラリの encode を呼び出して文字列をエンコードします。次に、エンコードされた文字列内のプラス記号 (+) を %20 に、アスタリスク (*) を %2A に、%7E をチルダ (~) に置き換えます。その結果が、前述のルールに準拠したエンコード済み文字列になります。

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

ROA スタイルの API の場合、このパラメーターは OpenAPI メタデータの path の値です (例:/api/v1/clusters)。

CanonicalQueryString (正規化クエリ文字列):「OpenAPI メタデータ」で、API のリクエストパラメーターに "in":"query" が含まれる場合、これらのリクエストパラメーターを以下のルールに基づいて連結します:

  • リクエストパラメーターをパラメーター名の昇順でソートします。

  • 各パラメーターの名前と値を、RFC3986 のルールに基づいて UTF-8 文字セットで個別にエンコードします。ルールは、前述の CanonicalURI エンコーディングルールと同じです。

  • 等号 (=) を使用して、エンコードされたパラメーター名とパラメーター値を接続します。パラメーターに値がない場合は、空の文字列をパラメーターの値として使用します。

  • アンパサンド (&) を使用して、複数のリクエストパラメーターを接続します。

  • リクエストパラメーターが配列またはオブジェクトタイプの場合、パラメーターの値をインデックス付きのキーと値のペアに変換します。

  • リクエストパラメーターが JSON 文字列の場合、JSON 文字列内のパラメーターの順序は署名計算に影響しません。

  • クエリ文字列が存在しない場合は、空の文字列を正規化クエリ文字列として使用します。

    例:
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))
  • 「OpenAPI メタデータ」で、API のリクエストパラメーターに "in": "body" または "in": "formData" が含まれる場合、パラメーターをリクエストボディで渡します:

    • リクエストボディで渡されるリクエストパラメーターがない場合は、リクエストボディを空の文字列に設定します。

    • リクエストパラメーターに "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 はメッセージダイジェスト関数を示します。SHA256 アルゴリズムのみがサポートされています。

  • HexEncode は、ダイジェストを小文字の 16 進数表記 (つまり Base16 エンコーディング) で返すエンコーディング関数を示します。

    リクエストボディが空の場合の HashedRequestPayload の値の例:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

CanonicalHeaders (正規化ヘッダー) は、以下のルールに基づいて共通リクエストヘッダーを連結したものです。共通リクエストヘッダーのリストについては、をご参照ください。

  • x-acs- をプレフィックスとするリクエストヘッダーと、host および content-type ヘッダーを選択します。

  • ヘッダー名を小文字に変換し、ヘッダーを昇順でソートします。

  • ヘッダー値の先頭と末尾のスペースを削除します。

  • 各ヘッダー名とヘッダー値をコロン (:) で接続し、改行 (\n) を追加して正規化ヘッダーエントリ (CanonicalHeaderEntry) を形成します。

  • 複数の正規化ヘッダーエントリ (CanonicalHeaderEntry) を 1 つの文字列に連結します。

    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 (署名対象ヘッダーリスト):現在のリクエストで署名計算に使用される共通リクエストヘッダーを指定します。名前は CanonicalHeaders のパラメーター名と 1 対 1 で対応します。以下のルールに基づいてリストを構築します:

  • CanonicalHeaders 内のリクエストヘッダーの名前を小文字に変換します。

  • 名前をアルファベットの昇順でソートし、セミコロン (;) で区切ります。

    擬似コードは次のとおりです:
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) を構築します:

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

    署名プロトコルでは ACS3-HMAC-SHA256 アルゴリズムのみがサポートされています。
  • HashedCanonicalRequest

    正規化リクエストのダイジェスト文字列。以下の擬似コードは、ダイジェスト文字列の計算方法を示しています:
HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
  • Hash はメッセージダイジェスト関数を示します。SHA256 アルゴリズムのみがサポートされています。

  • HexEncode は、ダイジェストを小文字の 16 進数表記 (つまり Base16 エンコーディング) で返すエンコーディング関数を示します。

    例:
ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259

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

以下の擬似コードに基づいて署名 (Signature) を計算します。

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

  • SignatureMethod:署名アルゴリズムとして HMAC-SHA256 が使用されます。

  • Secret:AccessKey Secret。

  • HexEncode:ダイジェストを小文字の 16 進数表記 (つまり 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

署名実装の検証

前述の 4 つのステップを以下の仮定されたパラメーター値に対して実行し、ご自身の出力と各ステップの期待される出力を比較します。実際のリクエストを送信する前に差異を特定してください。いずれかのステップで不一致があると、ゲートウェイが拒否する署名が生成されます。

必須パラメーター仮定値
AccessKeyIDYourAccessKeyId
AccessKeySecretYourAccessKeySecret
x-acs-signature-nonce3156853299f313e23d1673dc12e1703d
x-acs-date2023-10-26T10:22:32Z
x-acs-actionRunInstances
x-acs-version2014-05-26
hostecs.cn-shanghai.aliyuncs.com

以下の API リクエストパラメーターが使用されます:

パラメーター値
ImageIdwin2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd
RegionIdcn-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

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

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259

ステップ 3: 署名文字列を計算する

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

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

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

先行するリクエストの user-agent ヘッダーと accept ヘッダーは SignedHeaders には記載されていません。これらは署名計算には関与しません。

出力がステップの期待される出力と異なる場合は、続行する前にそのステップを修正してください。リクエストで署名エラーが引き続き発生する場合は、このトピックの『よくある質問』セクションで原因とソリューションを確認し、相違点を特定してください。

署名例

以下のサンプルは、主要なプログラミング言語における前述の署名方式の完全な実装を示しています。サンプルコードは、署名方式を理解するのに役立つ目的でのみ提供されており、普遍的に適用できるものではありません。各タブには、サンプルが検証されたランタイムバージョンと、インストールが必要な依存関係が記載されています。

Alibaba Cloud は、複数のプログラミング言語および開発フレームワーク向けの SDK も提供しています。これらの SDK は署名プロセスを自動で処理し、Alibaba Cloud 上で迅速にアプリケーションを構築するのに役立ちます。(推奨) ご自身でリクエストに署名する代わりに、SDK を使用してください。

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 {
                // 複雑なクエリパラメーターを処理します。
                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 スタイルと RPC スタイルの API は、canonicalUri 値の取得方法のみが異なります。残りのプロセスは同じです。
     * <p>
     * API メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、およびリクエストパラメーターの位置 (in) を取得し、パラメーターを SignatureRequest にカプセル化します。
     * 1. メタデータでリクエストパラメーターが "in":"query" とマークされている場合、それを queryParam で渡します。注:RPC スタイルの API の場合、このタイプのパラメーターはボディでも渡すことができ、content-type は application/x-www-form-urlencoded に設定されます。例 3 をご参照ください。
     * 2. メタデータでリクエストパラメーターが "in": "body" とマークされている場合、それをボディで渡し、MIME タイプは application/octet-stream または application/json に設定します。注:RPC スタイルの API の場合、application/json は推奨されません。代わりに例 3 を使用してください。
     * 3. メタデータでリクエストパラメーターが "in": "formData" とマークされている場合、それをボディで渡し、MIME タイプは application/x-www-form-urlencoded に設定します。
     */
    public static void main(String[] args) throws IOException {
        // 環境変数から AccessKey ペアを取得します。
        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("Set the ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables.");
            return;
        }

        // RPC スタイルの API の例 1:リクエストパラメーターは "in":"query" です。この例では ECS の DescribeInstanceStatus オペレーションを使用します。
        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"));

        /*// RPC スタイルの API の例 2:リクエストパラメーターは "in":"body" (ファイルアップロードシナリオ) です。この例では OCR の RecognizeGeneral オペレーションを使用します。
        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");*/

        /*// RPC スタイルの API の例 3:リクエストパラメーターは "in": "formData" または "in":"body" (非ファイルアップロードシナリオ) です。この例では Machine Translation の TranslateGeneral オペレーションを使用します。
        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 スタイルの API POST リクエストの例。この例では、Container Service for Kubernetes (ACK) にクラスターを作成します。
        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 スタイルの API GET リクエスト。この例では、Container Service for Kubernetes のクラスター情報をクエリします。
        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 スタイルの API 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("Unsupported HTTP method: " + 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()

        # ステップ 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)

        # ステップ 2:署名文字列を構築します。
        hashed_canonical_request = sha256_hex(canonical_request.encode("utf-8"))
        string_to_sign = f"{ALGORITHM}\n{hashed_canonical_request}"
        print(string_to_sign)

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

        # ステップ 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", "~")

# 環境変数から AccessKey ID と AccessKey Secret を取得します。
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"

"""
これは署名の例です。テストする際は、main 関数でシナリオを選択し、サンプル値を変更してください。例えば、SendSms を呼び出すには、例 1 を選択し、http_method、host、x_acs_action、x_acs_version、および query_param を変更します。
ROA スタイルと RPC スタイルの API は、canonicalUri 値の取得方法のみが異なります。

OpenAPI メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、およびリクエストパラメーターの位置 (in) を取得し、パラメーターを SignatureRequest にカプセル化します。
1. メタデータでリクエストパラメーターが "in":"query" とマークされている場合、それを queryParam で渡します。content-type を設定する必要はありません。注:RPC スタイルの API の場合、このタイプのパラメーターはボディでも渡すことができ、content-type は application/x-www-form-urlencoded に設定されます。例 3 をご参照ください。
2. メタデータでリクエストパラメーターが "in": "body" とマークされている場合、それをボディで渡し、シナリオに基づいて content-type を設定します。注:RPC スタイルの API の場合、application/json は推奨されません。代わりに例 3 を使用してください。
3. メタデータでリクエストパラメーターが "in": "formData" とマークされている場合、それをボディで渡し、content-type は application/x-www-form-urlencoded に設定します。
"""
if __name__ == "__main__":
    # RPC スタイルの API リクエストの例 1:リクエストパラメーターは "in":"query" です。
    http_method = "POST"  # リクエストメソッド。メタデータから取得できます。POST を推奨します。
    canonical_uri = "/"  # RPC スタイルの API にはリソースパスがないため、スラッシュ (/) を CanonicalURI として使用します。
    host = "ecs.cn-hangzhou.aliyuncs.com"  # Alibaba Cloud サービスのサービスエンドポイント。
    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 のリクエストパラメーターは次のとおりです:
    # RegionId はメタデータで String として表示され、"in":"query" であり、必須です。
    signature_request.query_param['RegionId'] = 'cn-hangzhou'
    # InstanceId はメタデータで配列として表示され、"in":"query" であり、任意です。
    signature_request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX",
                                                   "i-bp1incuofvzxXXXXXXXX"]

    # # RPC スタイルの API リクエストの例 2:リクエストパラメーターは "in":"body" (ファイルアップロードシナリオ) です。
    # 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)
    # # リクエストパラメーターはメタデータで "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"

    # # RPC スタイルの API リクエストの例 3:リクエストパラメーターは "in": "formData" または "in":"body" (非ファイルアップロードシナリオ) です。
    # 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 のリクエストパラメーターは次のとおりです:
    # # Context はメタデータで String として表示され、"in":"query" であり、任意です。
    # signature_request.query_param['Context'] = 'Morning'
    # # FormatType、SourceLanguage、TargetLanguage などのパラメーターはメタデータで "in":"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)
    # リクエストパラメーターはメタデータで "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"
    # # canonicalUri にパスパラメーターが含まれている場合、percent_code({path parameter}) を使用してパスパラメーターをエンコードします。
    # 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"
    # # canonicalUri にパスパラメーターが含まれている場合、percent_code({path parameter}) を使用してパスパラメーターをエンコードします。
    # 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.com/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 スタイルと RPC スタイルの API は、canonicalUri 値の取得方法のみが異なります。残りのプロセスは同じです。
// API メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、およびリクエストパラメーターの位置 (in) を取得し、パラメーターを SignatureRequest にカプセル化します。
// 1. メタデータでリクエストパラメーターが "in":"query" とマークされている場合、それを queryParam で渡します。注:RPC スタイルの API の場合、このタイプのパラメーターはボディでも渡すことができ、content-type は application/x-www-form-urlencoded に設定されます。例 3 をご参照ください。
// 2. メタデータでリクエストパラメーターが "in": "body" とマークされている場合、それをボディで渡し、MIME タイプは application/octet-stream または application/json に設定します。RPC スタイルの API の場合、application/json は推奨されません。代わりに例 3 を使用してください。
// 3. メタデータでリクエストパラメーターが "in": "formData" とマークされている場合、それをボディで渡し、MIME タイプは application/x-www-form-urlencoded に設定します。
func main() {
    // RPC スタイルの API リクエストの例 1:リクエストパラメーターは "in":"query" です。
    httpMethod := "POST"                   // リクエストメソッド。ほとんどの RPC スタイルの API は POST と GET の両方をサポートします。この例では POST を使用します。
    canonicalUri := "/"                    // RPC スタイルの API にはリソースパスがないため、スラッシュ (/) を CanonicalURI として使用します。
    host := "ecs.cn-hangzhou.aliyuncs.com" // Alibaba Cloud サービスのサービスエンドポイント。
    xAcsAction := "DescribeInstanceStatus" // API 名。
    xAcsVersion := "2014-05-26"            // API バージョン。
    req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
    // DescribeInstanceStatus のリクエストパラメーターは次のとおりです:
    // RegionId はメタデータで String として表示され、"in":"query" であり、必須です。
    req.queryParam["RegionId"] = "cn-hangzhou"
    // InstanceId はメタデータで配列として表示され、"in":"query" であり、任意です。
    instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
    req.queryParam["InstanceId"] = instanceIds

    // // RPC スタイルの API リクエストの例 2:リクエストパラメーターは "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"

    // // RPC スタイルの API リクエストの例 3:リクエストパラメーターは "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 はメタデータで String として表示され、"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({path parameter}) を使用してパスパラメーターをエンコードします。
    // 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({path parameter}) を使用してパスパラメーターをエンコードします。
    // 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) {
    // queryParam 内の 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 はオブジェクトを再帰的に処理し、Map や List などの複雑なオブジェクトをキーと値のペアにフラット化します。
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 and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables must be set.');
    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 リクエストなど)、RequestPayload は常に空の文字列です。
        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:Authorization ヘッダーを構築します。
        const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
        console.log('authorization=========>', authorization);
        signRequest.headers['Authorization'] = authorization;
    } catch (error) {
        console.error('Failed to get authorization');
        console.error(error);
    }
}

async function callApi(signRequest) {
    try {
        let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
        // リクエストパラメーターを追加します。
        if (signRequest.queryParam) {
            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) {
    // 値が空の場合、それ以上の処理は不要です。
    if (value === null) {
        return;
    }
    if (key === null) {
        key = "";
    }

    // 値が Array 型の場合、配列の各要素を反復処理し、再帰的に処理します。
    if (Array.isArray(value)) {
        value.forEach((item, index) => {
            processObject(map, `${key}.${index + 1}`, item);
        });
    } else if (typeof value === 'object' && value !== null) {
        // 値が Object 型の場合、オブジェクトの各キーと値のペアを反復処理し、再帰的に処理します。
        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 スタイルと RPC スタイルの API は、canonicalUri 値の取得方法のみが異なります。残りのプロセスは同じです。
 *
 * API メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、およびリクエストパラメーターの位置 (in) を取得し、パラメーターを SignatureRequest にカプセル化します。
 * 1. メタデータでリクエストパラメーターが "in":"query" とマークされている場合、それを queryParam で渡します。注:RPC スタイルの API の場合、このタイプのパラメーターはボディでも渡すことができ、content-type は application/x-www-form-urlencoded に設定されます。例 3 をご参照ください。
 * 2. メタデータでリクエストパラメーターが "in": "body" とマークされている場合、それをボディで渡し、MIME タイプは application/octet-stream または application/json に設定します。RPC スタイルの API の場合、application/json は推奨されません。代わりに例 3 を使用してください。
 * 3. メタデータでリクエストパラメーターが "in": "formData" とマークされている場合、それをボディで渡し、MIME タイプは application/x-www-form-urlencoded に設定します。
 */

// RPC スタイルの API リクエストの例 1:リクエストパラメーターは "in":"query" です。
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, xAcsVersion);
// DescribeInstanceStatus のリクエストパラメーターは次のとおりです:
signRequest.queryParam = {
    // RegionId はメタデータで String として表示され、"in":"query" であり、必須です。
    RegionId: 'cn-hangzhou',
    // InstanceId はメタデータで配列として表示され、"in":"query" であり、任意です。
    InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}

// // RPC スタイルの API リクエストの例 2:リクエストパラメーターは "in":"body" (ファイルアップロードシナリオ) です。
// 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, xAcsVersion);
// const filePath = 'D:\\test.png';
// const bytes = fs.readFileSync(filePath);
// // メタデータで "in": "body" とマークされたリクエストパラメーターはボディで渡されます。
// signRequest.body = bytes;
// signRequest.headers['content-type'] = 'application/octet-stream';

// // RPC スタイルの API リクエストの例 3:リクエストパラメーターは "in": "formData" または "in":"body" (非ファイルアップロードシナリオ) です。
// 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, xAcsVersion);
// // TranslateGeneral のリクエストパラメーターは次のとおりです:
// // Context はメタデータで String として表示され、"in":"query" であり、任意です。
// signRequest.queryParam["Context"] = "Morning";
// // FormatType、SourceLanguage、TargetLanguage などのパラメーターはメタデータで "in":"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, 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({path parameter}) を使用してパスパラメーターをエンコードします。
// 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, xAcsVersion);
// signRequest.queryParam = {
//     with_addon_resources: true,
// }

// // ROA スタイルの API DELETE リクエスト。
// const httpMethod = 'DELETE';
// // canonicalUri にパスパラメーターが含まれている場合、percentCode({path parameter}) を使用してパスパラメーターをエンコードします。
// 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, 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;
    // Access Key ID
    private $AccessKeyId;
    // Access Key Secret
    private $AccessKeySecret;

    private $SecurityToken;

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

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

        // // RPC スタイルの API リクエストの例 2:リクエストパラメーターは "in":"body" (ファイルアップロードシナリオ) です。
        // $request = $this->createRequest('POST', '/', 'ocr-api.cn-hangzhou.aliyuncs.com', 'RecognizeGeneral', '2021-07-07');
        // // リクエストパラメーターはメタデータで "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);

        // // RPC スタイルの API リクエストの例 3:リクエストパラメーターは "in": "formData" または "in":"body" (非ファイルアップロードシナリオ) です。
        // $request = $this->createRequest('POST', '/', 'mt.aliyuncs.com', 'TranslateGeneral', '2018-10-12');
        // // TranslateGeneral のリクエストパラメーターは次のとおりです:
        // $request['queryParam'] = [
        //     // Context はメタデータで String として表示され、"in":"query" であり、任意です。
        //     '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/resources", 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['body'];
                    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)
    {
        // 値が空の場合、それ以上の処理は不要です。
        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("The ALIBABA_CLOUD_ACCESS_KEY_ID environment variable is not set.");
        private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ?? throw new InvalidOperationException("The ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variable is not set.");
        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 スタイルと RPC スタイルの API は、canonicalUri 値の取得方法のみが異なります。残りのプロセスは同じです。
        *
        * API メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、およびリクエストパラメーターの位置 (in) を取得し、パラメーターを SignatureRequest にカプセル化します。
        * 1. メタデータでリクエストパラメーターが "in":"query" とマークされている場合、それを queryParam で渡します。注:RPC スタイルの API の場合、このタイプのパラメーターはボディでも渡すことができ、content-type は application/x-www-form-urlencoded に設定されます。例 3 をご参照ください。
        * 2. メタデータでリクエストパラメーターが "in": "body" とマークされている場合、それをボディで渡し、MIME タイプは application/octet-stream または application/json に設定します。RPC スタイルの API の場合、application/json は推奨されません。代わりに例 3 を使用してください。
        * 3. メタデータでリクエストパラメーターが "in": "formData" とマークされている場合、それをボディで渡し、MIME タイプは application/x-www-form-urlencoded に設定します。
        */
        public static void Main(string[] args)
        {
            // RPC スタイルの API リクエストの例 1:リクエストパラメーターは "in":"query" です。
            string httpMethod = "POST"; // リクエストメソッド。ほとんどの RPC スタイルの API は POST と GET の両方をサポートします。この例では POST を使用します。
            string canonicalUri = "/"; // RPC スタイルの API にはリソースパスがないため、スラッシュ (/) を CanonicalURI として使用します。
            string host = "ecs.cn-hangzhou.aliyuncs.com"; // Alibaba Cloud サービスのサービスエンドポイント。
            string xAcsAction = "DescribeInstanceStatus"; // API 名。
            string xAcsVersion = "2014-05-26"; // API バージョン。
            var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // DescribeInstanceStatus のリクエストパラメーターは次のとおりです:
            // RegionId はメタデータで String として表示され、"in":"query" であり、必須です。
            request.QueryParam["RegionId"] = "cn-hangzhou";
            // InstanceId はメタデータで配列として表示され、"in":"query" であり、任意です。
            List<string> instanceIds = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"];
            request.QueryParam["InstanceId"] = instanceIds;

            // // RPC スタイルの API リクエストの例 2:リクエストパラメーターは "in":"body" (ファイルアップロードシナリオ) です。
            // 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);
            // // リクエストパラメーターはメタデータで "in": "body" とマークされています。それをボディで渡します。
            // request.Body = File.ReadAllBytes(@"D:\test.png");
            // request.Headers["content-type"] = "application/octet-stream";

            // // RPC スタイルの API リクエストの例 3:リクエストパラメーターは "in": "formData" または "in":"body" (非ファイルアップロードシナリオ) です。
            // 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 として表示され、"in":"query" であり、任意です。
            // request.QueryParam["Context"] = "Morning";
            // // FormatType、SourceLanguage、TargetLanguage などのパラメーターはメタデータで "in":"formData" とマークされています。
            // 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) // main で定義された値と一致する必要があります。
                    {
                        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("Invalid URI syntax");
                Console.WriteLine(e.Message);
                return null;
            }
            catch (Exception e)
            {
                Console.WriteLine("Failed to send request");
                Console.WriteLine(e);
                return null;
            }
        }

        private static void GetAuthorization(Request request)
        {
            try
            {
                // queryParam 内の List または Map 型の値を持つクエリパラメーターをフラット化します。
                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("Failed to get authorization");
                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("The input string cannot be null or empty.");
            }
            return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
        }
    }
}

Rust

サンプルコードは rustc 1.82.0 で実行されます。実際の環境に応じてコードを調整する必要がある場合があります。

Rust の例を実行するには、Cargo.toml ファイルに次の依存関係を追加します。

[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;

// x-acs-date を生成します。
pub fn current_timestamp() -> Result<u64, SystemTimeError> {
    Ok(SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs())
}
// URL エンコーディングを実行します。
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) // String または &str を保持できる Cow<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
}

/// SHA256 ハッシュを計算します。
pub fn sha256_hex(message: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(message);
    format!("{:x}", hasher.finalize()).to_lowercase()
}
// HMAC SHA256
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())
}
// 署名ノンスを生成します。
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)
}
// 正規化クエリパラメーター (エンコード済み) を構築します。
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("&")
}
// 応答を読み取ります。
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))
}
// FormData の値の型を定義します。
#[derive(Debug, Clone)]
pub enum FormValue {
    String(String),
    Vec(Vec<String>),
    HashMap(HashMap<String, String>),
}
// Json、Binary、FormData のボディタイプを統一的に処理するためのリクエストボディの enum を定義します。
pub enum RequestBody {
    Json(HashMap<String, Value>), // Json
    Binary(Vec<u8>), // Binary
    FormData(HashMap<String, FormValue>), //  FormData
    None,
}
// リクエストを正規化します。
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> {

    // ボディタイプに基づいてリクエストボディを処理し、結果を body_content 変数に格納します。
    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(),
    };

    // リクエストボディの x-acs-content-sha256 を計算します。x-acs-date、x-acs-signature-nonce、および署名対象ヘッダーを準備します。
    let hashed_request_payload = if body_content.is_empty() {
        sha256_hex("")
    } else {
        sha256_hex(&body_content)
    };
    // x-acs-date
    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
    let signature_nonce = generate_nonce();
    println!("Signature Nonce: {}", signature_nonce);
    // 署名対象ヘッダー
    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. 正規化ヘッダーを構築します。
    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. 署名対象ヘッダーを構築します。
    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. 署名対象ヘッダーの SHA-256 ハッシュを計算します。
    let result = sha256_hex(&canonical_request);
    // 4. 署名文字列を構築します。
    let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", result);
    // 5. 署名を計算します。
    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. Authorization ヘッダーを構築します。
    headers.insert("Authorization", HeaderValue::from_str(&auth_data).unwrap());
    // URL を構築し、リクエストパラメーターを追加します。
    let url: String;
    if !query_params.is_empty() {
        url = format!("https://{}{}?{}", host, canonical_uri,canonical_query_string);
    } else {
        url = format!("https://{}{}", host, canonical_uri);
    }
    // リクエストを送信します。
    let response = send_request(
        &client,
        method,
        &url,
        headers,
        query_params,
        &body,
        &body_content,
    )
    .await?;

    // 応答を読み取ります。
    let (_, res) = read_response(response).await?;
    Ok(res)
}

/// リクエストを送信します。
async fn send_request(
    client: &Client,
    method: Method,
    url: &str,
    headers: HeaderMap,
    query_params: &[(&str, &str)],     // クエリパラメーターを受け取ります。
    body: &RequestBody,                // ボディデータの型を決定します。
    body_content: &str,                // ボディが空でない場合にボディパラメーターを受け取ります:FormData、Json、または Binary。
) -> Result<Response, String> {
    let mut request_builder = client.request(method.clone(), url);
    // リクエストヘッダーを追加します。
    for (k, v) in headers.iter() {
        request_builder = request_builder.header(k, v.clone());
    }
     // リクエストボディを追加します。
     match body {
        RequestBody::Binary(_) => {
            request_builder = request_builder.header("Content-Type", "application/octet-stream");
            request_builder = request_builder.body(body_content.to_string()); // ここに値を移動します。
        }
        RequestBody::Json(_) => {
            // ボディがマップで空でない場合、それを JSON に変換し、body_content 変数に格納し、application/json; charset=utf-8 を設定します。
            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(_) => {
            // form-data 型を処理し、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());
        }
    }
    // リクエストを構築します。
    let request = request_builder
        .build()
        .map_err(|e| format!("build request fail: {}", e))?;
    // リクエストを送信します。
    let response = client
        .execute(request)
        .await
        .map_err(|e| format!("execute request fail: {}", e))?;
    // 結果を返します。
    Ok(response)
}

 /**
     *
     * これは署名の例です。main メソッドのサンプルパラメーターを実際の値に置き換えてください。
     * <p>
     * API メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、およびリクエストパラメーターの位置 (in) を取得します。
     * 1. メタデータでリクエストパラメーターが "in":"query" とマークされている場合、それを query_params で渡します。注:RPC スタイルの API の場合、このタイプのパラメーターはボディでも渡すことができ、content-type は application/x-www-form-urlencoded に設定されます。例 3 をご参照ください。
     * 2. メタデータでリクエストパラメーターが "in": "body" とマークされている場合、それをボディで渡し、MIME タイプは application/octet-stream または application/json に設定します。RPC スタイルの API の場合、application/json は推奨されません。代わりに例 3 を使用してください。
     * 2. メタデータでリクエストパラメーターが "in": "formData" とマークされている場合、それをボディで渡し、MIME タイプは application/x-www-form-urlencoded に設定します。
*/
#[tokio::main]
async fn main() {
    // HTTP クライアントを作成します。
    let client = Client::new();
    // env::var() は環境変数から AccessKey ID と AccessKey Secret を取得します。
    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 リクエストの例 1:リクエストパラメーターは "in":"query" です。   POST
    let method = Method::POST; // リクエストメソッド
    let host = "ecs.cn-hangzhou.aliyuncs.com"; // エンドポイント
    let canonical_uri = "/"; // RPC スタイルの API にはリソースパスがないため、スラッシュ (/) を CanonicalURI として使用します。
    let action = "DescribeInstanceStatus"; // API 名
    let version = "2014-05-26"; // API バージョン
    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));
    }
    // クエリパラメーター
    let query_params: &[(&str, &str)] = &query;
    // リクエストボディが空の場合
    let body = RequestBody:: None;

    // "in":"query" を持つ RPC スタイル API で、クエリパラメーターが複雑な型の場合。  POST
    // let method = Method::POST; // リクエストメソッド
    // let host = "tds.cn-shanghai.aliyuncs.com"; // エンドポイント
    // let canonical_uri = "/"; // RPC スタイルの API にはリソースパスがないため、スラッシュ (/) を CanonicalURI として使用します。
    // let action = "AddAssetSelectionCriteria"; // API 名
    // let version = "2018-12-03"; // API バージョン
    // パラメーターを定義します。
    // let mut target_op = HashMap::new();
    // target_op.insert("Operation", "add");
    // target_op.insert("Target", "i-2ze1j7ocdXXXXXXXX");
    // TargetOperationList パラメーターを定義します。そのコレクションにはマップ型のアイテムが含まれます。
    // let target_operation_list = vec![target_op];
    // パラメーターをフラット化します。
    // let mut query = flatten_target_ops(target_operation_list, "TargetOperationList");
    // 通常のパラメーター
    // query.push(("SelectionKey", "85a561b7-27d5-47ad-a0ec-XXXXXXXX"));
    // let query_params: &[(&str, &str)] = &query;
    // let body = RequestBody:: None;

    // RPC スタイルの API リクエストの例 2:リクエストパラメーターは "in":"body" (ファイルアップロードシナリオ) です。  POST
    // let method = Method::POST; // リクエストメソッド
    // let host = "ocr-api.cn-hangzhou.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "RecognizeGeneral";
    // let version = "2021-07-07";
    // リクエストパラメーターはバイナリファイル型の "in":"body" です。
    // let binary_data = std::fs::read("<FILE_PATH>").expect("Failed to read the file."); // <FILE_PATH> を実際のファイルパスに置き換えてください。
    // ボディがバイナリ型の場合
    // let body = RequestBody::Binary(binary_data);
    // クエリパラメーターは空です。
    // let query_params = &[];

    // RPC スタイルの API リクエストの例 3:リクエストパラメーターは "in": "formData" または "in":"body" (非ファイルアップロードシナリオ) です。  POST
    // let method = Method::POST; // リクエストメソッド
    // let host = "mt.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "TranslateGeneral";
    // let version = "2018-10-12";
    // // FormatType、SourceLanguage、TargetLanguage などのパラメーターはメタデータで "in":"formData" とマークされています。
    // let mut form_data = HashMap::new();  // ボディタイプは FormData(HashMap<String, FormValue>) です。FormValue は Vec<String>、HashSet<String>、HashMap<String, String> などをサポートします。FormValue enum にさらに型を追加できます。
    // 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")];
    // ボディが FormData 型の場合、"in":"formdata"
    // let body = RequestBody::FormData(form_data);

    // ROA スタイルの API POST リクエスト。API:CreateCluster、クラスターを作成します。
    // API リクエスト定数を定義します。
    // let method = Method::POST; // リクエストメソッド
    // let host = "cs.cn-hangzhou.aliyuncs.com";
    // let canonical_uri = "/clusters";
    // let action = "CreateCluster";
    // let version = "2015-12-15";
    // リクエストボディパラメーターを設定します。
    // let mut body_json = HashMap::new();  //  ボディタイプは Json(HashMap<String, Value>) です。Value は次の型をサポートします:Value::String("test".to_string()) // String  Value::Number(serde_json::Number::from(42)) // Number  Value::Bool(true) // Boolean  Value::Null // Null  Value::Array(vec![Value::from(1), Value::from(2), Value::from(3)]) //Array json!({"nested_key": "nested_value"})
    // 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"),
    //     ]),
    // );
    // クエリパラメーターは空です。
    // let query_params = &[];
    // ボディが Json 型の場合
    // let body = RequestBody::Json(body_json);

    // ROA スタイルの API GET リクエスト。API:DescribeClusterResources、指定されたクラスターに関連付けられたリソースをクエリします。
    // let method = Method::GET; // リクエストメソッド
    // let host = "cs.cn-hangzhou.aliyuncs.com"; // エンドポイント
    // // リソースパスを連結します。
    // let uri = format!("/clusters/{}/resources", percent_code("ce196d21571a64be9XXXXXXXX").as_ref());
    // let canonical_uri = uri.as_str(); // リソースパスを &str 型に変換します。
    // let action = "DescribeClusterResources";   // API 名
    // let version = "2015-12-15"; // API バージョン
    // // クエリパラメーターを設定します。
    // let query_params = &[("with_addon_resources", if true { "true" } else { "false" })];  // "true" または "false"
    // // ボディパラメーターを空に設定します。
    // let body = RequestBody:: None;

    // ROA スタイルの API DELETE リクエスト。API:DeleteCluster、従量課金クラスターを削除します。
    // 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(); // リソースパスを &str 型に変換します。
    // let action = "DeleteCluster";
    // let version = "2015-12-15";
    // // クエリパラメーター
    // let query_params = &[];
    // // ボディパラメーターが空の場合
    // let body = RequestBody:: None;

    // SendSms API オペレーション
    // let method = Method::POST; // リクエストメソッド
    // let host = "dysmsapi.aliyuncs.com"; // エンドポイント
    // let canonical_uri = "/"; // RPC スタイルの API にはリソースパスがないため、スラッシュ (/) を CanonicalURI として使用します。
    // let action = "SendSms"; // API 名
    // let version = "2017-05-25"; // API バージョン
    // 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>"));
    // // クエリパラメーター
    // let query_params: &[(&str, &str)] = &query;
    // // リクエストボディが空の場合
    // let body = RequestBody:: None;

    // リクエストを送信します。
    match call_api(
        client.clone(),
        method,                                                  // API リクエストメソッド:POST、GET、または DELETE
        host,                                                    // API エンドポイント
        canonical_uri,                                           // API リソースパス
        query_params,                                            // "in":"query" クエリパラメーター
        action,                                                  // API 名
        version,                                                 // API バージョン
        body,                                                    // "in":"body" リクエストボディパラメーター。Json、FormData、および Binary 型がサポートされています。
        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 型のパラメーター:ボディ値は JSON 文字列です (例:"{'key1':'value1','key2':'value2'}")。署名対象ヘッダーに content-type:application/json; charset=utf-8 を追加する必要があります。
# body 型のパラメーターがバイナリファイルの場合:ボディを変更する必要はありません。署名対象ヘッダーに content-type:application/octet-stream を追加し、curl_command に --data-binary パラメーターを追加します。
# formdata 型のパラメーター:ボディパラメーターの形式は "key1=value1&key2=value2" で、署名対象ヘッダーに content-type:application/x-www-form-urlencoded を追加する必要があります。
body=""

# ISO 8601 標準の UTC 時刻
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=""
    localpos 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:正規化リクエストを構築します。
# queryParam のすべてのパラメーターをフラット化します。
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'"

# リクエストヘッダーを追加します。
IFS=$'\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>

// getenv() は環境変数から AccessKey ID と AccessKey Secret を取得します。
#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;
}
// リクエストを送信します。
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. リクエストパラメーターを追加します。
    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>
     * API メタデータからリクエストメソッド (methods)、リクエストパラメーター名 (name)、リクエストパラメーターの型 (type)、およびリクエストパラメーターの位置 (in) を取得します。
     * 1. メタデータでリクエストパラメーターが "in":"query" とマークされている場合、それを query_params で渡します。注:RPC スタイルの API の場合、このタイプのパラメーターはボディでも渡すことができ、content-type は application/x-www-form-urlencoded に設定されます。例 3 をご参照ください。
     * 2. メタデータでリクエストパラメーターが "in": "body" とマークされている場合、それをボディで渡し、MIME タイプは application/octet-stream または application/json に設定します。RPC スタイルの API の場合、application/json は推奨されません。代わりに例 3 を使用してください。
     * 2. メタデータでリクエストパラメーターが "in": "formData" とマークされている場合、それをボディで渡し、MIME タイプは application/x-www-form-urlencoded に設定します。
*/
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 配列パラメーターを定義します。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=cn-hangzhou    const char *query_params = "RegionId=cn-hangzhou";
    // 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("formdate_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 リクエストの例 3:リクエストパラメーターは "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("formdate_body: %s\n", body);
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * "in" "body" を持つ ROA スタイルの API POST リクエスト。
      */
//    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;
}

よくある質問

署名が失敗し、「指定された署名が計算結果と一致しません。」または「リクエスト署名が Aliyun の標準に準拠していません。」というエラーメッセージが返される

原因:

ほとんどの署名失敗は、CanonicalRequest を構築する際に発生します。一般的な原因は次のとおりです:

  • AccessKey ID または AccessKey Secret が正しく構成されていない、または AccessKey ペアが無効化または削除されている。

  • パラメーターが間違った位置で渡されている。例えば、クエリパラメーターがボディで渡されている。

  • CanonicalQueryString のパラメーターが昇順でソートされていない。

  • CanonicalHeaders 内のリクエストヘッダーが小文字のアルファベット順にソートされていません。V3 署名では、リクエストヘッダー名を厳密にアルファベット順にソートする必要があります。たとえば、content-type は host より前になり、host は x-acs-action より前になります。

  • HashedRequestPayload が正しく計算されていない。リクエストボディが空の JSON オブジェクト {} である POST リクエストの場合、SHA-256 ハッシュ値は 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a です。空の文字列のハッシュ値を使用しないでください。

  • スペースが %20 としてエンコードされていない。

  • URL エンコーディングが余分に 1 回実行されている。署名計算中、URL エンコーディングはパスパラメーターと正規化クエリ文字列を処理する際に 1 回だけ必要です。例えば、エラーメッセージに多数の %25 文字が含まれている場合、% 文字がエンコードされたことを示します。

  • HMAC-SHA256 署名に使用されるキーが正しくない。署名キーは生の AccessKey Secret です。Base64 エンコードやその他のエンコーディングを適用しないでください。

    ソリューション:

ローカルの計算結果がに記載されている期待される結果と一致するかどうかを確認した後、以下の項目を確認してください:

  • AccessKey ID と AccessKey Secret が正しく、AccessKey ペアが有効になっていることを確認してください。AccessKey ペアのステータスは、Resource Access Management (RAM) コンソールの AccessKey 管理ページで確認できます。

  • エラーメッセージ内の CanonicalRequest が、ローカルで計算した CanonicalRequest と同じであるか確認します。サーバーから返されるエラーメッセージには、期待される StringToSign と CanonicalRequest が含まれています。ローカルの計算結果とそれらを 1 行ずつ比較することで、差異を特定できます。それらが異なる場合は、前述の一般的な原因との説明を確認し、その後コードを注意深く確認してください。

  • CanonicalRequest が同じ場合、エラーメッセージの StringToSign が、ローカルで計算した StringToSign と異なるかどうかを確認してください。StringToSign の形式は、ACS3-HMAC-SHA256 の後に改行と CanonicalRequest の SHA-256 ハッシュ値が続きます。異なる場合、ハッシュアルゴリズムが間違っている可能性があります。

  • StringToSign が同じ場合、次の 2 つの原因のいずれかが考えられます:AccessKey Secret が正しくない、または HMAC-SHA256 計算で使用されるキー形式が正しくない。キーは生の AccessKey Secret である必要があります。

  • 問題が解決しない場合は、をご参照ください。

Postman を使用してテストする方法は?

Postman を使用して Alibaba Cloud API オペレーションを直接呼び出すことはできません。Postman で API オペレーションをテストするには、次の手順を実行します:

  • コードまたはスクリプトを使用して、署名方式に基づいて Authorization を計算します。

  • CanonicalHeaders のリクエストヘッダーを Postman の [Headers] にコピーし、Authorization 情報を [Headers] に追加します。例:

キー値の例
hostdysmsapi.aliyuncs.com
x-acs-actionSendSms
x-acs-content-sha256e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date2025-04-16T07:45:55Z
x-acs-signature-nonce315484d3-b129-4966-974a-699b7ee56647
x-acs-version2017-05-25
AuthorizationACS3-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
  • パラメーターの型に基づいて Postman でパラメーターを設定します。パラメーターの順序は、署名を計算したときに使用した順序と同じでなければなりません。

    • クエリパラメーターの場合は、[Params] にパラメーターを入力します。

    • ボディパラメーターの場合は、[Body] にパラメーターを入力します。

API バージョン (x-acs-version) の取得方法は?

  • Alibaba Cloud OpenAPI Developer Portal にアクセスし、呼び出したい API オペレーションの Alibaba Cloud サービスを選択します。この例では ECS を使用します。OpenAPI Explorer の上部ナビゲーションバーで、[Select a product] をクリックし、サービスのホームページに移動して推奨 API バージョンを確認します。

  • Alibaba Cloud サービスのホームページで推奨 API バージョンを確認します。例えば、ECS の推奨 API バージョンは 2014-05-26 です。

    ページの最下部には [Service regions] と [API overview] タブがあります。API バージョンのドロップダウンリストを使用して、対応するバージョンに切り替えて詳細を表示できます。

API オペレーションを呼び出すと「この操作を実行する権限がありません。」というエラーメッセージが返される

原因:使用している AccessKey ペアが属する RAM ユーザーに、API オペレーションを呼び出す権限がありません。

ソリューション:「コード 403、この操作を実行する権限がありません。アクション:xxxx。」をご参照ください。

AccessKey ペアの取得方法は?

AccessKey ペアは、Alibaba Cloud がユーザーに提供する永続的なアクセス認証情報です。AccessKey ID と AccessKey Secret で構成されます。API オペレーションを呼び出して Alibaba Cloud リソースにアクセスする際、システムはリクエストに含まれる AccessKey ID と、AccessKey Secret を使用して生成された署名に基づいて本人確認とリクエストの検証を行います。AccessKey ペアの取得方法の詳細については、「RAM ユーザーの AccessKey ペアの作成」をご参照ください。

お問い合わせ

署名計算中に解決できない問題が発生した場合は、DingTalk グループ 147535001692 に参加し、担当エンジニアにお問い合わせください。

署名計算に関係のない質問については、このグループに参加しても有効な回答が得られない場合があります。