AK/SK 请求签名

管理接口使用 Access Key(AK)识别调用方,并使用 Secret Key(SK)对请求内容签名。最终认证头格式如下:

Authorization: Qiniu <AccessKey>:<EncodedSign>

其中 Qiniu 是协议固定值,EncodedSign 是使用 SK 对规范化请求做 HMAC-SHA1 后得到的 URL-safe Base64 字符串。

警告

SK 只应保存在服务端或密钥管理服务中。不要把 SK 写入浏览器代码、移动端应用、日志或公开仓库。

签名流程

1. 构造待签名字符串

按照以下顺序拼接请求信息:

  1. 请求方法、一个空格、请求路径;存在查询参数时,追加 ? 和原始查询字符串。
  2. 换行后追加 Host: <host>
  3. 请求包含 Content-Type 时,换行后追加 Content-Type: <content-type>
  4. 请求包含以 X-Qiniu- 开头的扩展头时,按头名称升序排列,每个头占一行,格式为 <Header>: <value>
  5. 追加两个换行符。
  6. 请求体非空、存在 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 内置的 cryptofetch,无需安装额外依赖。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.mjs

Python 示例

以下示例使用 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。