帮你快速理解、总结文档立即下载

使用 JWT 签发访问令牌

最近更新时间:2026-10-27 17:33:38
本文档已由 AI 辅助审校
我的收藏

功能描述

用于通过 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
权限范围,语义与 生成访问令牌 的 Grant 参数完全一致,支持英文逗号分隔多个值。不同 sub_type 的取值约定见下表
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
客户端标识,语义同 生成访问令牌 的 ClientId,按需透传
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:使用访问令牌调用业务接口

获取到 accessToken 后,使用方式与 生成访问令牌 签发的令牌完全一致:
GET /api/v1/file/{LibraryId}/{SpaceId}/{FilePath}?access_token={AccessToken}

令牌续期

JWT Bearer 方式签发的访问令牌支持 访问令牌续期 接口:
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 策略拒绝请求;稍后重试,持续失败请联系运维