签名方法
## 申请安全凭证
本文使用的安全凭证为密钥,密钥包括 AccessKeyId 和 AccessKeySecret。每个用户最多可以拥有两对密钥。
* AccessKeyId:用于标识 API 调用者身份,可以简单类比为用户名。
* AccessKeySecret:用于验证 API 调用者的身份,可以简单类比为密码。
申请安全凭证请联系客服。
## 签名过程
为方便演示,请求地址用`https://httpbin.org/anything`。
假设用户的 AccessKeyId 和 AccessKeySecret 分别是:`Ufhax9qOFwKeQvKQ`和`yD6kvY9dfrS0FZDK6SqhzCpgg4mg5s1v`。
请求可能为:
```
curl -X POST https://httpbin.org/anything \
-H "Authorization: HMAC-SHA256 Credential=Ufhax9qOFwKeQvKQ/20190225/request, SignedHeaders=content-type;host;x-api-time, Signature=e0b2dd53a599d0095be20e2fcc3c58b73497c7626620b6bee5f7702b658e6932" \
-H "Content-Type: application/json; charset=utf-8" \
-H "X-Api-Time: 2019-02-26T00:44:25+08:00" \
-d '{"Limit": 1, "Filters": [{"Values": ["\u672a\u547d\u540d"], "Name": "instance-name"}]}'
```
下面详细解释签名计算过程。
### 1. 创建规范请求串
按如下伪代码格式拼接规范请求串(CanonicalRequest):
```
CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
HashedRequestPayload
```
* **HTTPRequestMethod**
HTTP 请求方法(GET、POST 等)。
此示例取值为 `POST`。
* **CanonicalURI**
URI 参数。HTTP URL中的路径部分,从主机到开始查询字符串参数的问号字符(?)的中间字符串。根据 [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) 标准化URI路径。移除冗余和相对路径部分。
如果绝对路径为空,则使用正斜杠(/)。
例如,原uri
`/documents and settings/`
编码后
`/documents%20and%20settings/`
此示例取值为`/anything`。
* **CanonicalQueryString**
发起 HTTP 请求 URL 中的查询字符串,对于 POST 请求,固定为空字符串"",对于 GET 请求,则为 URL 中问号(?)后面的字符串内容。
1. 按查询字符串key的ASCII 升序进行拼接。
1. 按照 [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) 进行 URLEncode,字符集 UTF8,推荐使用编程语言标准库,所有特殊字符均需编码,大写形式。
例如,原查询字符串
`id=2&action=getUserList&Time=2018-03-12 12:01:04`
编码后
`Time=2018-03-12%2012%3A01%3A04&action=getUserList&id=2`
* **CanonicalHeaders**
参与签名的头部信息,至少包含 host 和 x-api-time 两个头部,也可加入自定义的头部参与签名以提高自身请求的唯一性和安全性。
1. 头部 key 和 value 统一转成小写,并去掉首尾空格,按照 `key:value\n` 格式拼接。
1. 多个头部,按照头部 key(小写)的 ASCII 升序进行拼接。
1. 注意:content-type 必须和实际发送的相符合,有些编程语言网络库即使未指定也会自动添加 charset 值,如果签名时和发送时不一致,服务器会返回签名校验失败。
此示例计算结果是 `content-type:application/json; charset=utf-8\nhost:url\nx-api-time:2019-02-26T00:44:25+08:00\n`。
* **SignedHeaders**
参与签名的头部信息,说明此次请求有哪些头部参与了签名,和 CanonicalHeaders 包含的头部内容是一一对应的。 host 和 x-api-time 为必选头部。
拼接规则:
1. 头部 key 统一转成小写;
1. 多个头部 key(小写)按照 ASCII 升序进行拼接,并且以分号(;)分隔。
此示例为 `content-type;host;x-api-time`。
* **HashedRequestPayload**
请求正文(payload,即 body,此示例为 `{"Limit": 1, "Filters": [{"Values": ["\u672a\u547d\u540d"], "Name": "instance-name"}]}`)的哈希值,计算伪代码为 Lowercase(HexEncode(Hash.SHA256(RequestPayload))),即对 HTTP 请求正文做 SHA256 哈希,然后十六进制编码,最后编码串转换成小写字母。对于 GET 请求,RequestPayload 固定为空字符串。
此示例计算结果是 `35e9c5b0e3ae67532d3c9f17ead6c90222632e5b1ff7f6e89887f1398934f064`。
根据以上规则,示例中得到的规范请求串如下:
```
POST
/anything
content-type:application/json; charset=utf-8
host:httpbin.org
x-api-time:2019-02-26T00:44:25+08:00
content-type;host;x-api-time
35e9c5b0e3ae67532d3c9f17ead6c90222632e5b1ff7f6e89887f1398934f064
```
### 2. 创建待签名字符串
按如下格式拼接待签名字符串:
```
StringToSign =
Algorithm + \n +
RequestDateTime + \n +
CredentialScope + \n +
HashedCanonicalRequest
```
* **Algorithm**
签名算法,目前固定为 `HMAC-SHA256`。
* **RequestDateTime**
请求时间,即请求头部的公共参数 X-Api-Time 取值。格式为ISO8601。
此示例取值 `2019-02-26T00:44:25+08:00`。
* **CredentialScope**
凭证范围,格式为 Date/request,包含日期和终止字符串(`request`)。
Date 为 UTC 标准时间的日期,取值需要和公共参数 X-Api-Time 换算的 UTC 标准时间日期一致。
此示例计算结果是 `20190225/request`。
* **HashedCanonicalRequest**
前述步骤拼接所得规范请求串的哈希值,计算伪代码为 Lowercase(HexEncode(Hash.SHA256(CanonicalRequest)))。
此示例计算结果是 `b2b8b0dec0e30dcc0496ddeba9eb2c1ce94e8ef92039b48df44268aebd188919`。
注意:
> 1. Date 必须从时间戳 X-Api-Time 计算得到,且时区为 UTC+0。如果加入系统本地时区信息,例如东八区,将导致白天和晚上调用成功,但是凌晨时调用必定失败。假设时间戳为 1551113065(ISO8601格式:2019-02-26T00:44:25+08:00),在东八区的时间是 2019-02-26 00:44:25,但是计算得到的 Date 取 UTC+0 的日期应为 20190225,而不是 20190226。
> 2. Time 必须是当前系统时间,且需确保系统时间和标准时间是同步的,如果相差超过五分钟则必定失败。如果长时间不和标准时间同步,可能导致运行一段时间后,请求必定失败,返回签名过期错误。
根据以上规则,示例中得到的待签名字符串如下:
```
HMAC-SHA256
2019-02-26T00:44:25+08:00
20190225/request
b2b8b0dec0e30dcc0496ddeba9eb2c1ce94e8ef92039b48df44268aebd188919
```
### 3. 计算签名
1)计算派生签名密钥,伪代码如下:
```
SigningKeySecret = your secret access key
SigningKeyDate = HMAC_SHA256(SigningKeySecret, Date)
SigningKey = HMAC_SHA256(SigningKeyDate, "request")
```
派生出的密钥 `SigningKeySecret`、`SigningKeyDate` 和 `SigningKey` 是二进制的数据,可能包含不可打印字符,此处不展示中间结果。
请注意,不同的编程语言,HMAC 库函数中参数顺序可能不一样,请以实际情况为准。此处的伪代码密钥参数 key 在前,消息参数 data 在后。通常标准库函数会提供二进制格式的返回值,也可能会提供打印友好的十六进制格式的返回值,此处使用的是二进制格式。
* **signingKeySecret**
原始的 AccessKeySecret,即`yD6kvY9dfrS0FZDK6SqhzCpgg4mg5s1v`。
* **Date**
即 CredentialScope 中的 Date 字段信息。
此示例取值为 `20190225`。
2)计算签名,伪代码如下:
```
Signature = HexEncode(HMAC_SHA256(SigningKey, StringToSign))
```
此示例计算结果是 `e0b2dd53a599d0095be20e2fcc3c58b73497c7626620b6bee5f7702b658e6932`。
### 4. 拼接Authorization
按如下格式拼接 Authorization:
```
Authorization =
Algorithm + ' ' +
'Credential=' + AccessKeyId + '/' + CredentialScope + ', ' +
'SignedHeaders=' + SignedHeaders + ', ' +
'Signature=' + Signature
```
* **Algorithm**
签名方法,固定为`HMAC-SHA256`。
* **AccessKeyId**
密钥对中的AccessKeyId,即`Ufhax9qOFwKeQvKQ`。
* **CredentialScope**
见上文,凭证范围。
此示例计算结果是 `20190225/request`。
* **SignedHeaders**
见上文,参与签名的头部信息。
此示例取值为 `content-type;host;x-api-time`。
* **Signature**
签名值。
此示例计算结果是 `e0b2dd53a599d0095be20e2fcc3c58b73497c7626620b6bee5f7702b658e6932`。
根据以上规则,示例中得到的值为:
```
HMAC-SHA256 Credential=Ufhax9qOFwKeQvKQ/20190225/request, SignedHeaders=content-type;host;x-api-time, Signature=e0b2dd53a599d0095be20e2fcc3c58b73497c7626620b6bee5f7702b658e6932
```
最终完整的调用信息如下:
```
POST https://httpbin.org/anything
Authorization: HMAC-SHA256 Credential=Ufhax9qOFwKeQvKQ/20190225/request, SignedHeaders=content-type;host;x-api-time, Signature=e0b2dd53a599d0095be20e2fcc3c58b73497c7626620b6bee5f7702b658e6932
Content-Type: application/json; charset=utf-8
Host: httpbin.org
X-Api-Time: 2019-02-26T00:44:25+08:00
{"Limit": 1, "Filters": [{"Values": ["\u672a\u547d\u540d"], "Name": "instance-name"}]}
```
## 签名演示
为了更清楚的解释签名过程,下面以实际编程语言为例,将上述的签名过程具体实现。请求的域名、调用的接口和参数的取值都以上述签名过程为准,代码只为解释签名过程,并不具备通用性。
### PHP
安装依赖
`composer require guzzlehttp/guzzle:^7.0`
```
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Psr7\Request;
function buildCanonicalUri(string $uri): string
{
if (empty($uri)) {
return '/';
}
return $uri;
}
function buildCanonicalHeaders(array $headers): string
{
$canonicalHeaders = '';
ksort($headers, SORT_STRING);
foreach ($headers as $key => $header) {
$canonicalHeaders .= $key . ':' . $header . "\n";
}
return $canonicalHeaders;
}
function buildCanonicalQueryString(string $queryString): string
{
$queryParams = [];
parse_str($queryString, $queryParams);
ksort($queryParams, SORT_STRING);
return http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986);
}
function buildSignedHeaders(array $headers): string
{
$signedHeaders = '';
ksort($headers, SORT_STRING);
foreach ($headers as $key => $header) {
$signedHeaders .= $key . ';';
}
return trim($signedHeaders, ';');
}
function hashPayload(string $payload): string
{
return hash('SHA256', $payload);
}
function hashCanonicalRequest(string $canonicalRequest): string
{
return hash('SHA256', $canonicalRequest);
}
ini_set('date.timezone', 'Asia/Shanghai');
//示例参数
$method = 'POST';
$url = 'https://httpbin.org/anything';
$accessKeyId = 'Ufhax9qOFwKeQvKQ';
$accessKeySecret = 'yD6kvY9dfrS0FZDK6SqhzCpgg4mg5s1v';
$contentType = 'application/json; charset=utf-8';
$payload = '{"Limit": 1, "Filters": [{"Values": ["\u672a\u547d\u540d"], "Name": "instance-name"}]}';
$timestamp = 1551113065;
$parsedUrl = (new Request($method, $url))->getUri();
$host = $parsedUrl->getHost();
$queryString = $parsedUrl->getQuery() ?? '';
$uri = $parsedUrl->getPath();
$requestDateTime = date('c', $timestamp);
// step 1: create a canonical request
$toSignHeaders = ['host' => $host, 'x-api-time' => $requestDateTime];
if ($contentType) {
$toSignHeaders = array_merge($toSignHeaders, ['content-type' => $contentType]);
}
$httpRequestMethod = $method;
$canonicalUri = buildCanonicalUri($uri);
$canonicalHeaders = buildCanonicalHeaders($toSignHeaders);
$canonicalQueryString = buildCanonicalQueryString($queryString);
$signedHeaders = buildSignedHeaders($toSignHeaders);
$payloadHash = hashPayload($payload);
$canonicalRequest = $httpRequestMethod . "\n"
. $canonicalUri . "\n"
. $canonicalQueryString . "\n"
. $canonicalHeaders . "\n"
. $signedHeaders . "\n"
. $payloadHash;
// step 2: create the string to sign
$algorithm = 'HMAC-SHA256';
$date = gmdate('Ymd', strtotime($requestDateTime));
$credentialScope = $date . '/request';
$hashedCanonicalRequest = hashCanonicalRequest($canonicalRequest);
$stringToSign = $algorithm . "\n"
. $requestDateTime . "\n"
. $credentialScope . "\n"
. $hashedCanonicalRequest;
// step 3: calculate the signature
$signingKeyDate = hash_hmac('SHA256', $date, $accessKeySecret, true);
$signingKey = hash_hmac('SHA256', 'request', $signingKeyDate, true);
$signature = hash_hmac('SHA256', $stringToSign, $signingKey);
// step 4: create authorization
$authorization = $algorithm
. ' Credential=' . $accessKeyId . '/' . $credentialScope
. ', SignedHeaders=' . $signedHeaders . ', Signature=' . $signature;
// step 5: create request
$headers = [
'Authorization:' . $authorization,
'X-Api-Time:' . $requestDateTime,
];
if ($contentType) {
$headers = array_merge($headers, ['Content-Type:' . $contentType]);
}
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
if ($payload) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
}
$response = curl_exec($curl);
curl_close($curl);
var_dump(json_decode($response, true));
```
### Java
安装依赖
```
JDK:11+
gradle:
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
implementation 'org.apache.commons:commons-lang3:3.12.0'
```
```
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.stream.Collectors;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
public class Signature {
protected static String encodeUrlQuery(String rawQueryString) {
return Arrays.stream(rawQueryString.split("&"))
.map(param -> param.split("=")[0] + "="
+ URLEncoder.encode(param.split("=")[1], StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
protected static String buildCanonicalUri(String uri) {
if (uri.isEmpty()) {
return "/";
}
return uri;
}
protected static String buildCanonicalHeaders(Map<String, String> headers) {
StringBuilder canonicalHeaders = new StringBuilder();
headers.entrySet().stream().sorted(Map.Entry.comparingByKey())
.forEachOrdered(header -> canonicalHeaders.append(header.getKey()).append(":").append(header.getValue()).append("\n"));
return canonicalHeaders.toString();
}
protected static String buildCanonicalQueryString(String queryString) {
List<NameValuePair> queryParams = URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8);
queryParams.sort(Comparator.comparing(NameValuePair::getName));
return URLEncodedUtils.format(queryParams, StandardCharsets.UTF_8)
.replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
}
protected static String buildSignedHeaders(Map<String, String> headers) {
StringBuilder signedHeaders = new StringBuilder();
headers.entrySet().stream().sorted(Map.Entry.comparingByKey())
.forEachOrdered(header -> signedHeaders.append(header.getKey()).append(";"));
return StringUtils.stripEnd(signedHeaders.toString(), ";");
}
protected static String hashPayload(String payload) {
return DigestUtils.sha256Hex(payload);
}
protected static String hashCanonicalRequest(String canonicalRequest) {
return DigestUtils.sha256Hex(canonicalRequest);
}
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException {
TimeZone.setDefault(TimeZone.getTimeZone("PRC"));
//示例参数
String method = "POST";
String url = "https://httpbin.org/anything";
String accessKeyId = "Ufhax9qOFwKeQvKQ";
String accessKeySecret = "yD6kvY9dfrS0FZDK6SqhzCpgg4mg5s1v";
String contentType = "application/json; charset=utf-8";
String payload = "{\"Limit\": 1, \"Filters\": [{\"Values\": [\"\\u672a\\u547d\\u540d\"], \"Name\": " +
"\"instance-name\"}]}";
Instant timestamp = Instant.ofEpochSecond(1551113065L);
URL parsedUrl = new URL(url);
String host = parsedUrl.getHost();
String queryString = encodeUrlQuery(parsedUrl.getQuery());
String uri = parsedUrl.getPath();
String requestDateTime = ZonedDateTime.ofInstant(timestamp, ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"));
// step 1: create a canonical request
Map<String, String> toSignHeaders = new HashMap<>();
toSignHeaders.put("host", host);
toSignHeaders.put("x-api-time", requestDateTime);
if (!contentType.isEmpty()) {
toSignHeaders.put("content-type", contentType);
}
String httpRequestMethod = method;
String canonicalUri = buildCanonicalUri(uri);
String canonicalHeaders = buildCanonicalHeaders(toSignHeaders);
String canonicalQueryString = buildCanonicalQueryString(queryString);
String signedHeaders = buildSignedHeaders(toSignHeaders);
String payloadHash = hashPayload(payload);
String canonicalRequest = httpRequestMethod + "\n"
+ canonicalUri + "\n"
+ canonicalQueryString + "\n"
+ canonicalHeaders + "\n"
+ signedHeaders + "\n"
+ payloadHash;
// step 2: create the string to sign
String algorithm = "HMAC-SHA256";
String date = ZonedDateTime.ofInstant(timestamp, ZoneId.of("UTC"))
.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String credentialScope = date + "/request";
String hashedCanonicalRequest = hashCanonicalRequest(canonicalRequest);
String stringToSign = algorithm + "\n"
+ requestDateTime + "\n"
+ credentialScope + "\n"
+ hashedCanonicalRequest;
// step 3: calculate the signature
byte[] signingKeyDate = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, accessKeySecret).hmac(date);
byte[] signingKey = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, signingKeyDate).hmac("request");
String signature = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, signingKey).hmacHex(stringToSign);
// step 4: create authorization
String authorization = algorithm
+ " Credential=" + accessKeyId + "/" + credentialScope
+ ", SignedHeaders=" + signedHeaders + ", Signature=" + signature;
// step 5: create request
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", authorization);
headers.put("X-Api-Time", requestDateTime);
if (!contentType.isEmpty()) {
headers.put("Content-Type", contentType);
}
HttpClient client = HttpClient.newBuilder().build();
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(new URI(url))
.method(method, HttpRequest.BodyPublishers.ofString(payload));
headers.forEach(requestBuilder::header);
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}
```