AK/SK 请求签名
管理接口使用 Access Key(AK)识别调用方,并使用 Secret Key(SK)对请求内容签名。最终认证头格式如下:
Authorization: Qiniu <AccessKey>:<EncodedSign>其中 Qiniu 是协议固定值,EncodedSign 是使用 SK 对规范化请求做 HMAC-SHA1 后得到的 URL-safe Base64 字符串。
警告
SK 只应保存在服务端或密钥管理服务中。不要把 SK 写入浏览器代码、移动端应用、日志或公开仓库。
签名流程
1. 构造待签名字符串
按照以下顺序拼接请求信息:
- 请求方法、一个空格、请求路径;存在查询参数时,追加
?和原始查询字符串。 - 换行后追加
Host: <host>。 - 请求包含
Content-Type时,换行后追加Content-Type: <content-type>。 - 请求包含以
X-Qiniu-开头的扩展头时,按头名称升序排列,每个头占一行,格式为<Header>: <value>。 - 追加两个换行符。
- 请求体非空、存在
Content-Type且类型不是application/octet-stream时,追加请求体的原始字节。
例如,批量创建两个 API Key 的待签名字符串为:
POST /v1/apikeys
Host: api.qnaigc.com
Content-Type: application/json
{"count":2,"names":["batch-a","batch-b"]}信息
请求体必须先序列化,再使用完全相同的字符串进行签名和发送。JSON 的空格、换行或字段顺序发生变化,都会产生不同的签名。
2. 计算签名
使用 SK 作为密钥,对待签名字符串计算 HMAC-SHA1。再对摘要做 URL-safe Base64 编码:将标准 Base64 中的 + 替换为 -、/ 替换为 _,并保留末尾的 = 填充。
3. 设置认证头
将 AK 和编码后的签名写入 Authorization 请求头:
Qiniu <AccessKey>:<EncodedSign>Node.js 示例
以下示例仅使用 Node.js 内置的 crypto 和 fetch,无需安装额外依赖。Node.js 18 及以上版本可直接运行。
import crypto from "node:crypto";
const accessKey = process.env.MODELINK_ACCESS_KEY;
const secretKey = process.env.MODELINK_SECRET_KEY;
const url = "https://api.qnaigc.com/v1/apikeys";
const body = JSON.stringify({
count: 2,
names: ["batch-a", "batch-b"],
});
const headers = {
"Content-Type": "application/json",
};
function canonicalHeaderName(name) {
return name
.toLowerCase()
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("-");
}
function getHeader(headers, name) {
const target = name.toLowerCase();
const entry = Object.entries(headers).find(
([key]) => key.toLowerCase() === target,
);
return entry?.[1] ?? "";
}
function createAuthorization(method, url, headers, body, ak, sk) {
const parsed = new URL(url);
const requestTarget = `${parsed.pathname}${parsed.search}`;
let signingText = `${method} ${requestTarget}\nHost: ${parsed.host}`;
const contentType = getHeader(headers, "Content-Type");
if (contentType) {
signingText += `\nContent-Type: ${contentType}`;
}
const extensionHeaders = Object.entries(headers)
.map(([key, value]) => [canonicalHeaderName(key), value])
.filter(([key]) => key.startsWith("X-Qiniu-"))
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
for (const [key, value] of extensionHeaders) {
signingText += `\n${key}: ${value}`;
}
signingText += "\n\n";
if (body && contentType && contentType !== "application/octet-stream") {
signingText += body;
}
const encodedSign = crypto
.createHmac("sha1", sk)
.update(signingText)
.digest("base64")
.replaceAll("+", "-")
.replaceAll("/", "_");
return `Qiniu ${ak}:${encodedSign}`;
}
if (!accessKey || !secretKey) {
throw new Error("请先设置 MODELINK_ACCESS_KEY 和 MODELINK_SECRET_KEY");
}
headers.Authorization = createAuthorization(
"POST",
url,
headers,
body,
accessKey,
secretKey,
);
const response = await fetch(url, {
method: "POST",
headers,
body,
});
console.log(response.status, await response.text());运行前设置环境变量:
export MODELINK_ACCESS_KEY="替换为你的 AK"
export MODELINK_SECRET_KEY="替换为你的 SK"
node create-api-keys.mjsPython 示例
以下示例使用 requests 发送请求:
import base64
import hashlib
import hmac
import json
import os
from urllib.parse import urlsplit
import requests
def canonical_header_name(name: str) -> str:
return "-".join(part[:1].upper() + part[1:].lower() for part in name.split("-"))
def get_header(headers: dict[str, str], name: str) -> str:
target = name.lower()
return next((value for key, value in headers.items() if key.lower() == target), "")
def create_authorization(
method: str,
url: str,
headers: dict[str, str],
body: str,
access_key: str,
secret_key: str,
) -> str:
parsed = urlsplit(url)
request_target = parsed.path or "/"
if parsed.query:
request_target += f"?{parsed.query}"
signing_text = f"{method} {request_target}\nHost: {parsed.netloc}"
content_type = get_header(headers, "Content-Type")
if content_type:
signing_text += f"\nContent-Type: {content_type}"
extension_headers = sorted(
(
(canonical_header_name(key), value)
for key, value in headers.items()
if canonical_header_name(key).startswith("X-Qiniu-")
),
key=lambda item: item[0],
)
for key, value in extension_headers:
signing_text += f"\n{key}: {value}"
signing_text += "\n\n"
if body and content_type and content_type != "application/octet-stream":
signing_text += body
digest = hmac.new(
secret_key.encode(),
signing_text.encode(),
hashlib.sha1,
).digest()
encoded_sign = base64.urlsafe_b64encode(digest).decode()
return f"Qiniu {access_key}:{encoded_sign}"
access_key = os.environ["MODELINK_ACCESS_KEY"]
secret_key = os.environ["MODELINK_SECRET_KEY"]
url = "https://api.qnaigc.com/v1/apikeys"
body = json.dumps(
{"count": 2, "names": ["batch-a", "batch-b"]},
separators=(",", ":"),
)
headers = {"Content-Type": "application/json"}
headers["Authorization"] = create_authorization(
"POST", url, headers, body, access_key, secret_key
)
response = requests.post(url, headers=headers, data=body, timeout=30)
print(response.status_code, response.text)运行前安装依赖并设置环境变量:
python -m pip install requests
export MODELINK_ACCESS_KEY="替换为你的 AK"
export MODELINK_SECRET_KEY="替换为你的 SK"
python create_api_keys.py常见签名失败原因
- 签名使用的 Host 与实际请求 Host 不一致。
- 查询参数的顺序或编码在签名后发生变化。
- 签名后的 JSON 又被重新序列化,导致请求体字节不同。
- 漏签
Content-Type,或签名值与实际发送值不一致。 X-Qiniu-扩展头未按头名称升序签名。- URL-safe Base64 编码时错误地删除了末尾的
=填充。 - 使用了 API Key 代替 AK,或使用 AK 代替 SK 计算 HMAC。