不用 SDK:Node.js 手写华为云 API 的 SDK-HMAC-SHA256 签名
背景
调试开发者空间云开发环境时,我需要直接调用 devstation 的开放接口查询和关闭实例(例如 GET /open-api-public/v1/devenvs/current、POST /open-api-public/v1/devenvs/{id}/close)。官方 SDK 没有覆盖这几个接口,只能用 AK/SK 自己签名。本文记录用 Node.js 内置 crypto、不依赖任何第三方库手写华为云 SDK-HMAC-SHA256 签名的过程,以及几个容易出错的细节。
签名流程概览
华为云 API 网关的 AK/SK 认证分四步:
- 构造规范请求(CanonicalRequest):方法、规范 URI、规范查询串、规范头、参与签名的头列表、请求体 SHA256,用换行拼接。
- 构造待签字符串(StringToSign):
SDK-HMAC-SHA256、请求时间、规范请求的 SHA256 十六进制,用换行拼接。 - 用 SK 对待签字符串做 HMAC-SHA256,得到十六进制签名。
- 把 AK、SignedHeaders、Signature 放进 Authorization 头:
SDK-HMAC-SHA256 Access=AK, SignedHeaders=host;x-sdk-date, Signature=...。
最小实现
import crypto from "node:crypto";
const sha = (d) => crypto.createHash("sha256").update(d).digest("hex");
const hmac = (k, d) => crypto.createHmac("sha256", k).update(d).digest("hex");
function sign({ ak, sk, method, url, body = "" }) {
const u = new URL(url);
const ts = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+/, "");
// 规范 URI:逐段编码,并且以 / 结尾
const uri = "/" + u.pathname.split("/").filter(Boolean)
.map(encodeURIComponent).join("/") + "/";
// 规范查询串:按 key 排序
const query = [...u.searchParams].sort(([a], [b]) => a < b ? -1 : 1)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
const headers = `host:${u.host}\nx-sdk-date:${ts}\n`;
const signed = "host;x-sdk-date";
const canonical = [method, uri, query, headers, signed, sha(body)].join("\n");
const toSign = `SDK-HMAC-SHA256\n${ts}\n${sha(canonical)}`;
return {
"X-Sdk-Date": ts,
Authorization: `SDK-HMAC-SHA256 Access=${ak}, SignedHeaders=${signed}, Signature=${hmac(sk, toSign)}`,
"Content-Type": "application/json",
};
}
调用时把签名用的 body 字符串原样作为 fetch 的 body 发送:
const body = JSON.stringify({ source: "CLI" });
const r = await fetch(url, { method: "POST", headers: sign({ ak, sk, method: "POST", url, body }), body });
踩过的坑
1. 规范 URI 末尾的斜杠。华为云的规范 URI 要求以 / 结尾,即使真实请求路径没有斜杠。这一点和 AWS SigV4 不同,照搬 AWS 的实现会一直报签名不匹配。注意只是签名里加斜杠,实际请求的 URL 不要改。
2. 签名的 body 和发送的 body 必须是同一个字符串。我一开始对对象调用了两次 JSON.stringify,中间有人往对象里加了字段,导致哈希不一致。稳妥做法是先序列化成字符串,签名和发送都用这一个变量。GET 请求的 body 哈希是空字符串的 SHA256(e3b0c442...),不能省略这一行。
3. 时间格式和时钟偏差。X-Sdk-Date 是 UTC 的 YYYYMMDDTHHMMSSZ,不能带毫秒和冒号。本机时间偏差过大时网关会拒绝请求,报错和签名错误很像,排查时先对一下系统时间。
4. 临时凭证要多签一个头。使用 STS 临时 AK/SK 时,需要额外带 X-Security-Token,并且把 x-security-token 同时写进规范头和 SignedHeaders,否则返回 401。
5. 规范头必须小写并排序。SignedHeaders 列表与规范头部分都用小写名,按字典序排列,每行以换行结尾;最后一行后面也有换行,拼接时再加一个分隔换行,最容易少写一个。
排错建议
- 把 canonical 和 toSign 打印出来,和官方 SDK(例如 Python 版 apig-sdk)对同一请求生成的结果逐行比对,差异一般一眼就能看出。
- 先用只读的 GET 接口验证签名,确认通过后再调用会改变资源状态的 POST 接口。
- AK/SK 只放在本地配置文件或环境变量里,脚本和日志都不要打印 SK;用完的临时密钥及时在 IAM 删除,删除后复查一次接口返回 401,确认已经失效。
小结
SDK-HMAC-SHA256 本质上是“规范化请求 + 两次哈希 + 一次 HMAC”,几十行代码就能实现。真正耗时的是规范化细节:URI 末尾斜杠、body 一致性、时间格式和头部排序。把中间串打印出来逐行比对,是最快的排错方式。
- 点赞
- 收藏
- 关注作者
评论(0)