API 签名用于验证请求者身份并防止请求被篡改。为降低接入成本,推荐您使用火山引擎 SDK,免去签名过程,或使用 API Explorer 了解 API 的签名生成过程。本文介绍了火山引擎 API 的签名过程,帮助您调用云产品 API。
说明
如果在签名生成过程中遇到问题,您可以提交工单或点击页面右下方的在线客服,与火山引擎技术支持沟通。
火山引擎 SDK:火山引擎封装了常见编程语言的 SDK,您可以通过 SDK 直接调用云产品的 API。
API Explorer:如果目前提供的 SDK 无法满足您的需求,您也可以通过 API Explorer 的签名工具模块查看每个 API 的签名生成过程。
您只需输入 Access Key ID、Secret Access Key、Host 等信息,即可生成签名结果和可执行的 curl 命令。
注意
billing.volcengineapi.com。您可以查看不同云产品的 API 文档,获取服务地址。计算签名的流程如下所示。
由于 GET 和 POST 请求的签名细节存在差异,因此本文分别以 GET 和 POST 请求为例介绍如何计算签名。
GET 请求原始示例
GET https://billing.volcengineapi.com/?Action=QueryBalanceAcct&Version=2022-01-01 HTTP/1.1 Host: billing.volcengineapi.com X-Date: 20250329T180937Z
POST 请求原始示例
Content-Type 为 application/json 时POST https://billing.volcengineapi.com/?Action=ListBill&Version=2022-01-01 HTTP/1.1 Host: billing.volcengineapi.com Content-Type: application/json X-Date: 20250329T180937Z {"Limit":10,"BillPeriod":"2023-08"}
Content-Type 为 application/x-www-form-urlencoded 时POST https://iam.volcengineapi.com/?Action=CreateLoginProfile&Version=2018-01-01 HTTP/1.1 Host: iam.volcengineapi.com Content-Type: application/x-www-form-urlencoded X-Date: 20240619T071306Z LoginAllowed=true&Password=123&UserName=%E5%B0%8F%E6%98%8E
在签名过程中,本文以 Content-Type 为 application/json 为例进行签名结果演示。两种 Content-Type 的签名过程基本相同,仅 RequestPayload 的内容存在差异,详见步骤二。
获取火山引擎账号的访问密钥(Access Key ID 和 Secret Access Key)。具体操作,详见密钥管理。
本文中,假设 Access Key ID 和 Secret Access Key 分别为 AKLTYWViMTVmZGYzM2E0NDI5Mzk2MDZjNjFmMjc2MjRjMzg 和 WkRZeE1EQmxPVGhsWWpWak5HVmtNbUUxTXpZeU9UVXlOMlE1TmpZeVlqTQ==。
创建规范请求,可以确保您计算出的签名能够与火山引擎计算出的签名相匹配。
规范请求的伪代码如下所示。
说明
字符串之间请使用换行符分隔。
CanonicalRequest = HTTPRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload))
| 名称 | 描述 | GET 请求示例值 | POST 请求示例值 |
|---|---|---|---|
| HTTPRequestMethod | HTTP 请求方法,即 GET 或 POST。 |
GET |
POST |
| CanonicalURI |
规范化 URI,即请求地址的资源路径部分经过编码后的结果。资源路径部分指请求地址中 Host 和查询字符串之间的部分,包含 Host 后的 /,但不包含查询字符串前的 ?。火山引擎大部分 API 的 URI 均为 /。如果资源路径部分包含除 / 外的其他内容,则需要使用 UTF-8 字符集按照 RFC3986 规范进行 URL 编码,例如空格会编码为%20。说明
|
/ |
/ |
| CanonicalQueryString |
规范化查询字符串。生成方法如下所示:
说明 查询字符串为请求地址中问号( |
Action=QueryBalanceAcct&Version=2022-01-01 |
Action=ListBill&Version=2022-01-01 |
| CanonicalHeaders |
规范化请求头,使用换行符('\n')分隔各 Header 的名称和值对。伪代码如下所示:
Lowercase(HeaderName0) + ':' + Trim(HeaderValue) + '\n'
Lowercase(HeaderName1) + ':' + Trim(HeaderValue) + '\n'
...
Lowercase(HeaderNameN) + ':' + Trim(HeaderValue) + '\n'
说明
|
host:billing.volcengineapi.com x-date:20250329T180937Z |
host:billing.volcengineapi.com x-date:20250329T180937Z |
| SignedHeaders |
参与签名的 Header,与 CanonicalHeaders 中包含的 Header 一一对应,用于指明哪些 Header 参与签名计算。伪代码如下所示:
Lowercase(HeaderName0) + ';' +
Lowercase(HeaderName1) + ';' +
...
Lowercase(HeaderNameN)
说明 代理服务器(Proxy)可能在转发过程中修改 Keep-Alive、Proxy-Authorization 等 Header。为避免签名报错,建议此类 Header 不要参与签名。
说明
|
host;x-date |
host;x-date |
| RequestPayload |
使用 SHA256 作为哈希函数,将 HTTP 请求 Body 中的数据作为哈希函数的输入,计算哈希值,并对哈希值进行小写十六进制编码。 GET 和 POST 请求的 Body 存在差异。
说明
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
e8cc56e129d9759d56c936e679a345d001a4235b58bee8e935ccad97f23ed663 |
根据以上规则,在本文示例中,得到的规范请求如下所示。
注意
以下示例中,由于 CanonicalHeaders 本身必须以换行符(\n)结尾,当它与 SignedHeaders 通过另一个换行符(\n)拼接时,两个连续的换行符(\n\n)会自然地形成一个空行。
GET 请求
GET / Action=QueryBalanceAcct&Version=2022-01-01 host:billing.volcengineapi.com x-date:20250329T180937Z host;x-date e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
POST 请求
POST / Action=ListBill&Version=2022-01-01 host:billing.volcengineapi.com x-date:20250329T180937Z host;x-date e8cc56e129d9759d56c936e679a345d001a4235b58bee8e935ccad97f23ed663
创建待签名字符串的伪代码如下所示。
说明
字符串之间请使用换行符分隔。
StringToSign = Algorithm + '\n' + RequestDate + '\n' + CredentialScope + '\n' + HexEncode(Hash(CanonicalRequest))
名称 | 描述 | GET 请求示例值 | POST 请求示例值 |
|---|---|---|---|
Algorithm | 签名算法。目前火山引擎仅支持 HMAC-SHA256 算法。 |
|
|
RequestDate | 请求发起的时间, 遵循 ISO 8601 格式的 UTC+0 时间,精度为秒。格式为 |
|
|
CredentialScope | 凭证范围,格式为
|
|
|
CanonicalRequest | 使用 SHA256 作为哈希函数,将上一步中创建的规范请求( 说明
|
|
|
根据以上规则,在本文示例中,得到的待签名字符串如下所示。
GET 请求
HMAC-SHA256 20250329T180937Z 20250329/cn-beijing/billing/request 43171c1658c64b5db55c58d54988a4598d2d09a5613136beaa5eef40eae6e2c1
POST 请求
HMAC-SHA256 20250329T180937Z 20250329/cn-beijing/billing/request 27383e3b56d03850f5634483527fbddcbf06cf98de1bc8a6679ef2300bff3b15
在计算签名前,需要先使用访问密钥的 Secret Access Key 派生出签名密钥(kSigning)。
派生签名密钥的伪代码如下所示。
kSecret = Your Secret Access Key kDate = HMAC(kSecret, Date) kRegion = HMAC(kDate, Region) kService = HMAC(kRegion, Service) kSigning = HMAC(kService, "request")
以下步骤使用的哈希函数均为 HMAC-SHA256。
说明
此处生成的哈希值实际取值为二进制,但可能包含不可打印的字符,因此以下步骤中均使用转换为十六进制字符串的哈希值。
将 Secret Access Key 作为 String 类型的密钥,将 Date 的取值作为哈希函数的输入。其中,Date 的取值与 CredentialScope 中的 {YYYYMMDD} 部分相同。在本文的 GET 和 POST 请求示例中,Secret Access Key 为 WkRZeE1EQmxPVGhsWWpWak5HVmtNbUUxTXpZeU9UVXlOMlE1TmpZeVlqTQ==,Date 为 20250329,则计算得出的哈希值为 069b1da2ba9c0ecbd8e8aaf2a5742696ebc22f3fe95a649983d31b433ba94ff3。
kDate = HMAC(kSecret, Date)
将上一步的计算结果作为 Hex(十六进制)类型的密钥,将 Region 的取值作为哈希函数的输入。其中,Region 的取值与 CredentialScope 中的 {region} 部分相同。在本文的 GET 和 POST 请求示例中,Region 为 cn-beijing,则计算得出的哈希值为 2f41e8c797f1f0200484c9b0986f89cb371bf44d51e37ca7ad0e12dcbbd83cfd。
kRegion = HMAC(kDate, Region)
将上一步的计算结果作为 Hex(十六进制)类型的密钥,将 Service 的取值作为哈希函数的输入。其中,Service 的取值与 CredentialScope 中的 {service} 部分相同。 在本文的 GET 和 POST 请求示例中,Service 为 billing,则计算得出的哈希值为 2d9caf568d4a1bd052daf42378d281e7f3233c7110dd6849988bd17fd5423777。
kService = HMAC(kRegion, Service)
将上一步的计算结果作为 Hex(十六进制)类型的密钥,将 request 作为哈希函数的输入,则计算得出的哈希值(即最终的派生签名密钥)为 b491ed164936de3bb06c1eb23326aa9587b5aaa6a4e02144b9d523bbebb7ca9f。
kSigning = HMAC(kService, "request")
计算签名的伪代码如下所示。
Signature = HexEncode(HMAC(kSigning, StringToSign))
将派生签名密钥(kSigning)作为 Hex(十六进制)类型的密钥,将待签名字符串(StringToSign)作为 HMAC-SHA256 哈希函数的输入,并将计算得出的哈希值从二进制转换为十六进制,使用小写字符。在本文示例中,最终的计算结果如下所示。
GET 请求
1eda9e7e6b1728151a8e8791fdaf67cfbd28bd5c80d0fce2eb208746cf483105
POST 请求
5e8480ceea12d0000a23c054151c50dd02c1a7dec835004057d19f13d53a7658
按需将计算的签名添加至请求的 Header 或 Query 中。推荐添加至请求的 Header 中。
构建 Authorization 请求头,伪代码如下所示。
Authorization: Algorithm Credential={AccessKeyId}/{CredentialScope}, SignedHeaders={SignedHeaders}, Signature={Signature}
名称 | 描述 | GET 请求示例值 | POST 请求示例值 |
|---|---|---|---|
Algorithm | 签名算法。目前火山引擎仅支持 HMAC-SHA256 算法。 |
|
|
AccessKeyId | 您访问密钥的 Access Key ID。获取方式,详见步骤一。 |
|
|
CredentialScope | 凭证范围,详见步骤三。 |
|
|
SignedHeaders | 参与签名的 Header,详见步骤二。 |
|
|
Signature | 计算的签名。详见步骤五。 |
|
|
在本文示例中,Authorization 请求头如下所示。
GET 请求
HMAC-SHA256 Credential=AKLTYWViMTVmZGYzM2E0NDI5Mzk2MDZjNjFmMjc2MjRjMzg/20250329/cn-beijing/billing/request, SignedHeaders=host;x-date, Signature=1eda9e7e6b1728151a8e8791fdaf67cfbd28bd5c80d0fce2eb208746cf483105
POST 请求
HMAC-SHA256 Credential=AKLTYWViMTVmZGYzM2E0NDI5Mzk2MDZjNjFmMjc2MjRjMzg/20250329/cn-beijing/billing/request, SignedHeaders=host;x-date, Signature=5e8480ceea12d0000a23c054151c50dd02c1a7dec835004057d19f13d53a7658
最终完整的调用信息如下所示。
GET 请求
GET https://billing.volcengineapi.com/?Action=QueryBalanceAcct&Version=2022-01-01 HTTP/1.1 Authorization: HMAC-SHA256 Credential=AKLTYWViMTVmZGYzM2E0NDI5Mzk2MDZjNjFmMjc2MjRjMzg/20250329/cn-beijing/billing/request, SignedHeaders=host;x-date, Signature=1eda9e7e6b1728151a8e8791fdaf67cfbd28bd5c80d0fce2eb208746cf483105 Host: billing.volcengineapi.com X-Date: 20250329T180937Z
POST 请求
POST https://billing.volcengineapi.com/?Action=ListBill&Version=2022-01-01 HTTP/1.1 Host: billing.volcengineapi.com Content-Type: application/json X-Date: 20250329T180937Z Authorization: HMAC-SHA256 Credential=AKLTYWViMTVmZGYzM2E0NDI5Mzk2MDZjNjFmMjc2MjRjMzg/20250329/cn-beijing/billing/request, SignedHeaders=host;x-date, Signature=5e8480ceea12d0000a23c054151c50dd02c1a7dec835004057d19f13d53a7658 {"Limit":10,"BillPeriod":"2023-08"}
伪代码如下所示。
X-Algorithm=algorithm&X-Credential=credential&X-Date=date&X-Expires=time&X-SignedHeaders=headers&X-SignedQueries=queries&X-Signature=signature
名称 | 描述 | GET 请求示例值 | POST 请求示例值 |
|---|---|---|---|
X-Algorithm | 签名算法。目前火山引擎仅支持 HMAC-SHA256 算法。 |
|
|
X-Credential | 由 在该示例中,您只需将 |
|
|
X-Date | 请求发起的时间,遵循 ISO 8601 格式的 UTC+0 时间,精度为秒。格式为 |
|
|
X-Expires | 签名的有效时间。该参数可选。默认值为 |
|
|
X-SignedHeaders | 参与签名的 Header,详见步骤二。 |
|
|
X-SignedQueries | 参与签名的 Query,即步骤二中 |
|
|
X-Signature | 计算的签名。详见步骤五。 |
|
|
在实际调用 API 时,推荐您使用配套的 SDK。SDK 封装了签名过程,开发接入成本更低。目前支持的编程语言有:
以下签名示例以主流编程语言为例,完整实现了上述的签名过程,以便您更清晰地理解签名机制。您可以根据实际情况,结合上述签名过程,按需对签名示例进行改造,从而完成签名。
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.*; /** * Copyright (year) Beijing Volcano Engine Technology Ltd. * <p> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ public class Sign { private static final BitSet URLENCODER = new BitSet(256); private static final String CONST_ENCODE = "0123456789ABCDEF"; public static final Charset UTF_8 = StandardCharsets.UTF_8; private final String region; private final String service; private final String schema; private final String host; private final String path; private final String ak; private final String sk; static { int i; for (i = 97; i <= 122; ++i) { URLENCODER.set(i); } for (i = 65; i <= 90; ++i) { URLENCODER.set(i); } for (i = 48; i <= 57; ++i) { URLENCODER.set(i); } URLENCODER.set('-'); URLENCODER.set('_'); URLENCODER.set('.'); URLENCODER.set('~'); } public Sign(String region, String service, String schema, String host, String path, String ak, String sk) { this.region = region; this.service = service; this.host = host; this.schema = schema; this.path = path; this.ak = ak; this.sk = sk; } public static void main(String[] args) throws Exception { String SecretAccessKey = "****"; String AccessKeyID = "AK****"; // 请求地址 String endpoint = "iam.volcengineapi.com"; String path = "/"; // 路径,不包含 Query// 请求接口信息 String service = "iam"; String region = "cn-beijing"; String schema = "https"; Sign sign = new Sign(region, service, schema, endpoint, path, AccessKeyID, SecretAccessKey); String action = "ListPolicies"; String version = "2018-01-01"; Date date = new Date(); HashMap<String, String> queryMap = new HashMap<>() {{ put("Limit", "1"); }}; sign.doRequest("POST", queryMap, null, date, action, version); } public void doRequest(String method, Map<String, String> queryList, byte[] body, Date date, String action, String version) throws Exception { if (body == null) { body = new byte[0]; } String xContentSha256 = hashSHA256(body); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); sdf.setTimeZone(TimeZone.getTimeZone("GMT")); String xDate = sdf.format(date); String shortXDate = xDate.substring(0, 8); String contentType = "application/x-www-form-urlencoded"; String signHeader = "host;x-date;x-content-sha256;content-type"; SortedMap<String, String> realQueryList = new TreeMap<>(queryList); realQueryList.put("Action", action); realQueryList.put("Version", version); StringBuilder querySB = new StringBuilder(); for (String key : realQueryList.keySet()) { querySB.append(signStringEncoder(key)).append("=").append(signStringEncoder(realQueryList.get(key))).append("&"); } querySB.deleteCharAt(querySB.length() - 1); String canonicalStringBuilder = method + "\n" + path + "\n" + querySB + "\n" + "host:" + host + "\n" + "x-date:" + xDate + "\n" + "x-content-sha256:" + xContentSha256 + "\n" + "content-type:" + contentType + "\n" + "\n" + signHeader + "\n" + xContentSha256; System.out.println(canonicalStringBuilder); String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes()); String credentialScope = shortXDate + "/" + region + "/" + service + "/request"; String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString; byte[] signKey = genSigningSecretKeyV4(sk, shortXDate, region, service); String signature = HexFormat.of().formatHex(hmacSHA256(signKey, signString)); URL url = new URL(schema + "://" + host + path + "?" + querySB); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); conn.setRequestProperty("Host", host); conn.setRequestProperty("X-Date", xDate); conn.setRequestProperty("X-Content-Sha256", xContentSha256); conn.setRequestProperty("Content-Type", contentType); conn.setRequestProperty("Authorization", "HMAC-SHA256" + " Credential=" + ak + "/" + credentialScope + ", SignedHeaders=" + signHeader + ", Signature=" + signature); if (!Objects.equals(conn.getRequestMethod(), "GET")) { conn.setDoOutput(true); OutputStream os = conn.getOutputStream(); os.write(body); os.flush(); os.close(); } conn.connect(); int responseCode = conn.getResponseCode(); InputStream is; if (responseCode == 200) { is = conn.getInputStream(); } else { is = conn.getErrorStream(); } String responseBody = new String(is.readAllBytes()); is.close(); System.out.println(responseCode); System.out.println(responseBody); } private String signStringEncoder(String source) { if (source == null) { return null; } StringBuilder buf = new StringBuilder(source.length()); ByteBuffer bb = UTF_8.encode(source); while (bb.hasRemaining()) { int b = bb.get() & 255; if (URLENCODER.get(b)) { buf.append((char) b); } else if (b == 32) { buf.append("%20"); } else { buf.append("%"); char hex1 = CONST_ENCODE.charAt(b >> 4); char hex2 = CONST_ENCODE.charAt(b & 15); buf.append(hex1); buf.append(hex2); } } return buf.toString(); } public static String hashSHA256(byte[] content) throws Exception { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); return HexFormat.of().formatHex(md.digest(content)); } catch (Exception e) { throw new Exception( "Unable to compute hash while signing request: " + e.getMessage(), e); } } public static byte[] hmacSHA256(byte[] key, String content) throws Exception { try { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key, "HmacSHA256")); return mac.doFinal(content.getBytes()); } catch (Exception e) { throw new Exception( "Unable to calculate a request signature: " + e.getMessage(), e); } } private byte[] genSigningSecretKeyV4(String secretKey, String date, String region, String service) throws Exception { byte[] kDate = hmacSHA256((secretKey).getBytes(), date); byte[] kRegion = hmacSHA256(kDate, region); byte[] kService = hmacSHA256(kRegion, service); return hmacSHA256(kService, "request"); } }