

# 创建已签名的 AWS API 请求
<a name="reference_sigv-create-signed-request"></a>

**重要**  
如果您使用 AWS SDK（请参阅[示例代码和库](https://aws.amazon.com/developer/)）或 AWS Command Line Interface（AWS CLI）工具向 AWS 发送 API 请求，则可以跳过本部分，因为 SDK 和 CLI 客户端会使用您提供的访问密钥来验证您的请求。除非您有充分的理由不这样做，否则我们建议您始终使用 SDK 或 CLI。  
在支持多个签名版本的区域中，手动签名请求意味着您必须指定使用的签名版本。当您向多区域访问点提供请求时，SDK 和 CLI 会自动切换为使用签名版本 4A，而无需进行其他配置。

您可以使用 AWS SigV4 签名协议为 AWS API 请求创建签名的请求。

1. 根据请求详细信息创建规范请求。

1. 使用 AWS 凭证计算签名。

1. 将此签名作为授权标头添加到请求。

然后 AWS 复制此过程并验证签名，相应地授予或拒绝访问权限。

要了解如何使用 AWS SigV4 对 API 请求进行签名，请参阅 [请求签名示例](reference_sigv-examples.md)。

下表介绍了创建签名请求过程中使用的函数。您需要为这些函数实现代码。如需了解更多信息，请参阅 [AWS 软件开发工具包中的代码示例](reference_sigv.md#reference_aws-signing-resources)。


| 函数 | 说明 | 
| --- | --- | 
|  `Lowercase()`  |  将字符串转换为小写。  | 
|  `Hex()`  |  base-16 编码的小写形式。  | 
|  `SHA256Hash()`  |  安全哈希算法（SHA）加密哈希函数。  | 
|  `HMAC-SHA256()`  |  使用 SHA256 算法和提供的签名密钥计算 HMAC。这是您使用 SigV4 签名的最终签名。  | 
|  `ECDSA-Sign`  |  使用基于公私密钥加密的非对称签名计算椭圆曲线数字签名算法（ECDSA）签名。  | 
|  `KDF(K, Label, Context, L)`  |  处于计数器模式的 NIST SP800-108 KDF 使用 [NIST SP 800-108r1](https://doi.org/10.6028/NIST.SP.800-108r1-upd1) 中定义的 PRF 函数 HMAC-SHA256。  | 
|  `Oct2Int(byte[ ])`  |  ANSI X9.62 中描述的整数函数的八位字节。  | 
|  `Trim()`  |  删除所有前导空格或尾随空格。  | 
|  `UriEncode()`  |  URI 对每个字节进行编码。UriEncode() 必须强制执行以下规则： [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_sigv-create-signed-request.html)  由于底层 RFC 中的实现差异和相关歧义，您开发平台提供的标准 UrienCode 函数可能无法正常工作。建议您编写自己的自定义 UrienCode 函数，以确保编码能够正常工作。  要查看 Java 中的 UriEncode 函数示例，请参阅 GitHub 网站上的 [Java Utilities](https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/util/SdkHttpUtils.java#L66)。  | 

**注意**  
签署请求时，您可以使用 AWS SigV4 或 AWS SigV4a。两者之间的关键区别取决于签名的计算方式。使用 SigV4a 时，区域集包含在待签字符串中，但不是凭证派生步骤的一部分。

## 使用临时安全凭证签名请求
<a name="temporary-security-credentials"></a>

您可使用 AWS Security Token Service（AWS STS）提供的临时安全凭证来签署请求，而不是使用长期凭证。

使用临时安全凭证时，必须将 `X-Amz-Security-Token` 添加至授权标头或将其包含在查询字符串中以保存会话令牌。某些服务会要求您将 `X-Amz-Security-Token` 添加至规范请求。其他服务仅会要求您在计算出签名后在末尾添加 `X-Amz-Security-Token`。有关具体要求，请查看每个 AWS 服务的文档。

## 签名步骤摘要
<a name="create-signed-request-steps"></a>

**创建规范请求**  
将请求的内容（主机、操作、标头等）组织为标准规范格式。规范请求是用于创建待签字符串的输入之一。有关创建规范请求的详细信息，请参阅[AWS API 请求签名的元素](reference_sigv-signing-elements.md)。

**创建规范请求的哈希值**  
使用创建负载的哈希时所使用的相同算法来哈希规范请求。经过哈希处理的规范请求必须以小写十六进制字符串形式表示。

**创建待签字符串**  
使用规范请求和额外信息（例如算法、请求日期、凭证范围和规范请求的哈希）创建待签字符串。

**派生签名密钥**  
使用秘密访问密钥派生用于对请求进行签名的密钥。

**计算签名**  
使用派生的签名密钥作为哈希密钥，对待签字符串执行加密哈希操作。

**将签名添加至请求**  
将计算的签名添加到请求的 HTTP 标头或查询字符串中。

## 创建规范请求
<a name="create-canonical-request"></a>

要创建规范请求，请串联由换行符分隔的以下字符串。这有助于确保您计算出的签名能够与 AWS 计算出的签名相匹配。

```
<HTTPMethod>\n
<CanonicalURI>\n
<CanonicalQueryString>\n
<CanonicalHeaders>\n
<SignedHeaders>\n
<HashedPayload>
```
+ *HTTPMethod* – HTTP 方法，例如 `GET`、`PUT`、`HEAD` 和 `DELETE`。
+ *CanonicalUri*：绝对路径组件 URI 的 URI 编码版本，以域名后面的 `/` 开头，直至字符串结尾处，或者如果包含查询字符串参数，则直至问号字符（`?`）。如果绝对路径为空，则使用正斜杠字符（`/`）。以下示例中的 URI `/amzn-s3-demo-bucket/myphoto.jpg` 是绝对路径，并且您无需在绝对路径中对 `/` 进行编码：

  ```
  http://s3.amazonaws.com/amzn-s3-demo-bucket/myphoto.jpg
  ```
+ *CanonicalQueryString* – URI 编码的查询字符串参数。您可以单独对每个名称和值进行 URI 编码。您还必须按键名称的字母顺序对规范查询字符串中的参数进行排序。编码后进行排序。以下 URI 示例中的查询字符串是：

  ```
  http://s3.amazonaws.com/amzn-s3-demo-bucket?prefix=somePrefix&marker=someMarker&max-keys=2
  ```

  规范查询字符串如下所示（为便于阅读，此示例中添加了换行符：）：

  ```
  UriEncode("marker")+"="+UriEncode("someMarker")+"&"+
  UriEncode("max-keys")+"="+UriEncode("20") + "&" +
  UriEncode("prefix")+"="+UriEncode("somePrefix")
  ```

  当请求针对子资源时，相应的查询参数值将为空字符串（`""`）。例如，以下 URI 标识了 `amzn-s3-demo-bucket` 存储桶上的 `ACL` 子资源：

   

  ```
  http://s3.amazonaws.com/amzn-s3-demo-bucket?acl
  ```

  在这种情况下，CanonicalQueryString 将为：

   

  ```
  UriEncode("acl") + "=" + ""
  ```

  如果 URI 不包含 `?`，则请求中没有查询字符串，并且您需要将规范查询字符串设置为空字符串（`""`）。您仍然需要包含换行符 (`"\n"`)。
+ *CanonicalHeaders*：请求标头及其值的列表。各个标头名称和值对用换行符（`"\n"`）分隔。以下是 CanonicalHeader 的示例：

  ```
  Lowercase(<HeaderName1>)+":"+Trim(<value>)+"\n"
  Lowercase(<HeaderName2>)+":"+Trim(<value>)+"\n"
  ...
  Lowercase(<HeaderNameN>)+":"+Trim(<value>)+"\n"
  ```

  CanonicalHeaders 列表必须包含以下内容：
  + HTTP `host` 标头。
  + 如果请求中存在 `Content-Type` 标头，则必须将其添加到 *CanonicalHeaders* 列表中。
  + 此外，还必须添加计划在请求中包含的所有 `x-amz-*` 标头。例如，如果您使用临时安全凭证，则请求中必须包含 `x-amz-security-token`。您必须将此标头添加到 *CanonicalHeaders* 列表中。
  + 对于 SigV4a，您必须包含一个区域集标头，该标头指定请求将在哪一组区域中生效。标头 `X-Amz-Region-Set` 被指定为逗号分隔值的列表。下面的示例显示一个区域标头，该标头允许在 us-east-1 和 us-west-1 区域中进行请求。

    `X-Amz-Region-Set=us-east-1,us-west-1 `

    您可以在区域中使用通配符（\$1）来指定多个区域。在下面的示例中，标头允许在 us-west-1 和 us-west-2 中进行请求。

    `X-Amz-Region-Set=us-west-*`
**注意**  
`x-amz-content-sha256` 标题是 Amazon S3 AWS 请求所必需的。它将提供请求负载的哈希。如果不包含有效负载，则必须提供空字符串的哈希值。

  每个标头名称必须：
  + 使用小写字符。
  + 按字母顺序显示。
  + 后跟冒号（`:`）。

  对于值，您必须：
  + 去除任何前导空格或尾随空格。
  + 将连续空格转换为单个空格。
  + 使用逗号分隔多值标头的值。
  + 签名中必须包含 host 标头（HTTP/1.1）或 :authority 标头（HTTP/2）以及任何 `x-amz-*` 标头。签名中也可以包含其他标准标头，例如 content-type。

  上一部分介绍了本示例中使用的 `Lowercase()` 和 `Trim()` 函数。

  以下是示例 `CanonicalHeaders` 字符串。标头名称为小写且已排序。

   

  ```
  host:s3.amazonaws.com
  x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  x-amz-date:20130708T220855Z
  ```

   
**注意**  
为了计算授权签名，只有主机以及任何 `x-amz-*` 标头为必填项；但是，为了防止数据篡改，您应该考虑在签名计算中包含其他标头。  
请勿包含会在复杂系统传输中频繁更改的逐跳标头。这包括所有由代理、负载均衡器和分布式系统中的节点改变的易失性传输标头，包括 `connection`、`x-amzn-trace-id`、`user-agent`、`keep-alive`、`transfer-encoding`、`TE`、`trailer`、`upgrade`、`proxy-authorization` 和 `proxy-authenticate`。
+ *SignedHeaders*：按字母顺序排序、以分号分隔的小写请求标头名称列表。列表中的请求标头与您在 `CanonicalHeaders` 字符串中包含的标头相同。对于前面的示例，*SignedHeaders* 的值如下：

  ```
  host;x-amz-content-sha256;x-amz-date
  ```
+ *HashedPayload* – 使用 HTTP 请求正文中的负载作为哈希函数的输入创建的字符串。此字符串使用小写十六进制字符。

  ```
  Hex(SHA256Hash(<payload>>))
  ```

  如果请求中不包含有效负载，则计算空字符串的哈希值，例如，当使用 `GET` 请求检索对象时，有效负载中没有任何内容。

  ```
  Hex(SHA256Hash(""))
  ```
**注意**  
对于 Amazon S3，请在构造规范请求时包含文字字符串 `UNSIGNED-PAYLOAD`，并在发送请求时设置与 `x-amz-content-sha256` 标头值相同的值。  
`Hex(SHA256Hash("UNSIGNED-PAYLOAD"))`

## 创建规范请求的哈希值
<a name="create-canonical-request-hash"></a>

使用创建负载的哈希时所使用的相同算法来创建规范请求的哈希（摘要）。经过哈希处理的规范请求必须以小写十六进制字符串形式表示。

## 创建待签字符串
<a name="create-string-to-sign"></a>

要创建待签字符串，请串联以下由换行符分隔的以下字符串。请勿使用换行符作为此字符串的结尾。

```
Algorithm \n
RequestDateTime \n
CredentialScope  \n
HashedCanonicalRequest
```
+ *Algorithm* – 用于创建规范请求的哈希的算法。
  + SigV4 – 使用 `AWS4-HMAC-SHA256` 指定 `HMAC-SHA256` 哈希算法。
  + SigV4a – 使用 `AWS4-ECDSA-P256-SHA256` 指定 `ECDSA-P256-SHA-256` 哈希算法。
+ *RequestDateTime* – 在凭证范围内使用的日期和时间。该值是采用 ISO 8601 格式的当前 UTC 时间（例如 `20130524T000000Z`）。
+ *CredentialScope*：凭证范围，将生成的签名限制在指定的区域和服务范围内。
  + SigV4 – 凭证包括访问密钥 ID、`YYYYMMDD` 格式的日期、区域代码、服务代码和 `aws4_request` 终止字符串，用斜杠（/）分隔。区域代码、服务代码和终止字符串必须使用小写字符。字符串具有以下格式：`YYYYMMDD/region/service/aws4_request`。
  + SigV4a – 凭证包括 `YYYYMMDD` 格式的日期、服务名称和 `aws4_request` 终止字符串，用斜杠（/）分隔。请注意，凭证范围不包括区域，因为该区域包含在单独的标头 `X-Amz-Region-Set` 中。字符串具有以下格式：`YYYYMMDD/service/aws4_request`。
+ *HashedCanonicalRequest*：上一步中计算出的规范请求的哈希。

以下是要签名的字符串的示例。

```
"<Algorithm>" + "\n" +
timeStampISO8601Format + "\n" +
<Scope> + "\n" +
Hex(<Algorithm>(<CanonicalRequest>))
```

## 派生签名密钥
<a name="derive-signing-key"></a>

要派生签名密钥，请选择以下过程之一来计算 SigV4 或 SigV4a 的签名密钥。

### 派生 SigV4 的签名密钥
<a name="derive-signing-key-sigv4"></a>

要派生 SigV4 的签名密钥，请使用 AWS 秘密访问密钥作为初始哈希操作的密钥，对请求日期、区域和服务执行一系列加密哈希操作（HMAC）。

对于每个步骤，使用所需的密匙和数据调用哈希函数。每次调用哈希函数的结果都会变成下一次调用哈希函数的输入。

以下示例说明了如何派生本过程下一部分中使用的 `SigningKey`，并说明了输入的串联和哈希顺序。`HMAC-SHA256` 是用于对数据进行哈希处理的哈希功能，如下所示。

```
DateKey = HMAC-SHA256("AWS4"+"<SecretAccessKey>", "<YYYYMMDD>")
DateRegionKey = HMAC-SHA256(<DateKey>, "<aws-region>")
DateRegionServiceKey = HMAC-SHA256(<DateRegionKey>, "<aws-service>")
SigningKey = HMAC-SHA256(<DateRegionServiceKey>, "aws4_request")
```

**必填项**
+ `Key` – 包含您的秘密访问密钥的字符串。
+ `Date` – 包含在凭证范围中使用的日期的字符串，格式为 *YYYYMMDD*。
+ `Region` – 包含区域代码的字符串（例如，`us-east-1`）。

  有关区域字符串的列表，请参阅 *AWS 一般参考* 中的 [Regional Endpoints](https://docs.aws.amazon.com//general/latest/gr/rande.html#regional-endpoints)。
+ `Service` – 包含服务代码的字符串（例如，`ec2`）。
+ 在上一步中创建的要签名的字符串。

**要派生 SigV4 的签名密钥**

1. 串联 `"AWS4"` 和秘密访问密钥。使用密钥和数据调用哈希函数，并将连接的字符串作为密钥，而日期字符串作为数据。

   ```
   DateKey = hash("AWS4" + Key, Date)
   ```

1. 使用密钥和数据调用哈希函数，并将上一次调用的结果作为密钥，而区域字符串作为数据。

   ```
   DateRegionKey = hash(kDate, Region)
   ```

1. 使用密钥和数据调用哈希函数，并将上一次调用的结果作为密钥，而服务字符串作为数据。

   服务代码由服务定义。您可以在 *AWS Pricing CLI* 中使用 [get-products](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/pricing/get-products.html) 返回服务的服务代码。

   ```
   DateRegionServiceKey = hash(kRegion, Service)
   ```

1. 使用密钥和数据调用哈希函数，并将上一次调用的结果作为密钥，而“aws4\$1request”作为数据。

   ```
   SigningKey = hash(kService, "aws4_request")
   ```

### 派生 SigV4a 的签名密钥
<a name="derive-signing-key-sigv4a"></a>

要为 SigV4a 创建签名密钥，请使用以下过程从秘密访问密钥派生密钥对。有关此派生实现的示例，请参阅 [AWS 客户端身份验证的 C99 库实现](https://github.com/awslabs/aws-c-auth/blob/e8360a65e0f3337d4ac827945e00c3b55a641a5f/source/key_derivation.c#L291.) 

```
n = [NIST P-256 elliptic curve group order]
G = [NIST P-256 elliptic curve base point]
label = "AWS4-ECDSA-P256-SHA256"

akid = [AWS access key ID as a UTF8 string]
sk = [AWS secret access Key as a UTF8 Base64 string]

input_key = "AWS4A" || sk
count = 1
while (counter != 255) {
  context = akid || counter // note: counter is one byte
  key = KDF(input_key, label, context, 256)
  c = Oct2Int(key)
  if (c > n - 2) {
    counter++
  } else {
    k = c + 1   // private key
    Q = k * G   // public key
  }
}

if (c < 255) {
  return [k, Q]
} else {
  return FAILURE
}
```

## 计算签名
<a name="calculate-signature"></a>

派生签名密钥后，计算要添加到请求中的签名。此过程因您使用的签名版本而不同。

**要计算 SigV4 的签名**

1. 将上一次调用的结果作为密钥，**要签名的字符串**作为数据来调用哈希函数。使用派生的签名密钥作为此操作的哈希密钥。结果是作为二进制值的签名。

   ```
   signature = hash(SigningKey, string-to-sign)
   ```

1. 将签名从二进制转换为十六进制表示形式，使用小写字符。

**要计算 SigV4a 的签名**

1. 使用数字签名算法（ECDSA P-256），对您在上一步中创建的**要签名的字符串**进行签名。用于此签名的密钥是从上述秘密访问密钥派生的非对称私有密钥。

   ```
   signature = base16(ECDSA-Sign(k, string-to-sign))
   ```

1. 将签名从二进制转换为十六进制表示形式，使用小写字符。

## 将签名添加至请求
<a name="add-signature-to-request"></a>

将计算出的签名添加到您的请求中。

**Example 示例：授权标头**  
**SigV4**  
以下示例显示了使用 AWS SigV4 的 `DescribeInstances` 操作的 `Authorization` 标头。为便于阅读，此示例已使用换行符编排过格式。在您的代码中，这必须是连续的字符串。算法和 `Credential` 之间没有逗号。但是，必须使用逗号分隔其他元素。

```
Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20220830/us-east-1/ec2/aws4_request,
SignedHeaders=host;x-amz-date,
Signature=calculated-signature
```

**SigV4a**  
以下示例显示了使用 AWS SigV4a 的 `CreateBucket` 操作的授权标头。为便于阅读，此示例已使用换行符编排过格式。在您的代码中，这必须是连续的字符串。算法和凭证之间没有逗号。但是，必须使用逗号分隔其他元素。

```
Authorization: AWS4-ECDSA-P256-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20220830/s3/aws4_request,
SignedHeaders=host;x-amz-date;x-amz-region-set,
Signature=calculated-signature
```

**Example 示例：请求在查询字符串中使用身份验证参数**  
**SigV4**  
以下示例显示了对包含身份验证信息的使用 AWS SigV4 的 `DescribeInstances` 操作的查询。为便于阅读，此示例已使用换行符编排过格式，而非 URL 编码。在您的代码中，查询字符串必须是采用 URL 编码的连续字符串。

```
https://ec2.amazonaws.com/?
Action=DescribeInstances&
Version=2016-11-15&
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20220830/us-east-1/ec2/aws4_request&
X-Amz-Date=20220830T123600Z&
X-Amz-SignedHeaders=host;x-amz-date&
X-Amz-Signature=calculated-signature
```

**SigV4a**  
以下示例显示了对包含身份验证信息的使用 AWS SigV4a 的 `CreateBucket` 操作的查询。为便于阅读，此示例已使用换行符编排过格式，而非 URL 编码。在您的代码中，查询字符串必须是采用 URL 编码的连续字符串。

```
https://ec2.amazonaws.com/?
Action=CreateBucket&
Version=2016-11-15&
X-Amz-Algorithm=AWS4-ECDSA-P256-SHA256&
X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20220830/s3/aws4_request&
X-Amz-Region-Set=us-west-1&
X-Amz-Date=20220830T123600Z&
X-Amz-SignedHeaders=host;x-amz-date;x-amz-region-set&
X-Amz-Signature=calculated-signature
```