功能描述
用于通过 JWT Bearer 授权方式生成调用智能媒资托管服务的访问令牌(Access Token)。调用该接口无需使用媒体库密钥,而是由业务后端使用自持的 Ed25519 私钥对 JWT 断言(assertion)签名,服务端使用预先登记在媒体库上的公钥验签后签发访问令牌。
接口说明
该接口用于获取后续调用媒体托管其他接口的访问令牌(Access Token);签名私钥由业务方自行保管,必须在后端完成签名与签发以保证私钥安全;
使用该方式前,需先将 Ed25519 公钥登记到媒体库并启用 JWS 鉴权,详见步骤 2;
签名算法固定为 EdDSA(Ed25519),服务端不接受其他算法,用于防止算法混淆攻击;
assertion 中的
sub_type 决定签发令牌的身份类型,支持 user(指定用户)、service(媒体库管理员)、anonymous(匿名并绑定指定空间)三种;assertion 中的
scope 语义与 生成访问令牌 的 Grant 参数一致,业务后端应当先验证前端的用户身份,再根据业务中该用户的权限指定 scope;每个 assertion 必须携带唯一的
jti,服务端对其做防重放校验,重复使用将被拒绝;assertion 自身为短生命周期凭证,其有效期上限受
sub_type 约束,详见步骤 3;assertion 的有效期不影响签出访问令牌的有效期;同一媒体库支持同时登记多个活跃公钥(原则上2把公钥轮换),可用于在不中断业务的情况下轮换签名密钥;
通过该方式签发的访问令牌,其使用方式、续期与删除均与 生成访问令牌 签发的令牌一致;
整体流程

步骤 1:生成 Ed25519 密钥对
签名算法固定为 EdDSA(Ed25519),不支持 RSA / ECDSA / HMAC。
# 生成 Ed25519 私钥(PKCS#8,BEGIN PRIVATE KEY)openssl genpkey -algorithm ed25519 -out priv.pem# 由私钥导出公钥(PKIX/SPKI,BEGIN PUBLIC KEY)openssl pkey -in priv.pem -pubout -out pub.pem
注意:
安全提醒:
priv.pem 是核心机密,仅在业务后端保管,请勿下发到前端、写入日志、提交到代码仓库。步骤 2:将公钥登记到媒体库
可通过以下两种方式之一,把公钥登记到目标媒体库并开启 JWS 鉴权:
方式一:SMH 控制台(推荐),登录 SMH 控制台 - 媒体库管理,在目标媒体库的操作列点击「设置公钥」,按页面提示登记公钥并启用 JWS 鉴权。
方式二:云 API ,调用云 API
ModifyLibraryJWSConfig 完成登记。请求示例:
{"Action": "ModifyLibraryJWSConfig","LibraryId": "smhxxx","Enabled": true,"AddKey": {"PublicKey": "-----BEGIN PUBLIC KEY-----\\nMCowBQYDK2VwAyEA...\\n-----END PUBLIC KEY-----\\n","Remark": "业务后端 A"}}
请求参数:
请求参数 | 描述 | 类型 | 是否必选 |
Action | 固定为 ModifyLibraryJWSConfig,必选参数; | String | 是 |
LibraryId | 媒体库 ID,必选参数; | String | 是 |
Enabled | 是否启用 JWS 鉴权,布尔值,不传表示不修改该项,可选; | Boolean | 否 |
AddKey | 新增一把公钥,不传表示不新增: | Object | 否 |
AddKey.PublicKey | Ed25519 公钥,PEM(PKIX/SPKI)格式,即 -----BEGIN PUBLIC KEY----- 开头,必选; | String | 条件必选(传入 AddKey 时) |
AddKey.NotAfter | 公钥过期时间,Unix 毫秒时间戳,不传或 0 表示永不过期,可选; | Integer | 否 |
AddKey.Remark | 备注,最长 250 个字符,便于区分多把公钥的归属,可选; | String | 否 |
RevokeKid | 吊销指定 kid 的公钥,不传表示不吊销;被吊销的公钥不会物理删除,仅标记为已吊销用于审计,可选; | String | 否 |
响应示例:
{"LibraryId": "smhxxx","NewKid": "lk-a1b2c3d4e5f6...","JWSConfig": { "Enabled": true, "Keys": [ "..." ] }}
响应字段说明:
节点名称(关键字) | 描述 | 类型 |
NewKid | 本次新增公钥的唯一标识,格式为 lk- + 32 位十六进制字符;未新增公钥时为空;
在多把公钥场景下可写入 assertion 的 kid 字段以精确指定验签公钥;不写 kid 时服务端会遍历所有活跃公钥依次尝试验签; | String |
JWSConfig | 修改后的完整配置,可用于确认当前活跃公钥列表; | Object |
说明:
单个媒体库支持同时登记多个活跃公钥,便于滚动轮换与应急替换。
步骤 3:构造 JWT Assertion
assertion 是一段标准 JWT,由
header.payload.signature 三段 base64url 字符串以英文句点(.)拼接而成。JWT Header
{"alg": "EdDSA","typ": "JWT","kid": "lk-a1b2c3d4e5f6..."}
字段说明:
字段名称 | 描述 | 类型 | 是否必选 |
alg | 签名算法,留空时默认为 EdDSA;显式传入时必须严格等于 EdDSA(大小写敏感)。传入 none、HS256、RS256 等其他算法一律拒绝,用于防御 JWT 算法混淆攻击; | String | 未明确(可留空) |
typ | 令牌类型,留空时默认为 JWT;显式传入时必须为 JWT; | String | 未明确(可留空) |
kid | 指定验签公钥,即步骤 2 返回的 NewKid,可选参数,最长 128 个字符且只能包含可打印 ASCII 字符。
指定时必须精确匹配到一把未吊销、未过期的公钥,否则返回 JWSKidMismatch;不指定时服务端遍历所有活跃公钥依次尝试验签; | String | 否 |
JWT Payload
字段名称 | 描述 | 类型 | 是否必选 |
iss | 签发者,必须等于 LibraryId | string | 是 |
aud | 受众,必须等于 LibraryId | string | 是 |
sub_type | 主体类型,取值 user / service / anonymous | string | 是 |
sub | 主体标识。 sub_type=user 时必选,作为用户 ID;其他类型可选,仅作标识 | string | 条件必选 |
scope | string | 条件必选 | |
space_id | 空间 ID。 sub_type=anonymous 时必选;user / service 时必须不传 | string | 条件必选 |
acl_entries | 内联 ACL 权限条目,仅当 sub_type=anonymous 且 scope 严格等于 inline_acl 时生效,其余情况静默忽略,详见下文说明 | string | 否 |
auto_create | 仅 sub_type=user 生效。为 true 时用户不存在则自动创建,默认 false | boolean | 否 |
jti | 防重放唯一标识,长度 16~128 个字符,建议使用 UUID | string | 是 |
exp | 过期时间,Unix 秒,必须为 JSON 整数字面量 | integer | 是 |
nbf | 生效时间,Unix 秒,必须为 JSON 整数字面量;不传视为当前时间 | integer | 否 |
client_id | string | 否 | |
session_id | 会话标识,按需透传 | string | 否 |
allow_space_tag | 允许的空间标签,按需透传 | string | 否 |
注意:
时间字段类型约束
exp / nbf / auto_create 必须使用正确的 JSON 类型。若传入字符串(如 "exp": "1716300300")或布尔值错位,服务端在 payload 反序列化阶段即失败,返回 400 ParamInvalid。sub_type 与其他字段的对应关系
sub_type | scope 取值 | sub | space_id | 最终身份 |
user | 应为 acl | 必选(用户 ID) | 必须不传 | 指定用户身份,权限由持久化 ACL 规则决定 |
service | 任意(服务端忽略) | 可选(仅作标识) | 必须不传 | 服务端强制以 admin(媒体库管理员)签发 |
anonymous | 留空为只读;可传逗号分隔的权限项;或严格等于 inline_acl | 可选 | 必选 | 匿名身份并绑定指定空间,适合分享 / 外链场景 |
对于
sub_type=anonymous,scope 会原样作为 Grant 处理,因此 生成访问令牌 文档中列出的所有权限项(upload_file、create_directory、delete_file 等)均可使用。注意:
acl_entries 仅在 sub_type=anonymous + scope 严格等于 inline_acl 时生效重要:细粒度权限项与管理员权限不等价
部分接口(如
/api/v1/user/* 下的用户与用户组管理接口)在实现上仅接受管理员权限,不接受任何细粒度权限项。例如携带 scope=upload_file,create_directory 的令牌调用查询用户列表接口,仍会返回 403 NoPermission。
需要调用此类接口时,请使用 sub_type=service(服务端强制签发管理员权限,并自动应用更严格的 5 分钟 assertion 生命周期约束)。space_id 与 sub_type 互斥约束
space_id 是仅属于 anonymous 场景的字段:sub_type | 携带 space_id | 服务端行为 |
anonymous | 必选 | 将令牌绑定到该空间;未传返回 400 ParamInvalid |
user | 禁止 | 返回 400 ParamInvalid |
service | 禁止 | 返回 400 ParamInvalid |
user / service 签出的令牌作用域由 ACL 规则或管理员权限决定,覆盖整个媒体库。若调用方误传 space_id(真实意图通常是「把令牌收窄到这一个空间」)而服务端静默丢弃,实际签出令牌的权限范围反而被放大,违反最小权限原则。因此服务端选择显式拒绝而非静默忽略。该校验发生在
jti 防重放占位之前,被拒绝时不会消耗 jti,修正 payload 后可立即重新签发。若确实需要将令牌限定到单个空间,请使用
sub_type=anonymous + space_id。assertion 生命周期限制
sub_type | exp - nbf 上限 | 建议值 |
service | 5 分钟 | 1~3 分钟 |
user / anonymous | 10 分钟 | 1~5 分钟 |
超过上限返回
JWSAssertionLifetimeTooLong;exp / nbf 的校验均允许 60 秒的客户端时钟偏移;service 的上限比其他类型更严格,因为它对应管理员权限,需要压缩泄露窗口。若沿用 user 场景的时间参数签发 service assertion 会被判定为超限。Payload 示例
指定用户身份:
{"iss": "smhxxx","aud": "smhxxx","sub_type": "user","sub": "user12345678","scope": "acl","auto_create": true,"jti": "550e8400-e29b-41d4-a716-446655440000","nbf": 1716300000,"exp": 1716300300}
服务管理员身份:
{"iss": "smhxxx","aud": "smhxxx","sub_type": "service","sub": "backend-service-A","jti": "660e8400-e29b-41d4-a716-446655440001","nbf": 1716300000,"exp": 1716300180}
匿名身份 + 空间内写权限:
{"iss": "smhxxx","aud": "smhxxx","sub_type": "anonymous","space_id": "spacexxx","scope": "upload_file,create_directory","jti": "770e8400-e29b-41d4-a716-446655440002","nbf": 1716300000,"exp": 1716300300}
匿名身份 + 内联 ACL 细粒度授权:
{"iss": "smhxxx","aud": "smhxxx","sub_type": "anonymous","space_id": "spacexxx","scope": "inline_acl","acl_entries": "Read:file/spacexxx/dir/a.txt:allow;Write:file/spacexxx/dir:allow","jti": "880e8400-e29b-41d4-a716-446655440003","nbf": 1716300000,"exp": 1716300600}
acl_entries 为分号分隔的权限三元组,每条格式为 <Action>:<Object>:<Effect>:格式项 | 描述 | 类型 |
Action | 大驼峰动作名(如 Read / Write / Overwrite),或 ag:<动作组 ID> 形式; | String |
Object | 必须以 file/ 或 fileId/ 为前缀; | String |
Effect | 只能为小写 allow 或 deny。 | String |
步骤 4:签名并组装 assertion
signing_input = base64url(header) + "." + base64url(payload)signature = Ed25519_Sign(privateKey, signing_input)assertion = signing_input + "." + base64url(signature)
注意:
注意事项:
base64url 编码不带填充,需去掉末尾的
=;Ed25519 内部自行对消息做哈希,不要预先对
signing_input 做 SHA256 再签名。步骤 5:换取访问令牌
请求示例
POST /api/v1/token?library_id={LibraryId}&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
请求参数 | 描述 | 类型 | 是否必选 |
LibraryId | 媒体库 ID,必选参数; | String | 是 |
grant_type | 固定为 urn:ietf:params:oauth:grant-type:jwt-bearer,必选参数; | String | 是 |
说明:
该接口仅支持 POST 方法,使用 GET 返回 405
MethodNotAllowed。携带该
grant_type 时不需要、也不应该传 library_secret。请求体
application/json
请求体示例:
{"appId": "smhxxx","assertion": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOi..."}
请求体字段说明:
字段名称 | 描述 | 类型 | 是否必选 |
appId | 必选参数,当前阶段必须等于 LibraryId,不一致返回 401 JWSAppNotFound。 | String | 是 |
assertion | 必选参数,步骤 4 组装的完整 JWT 断言。 | String | 是 |
响应码
签发成功,返回 HTTP 200 OK
响应体
application/json
响应体示例:
{ "accessToken": "acctk02xxxx", "expiresIn": 7200 }
响应体字段说明:
节点名称(关键字) | 描述 | 类型 |
accessToken | 字符串,访问令牌的具体值。 | String |
expiresIn | 整数,访问令牌的有效时长,单位为秒,当前固定为 7200(2 小时)。 | Integer |
说明:
assertion 中的
exp 只用于校验断言自身是否有效,不影响签出访问令牌的有效期。步骤 6:使用访问令牌调用业务接口
GET /api/v1/file/{LibraryId}/{SpaceId}/{FilePath}?access_token={AccessToken}
令牌续期
POST /api/v1/token/{LibraryId}/{AccessToken}
续期后返回原令牌值与新的有效时长(7200秒)。也可以直接用新的
jti 重新走步骤 3~5 签发一个新令牌,两种方式均可。错误码
HTTP Status | 错误码 | 触发条件 | 处理建议 |
400 | ParamInvalid | 请求体格式错误;缺少 appId / assertion;assertion 结构不是 3 段或 base64url 解码失败;payload JSON 反序列化失败(含 exp / nbf / auto_create 类型错误);header 含非法字段或算法非 EdDSA、typ 非 JWT;iss / aud 不匹配;缺少 exp;jti 缺失或长度不在 16~128;sub_type 缺失或非法;sub_type=user 时缺少 sub;sub_type=user / service 携带了 space_id;sub_type=anonymous 缺少 space_id | 按本文档核对参数与字段类型 |
401 | JWSAppNotFound | appId 与 URL 中的 library_id 不一致 | 确认两者相同 |
401 | JWSConfigDisabled | 媒体库未开启 JWS 鉴权 | 通过 ModifyLibraryJWSConfig 设置 Enabled=true |
401 | JWSPublicKeyNotConfigured | 媒体库未登记公钥;或所有公钥均已吊销 / 过期;或公钥内容无法解析为 Ed25519 公钥 | 登记有效公钥 |
401 | JWSKidMismatch | assertion 中的 kid 未匹配到任何活跃公钥 | 核对 kid,或不传 kid 让服务端遍历 |
401 | JWSSignatureVerificationFailed | 签名校验失败;或签名长度不是 64 字节 | 确认所用私钥与已登记公钥配对,确认未对 signing_input 预哈希 |
401 | JWSAssertionExpired | 当前时间已超过 exp,或尚未到达 nbf(容忍 60 秒偏移) | 校准服务器时钟(NTP),重新签发 |
401 | JWSAssertionTimestampInvalid | exp / nbf 是合法整数但取值不在有效 Unix 秒区间(如误传 13 位毫秒时间戳、负值) | 使用 10 位 Unix 秒时间戳 |
401 | JWSAssertionLifetimeTooLong | exp - nbf 超过上限 | service 不超过 5 分钟,其他不超过 10 分钟 |
401 | JWSJtiReplayed | 同一 jti 被重复使用 | 每次签发使用全新 jti,推荐 UUID |
403 | NoPermission | 令牌已成功签发,但 scope 授予的权限不满足目标接口要求 | 核对目标接口所需权限;管理员类接口需使用 sub_type=service |
404 | UserNotFound | sub_type=user 且用户不存在、auto_create 为 false | 设置 auto_create=true 或提前创建用户 |
500 | JWSJtiStoreUnavailable | 防重放存储不可用 | 服务端采用 fail-closed 策略拒绝请求;稍后重试,持续失败请联系运维 |