火山素材接口认证 Key 与 V4 签名
这里的“验证 Key”不是一把单独的火山素材密钥。调用 Modelink 的火山素材兼容接口时,先在 Modelink 控制台创建一个 API Key;这个 Key 用于查询项目,也同时作为火山 V4 签名中的 Access Key 和签名密钥。
信息
GET /v1/asset-projects 使用 Authorization: Bearer,用于查询当前 Key 可用的
ProjectName。素材 Action 使用 POST /volcengine/assets/,必须针对最终 URL
和原始请求体生成 HMAC-SHA256 签名。两种请求使用同一个 API Key。
如果你直接调用火山方舟官方域名,请在火山控制台创建官方 AK/SK,并将它们传给同样的签名算法;不要把官方 AK/SK 和 Modelink API Key 混用。本文的请求示例默认使用 Modelink 的兼容入口。
先准备 API Key
在控制台创建 Key
打开 Modelink 控制台的 API Key 页面,创建一个专用于素材服务的 Key。名称建议包含环境和用途,例如
staging-volcengine-assets或prod-avatar-assets。创建后立即复制完整 Key。出于安全原因,后续页面通常只展示脱敏值。
只在服务端配置 Key
将 Key 注入服务端环境变量或密钥管理服务。中国大陆和海外入口分别如下:
export MODELINK_API_KEY="替换为你的 Modelink API Key" export MODELINK_BASE_URL="https://api.qnaigc.com" # 海外部署:export MODELINK_BASE_URL="https://api.modelink.ai"不要把 Key 写进浏览器代码、移动端包、日志、截图或公开仓库。
先验证 Key 的项目权限
素材 Action 的 ProjectName 不能自行填写,也不能使用 default。先调用项目查询接口,并选择返回结果中与你的素材类型匹配的项目:
curl "$MODELINK_BASE_URL/v1/asset-projects" \
-H "Authorization: Bearer $MODELINK_API_KEY"成功响应示例:
{
"data": [
{
"project_name": "cv1-bogic",
"supported_asset_types": ["aigc", "liveness_face"]
}
]
}将返回的项目名保存下来:
export PROJECT_NAME="cv1-bogic"aigc 用于虚拟人像素材,liveness_face 用于需要真人认证的素材。如果 data 为空,说明当前 Key 尚未分配素材项目或对应能力,请先联系项目管理员;这不是签名格式问题。
V4 签名需要哪些值
每个素材 Action 都发送到下面的固定路径,末尾斜杠不能省略:
POST /volcengine/assets/?Action=<Action>&Version=2024-01-01签名 helper 会从 Host 推断区域;Modelink 入口使用默认区域 cn-beijing,火山官方 Host(例如 ark.cn-beijing.volces.com)会从域名读取区域。签名覆盖的请求头顺序固定为:
content-type;host;x-content-sha256;x-date最终请求必须带上以下头:
Authorization: HMAC-SHA256 Credential=<access-key>/<date>/<region>/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=<hex-signature>
X-Date: <UTC yyyyMMddTHHmmssZ>
X-Content-Sha256: <request-body-sha256>
Content-Type: application/json注意事项:
X-Date使用 UTC,服务端允许的时钟偏差为 5 分钟。X-Content-Sha256必须对最终发送的请求体原始字节计算,签名后不能再次序列化 JSON。Action和Version是查询参数,必须把最终顺序和编码后的值用于签名。- 每个请求都重新生成
X-Date、摘要和Authorization,不能跨请求复用。
在线生成签名请求头
下面的组件使用浏览器内置 Web Crypto,在本地生成与 volc_sign.py 等价的四个请求头。它不会创建真实 API Key,也不会将 AK/SK 发送到服务器;真实 Key 仍需在控制台创建。
在线生成 V4 签名请求头
计算仅在当前浏览器完成,不会发送或保存你的 AK/SK。
必须包含最终的 Action、Version 和末尾斜杠;签名后不要改变 URL。
摘要和签名使用这里的原始字符;发送时必须使用完全相同的 body 字节。
警告
只建议用临时测试凭据体验此组件。生产环境不要在浏览器输入 Secret Key;应在服务端生成签名并将请求发往素材接口。
保存签名 helper
下面的实现根据 volc_sign.py 的规则整理,只有 Python 标准库依赖。将代码保存为 volc_sign.py。它同时支持 Modelink 兼容 Host 和火山官方 ark.<region>.volces.com Host。
#!/usr/bin/env python3
"""火山/BytePlus Signature V4 helper。"""
from __future__ import annotations
import hashlib
import hmac
import urllib.parse
from datetime import datetime, timezone
DEFAULT_REGION = "cn-beijing"
SERVICE = "ark"
SIGNED_HEADERS = "content-type;host;x-content-sha256;x-date"
def _hmac_sha256(key: bytes, message: str) -> bytes:
return hmac.new(key, message.encode("utf-8"), hashlib.sha256).digest()
def _sha256_hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _rfc3986_escape(value: str) -> str:
return urllib.parse.quote(value, safe="-_.~")
def _canonical_query(raw_query: str) -> str:
if not raw_query:
return ""
pairs = urllib.parse.parse_qsl(raw_query, keep_blank_values=True)
pairs.sort(key=lambda item: (item[0], item[1]))
return "&".join(
f"{_rfc3986_escape(key)}={_rfc3986_escape(value)}"
for key, value in pairs
)
def _region_from_host(host: str) -> str:
try:
hostname = urllib.parse.urlparse(f"//{host}").hostname
except ValueError:
return DEFAULT_REGION
if not hostname:
return DEFAULT_REGION
hostname = hostname.lower()
prefix = "ark."
if not hostname.startswith(prefix):
return DEFAULT_REGION
rest = hostname.removeprefix(prefix)
for suffix in (".byteplusapi.com", ".volces.com"):
if rest.endswith(suffix):
region = rest.removesuffix(suffix)
if region and "." not in region:
return region
return DEFAULT_REGION
def sign_headers(
*,
access_key: str,
secret_key: str,
method: str,
host: str,
path: str,
raw_query: str,
body: bytes,
now: datetime | None = None,
) -> dict[str, str]:
"""Return Content-Type, X-Date, X-Content-Sha256 and Authorization."""
now = now or datetime.now(timezone.utc)
region = _region_from_host(host)
x_date = now.strftime("%Y%m%dT%H%M%SZ")
short_date = now.strftime("%Y%m%d")
body_hash = _sha256_hex(body)
canonical_headers = (
"content-type:application/json\n"
f"host:{host}\n"
f"x-content-sha256:{body_hash}\n"
f"x-date:{x_date}\n"
)
canonical_request = (
f"{method}\n"
f"{path}\n"
f"{_canonical_query(raw_query)}\n"
f"{canonical_headers}\n"
f"{SIGNED_HEADERS}\n"
f"{body_hash}"
)
credential_scope = f"{short_date}/{region}/{SERVICE}/request"
string_to_sign = (
"HMAC-SHA256\n"
f"{x_date}\n"
f"{credential_scope}\n"
f"{_sha256_hex(canonical_request.encode('utf-8'))}"
)
# 火山/BytePlus 风格的派生密钥直接以 SK 为种子。
k_date = _hmac_sha256(secret_key.encode("utf-8"), short_date)
k_region = _hmac_sha256(k_date, region)
k_service = _hmac_sha256(k_region, SERVICE)
k_signing = _hmac_sha256(k_service, "request")
signature = hmac.new(
k_signing,
string_to_sign.encode("utf-8"),
hashlib.sha256,
).hexdigest()
authorization = (
f"HMAC-SHA256 Credential={access_key}/{credential_scope}, "
f"SignedHeaders={SIGNED_HEADERS}, "
f"Signature={signature}"
)
return {
"Content-Type": "application/json",
"X-Date": x_date,
"X-Content-Sha256": body_hash,
"Authorization": authorization,
}用只读 Action 验证签名
创建 verify_asset_key.py,用 ListAssetGroups 做最小验证。这个请求不会创建或删除素材;返回空列表也代表签名和项目权限通过。
#!/usr/bin/env python3
import json
import os
import urllib.error
import urllib.request
from urllib.parse import urlsplit
from volc_sign import sign_headers
api_key = os.environ["MODELINK_API_KEY"]
base_url = os.environ.get("MODELINK_BASE_URL", "https://api.qnaigc.com").rstrip("/")
project_name = os.environ["PROJECT_NAME"]
url = (
f"{base_url}/volcengine/assets/"
"?Action=ListAssetGroups&Version=2024-01-01"
)
payload = {
"Filter": {"GroupType": "AIGC"},
"PageNumber": 1,
"PageSize": 10,
"ProjectName": project_name,
}
# body 先序列化成最终字节,再同时用于摘要、签名和发送。
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode(
"utf-8"
)
parsed = urlsplit(url)
headers = sign_headers(
access_key=api_key,
secret_key=api_key,
method="POST",
host=parsed.netloc,
path=parsed.path,
raw_query=parsed.query,
body=body,
)
request = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(request, timeout=30) as response:
print(response.status)
print(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
print(error.code)
print(error.read().decode("utf-8"))运行:
python verify_asset_key.py验证成功时,响应的 ResponseMetadata.Action 应为 ListAssetGroups;如果没有素材组,Result 为空列表是正常结果。拿到 Active 素材后,再按火山协议 · 虚拟人像素材管理或火山协议 · 真人人像素材管理继续操作。
常见失败原因
| 现象 | 优先检查 |
|---|---|
401 或 Bearer 鉴权失败 | GET /v1/asset-projects 是否使用 Authorization: Bearer <API Key>,Key 是否被禁用或复制不完整 |
403 或签名无效 | access_key 和 secret_key 是否都使用同一个 Key;Host、路径末尾斜杠、Action/Version 查询参数是否与实际请求一致 |
| 请求提示时间无效 | X-Date 是否为 UTC,客户端时钟是否与服务端相差超过 5 分钟 |
X-Content-Sha256 不匹配 | 摘要、签名和发送是否使用了同一份原始 body;不要让 HTTP 客户端再次格式化 JSON |
| 项目或素材权限错误 | ProjectName 是否来自当前 Key 的 /v1/asset-projects 响应,且没有写成 default |
| 火山官方域名签名失败 | Host 是否完整包含 ark.<region>.volces.com,并使用对应官方 AK/SK,而不是 Modelink API Key |
完整字段、响应和错误码请参阅火山素材兼容接口。
警告
签名验证通过后仍应在服务端实施权限隔离、请求日志脱敏、超时和重试控制。尤其不要为了排查问题把完整 API Key 或签名密钥发到聊天、工单或前端日志中。