Permission API
Permission API 是一个基于角色的访问控制(RBAC)服务,允许第三方应用管理用户角色和权限。你可以定义多个角色(Role),每个角色包含允许(Allow)和拒绝(Deny)两组权限标识,然后将角色分配给用户。多个角色之间通过优先级(Priority)进行合并,高优先级角色的决定覆盖低优先级角色。
概览
| 项目 | 说明 |
|---|---|
| 基础路径 | /permission/api/ |
| 认证方式 | Basic(应用模式)或 Bearer(用户模式) |
| 数据格式 | JSON |
| 角色数量上限 | 每个应用最多 50 个 |
| 权限数量上限 | 每个角色 allow + deny 合计最多 200 条 |
认证
Permission API 支持两种认证模式,通过 Authorization 请求头区分:
应用模式(Basic)
使用应用的 client_id 和 client_secret 进行认证。应用模式可以执行所有角色和用户管理操作。
Authorization: Basic base64(client_id:client_secret)⚠️ 安全提示:
client_secret拥有应用的完整管理权限。绝对不要将其写入分发给用户的客户端程序,否则密钥可被逆向提取。应用模式仅应在受信任的服务器环境或内部开发工具中使用。推荐架构:面向用户的客户端应使用 Bearer 模式(用户 OIDC 登录),配合云函数和权限组实现业务逻辑。
用户模式(Bearer)
使用用户的 OAuth access_token 进行认证。用户模式下,可执行的操作取决于角色的可见性(Visibility)设置——详见下文「可见性与公开操作」章节。
Authorization: Bearer <access_token>🔒 访问说明:用户模式的访问范围取决于角色的公开设置。角色默认为私有,用户无法通过 API 直接访问。如需客户端直接调用,请在开发者面板中开启对应的公开访问开关。推荐统一使用云函数处理权限逻辑。
应用模式(Basic 认证)不受可见性限制,始终可以执行所有操作。
重放防护
所有 API 请求必须携带以下请求头以防止重放攻击:
| 请求头 | 说明 |
|---|---|
X-Timestamp | 当前 Unix 时间戳(秒),与服务器时差不超过 ±5 分钟 |
X-Nonce | 随机唯一字符串,16~64 个字母数字字符,每次请求不同 |
核心概念
角色(Role)
角色是权限的集合。每个角色有以下属性:
| 属性 | 类型 | 说明 |
|---|---|---|
name | string | 角色名称,在应用内唯一 |
description | string | 角色描述 |
allow | string[] | 允许的权限标识列表 |
deny | string[] | 拒绝的权限标识列表 |
priority | number | 优先级,数值越大优先级越高,用于多角色合并时的冲突解决 |
visibility | number | 可见性位掩码(详见下文) |
publicMemberOp | string | 公开成员操作范围:add / remove / all |
publicPermOp | string | 公开权限操作范围:add / remove / all |
权限标识(Permission)
权限标识是自定义字符串,格式由你的应用自行约定(建议使用 resource.action 的形式,如 user.read、post.write)。
Allow / Deny 模型
每个角色可以同时定义 allow(允许)和 deny(拒绝)权限。当用户拥有多个角色时,系统使用基于优先级的合并算法来计算最终权限:
- 将用户的所有角色按
priority从高到低排序 - 对于每一个权限标识,第一个声明它的角色(无论是在 allow 还是 deny 中)拥有最终决定权
- 低优先级角色对同一权限的声明会被忽略
示例:
Role A (priority: 10)
allow: ["read", "write"]
deny: ["delete"]
Role B (priority: 5)
allow: ["delete", "admin"]
deny: ["write"]合并结果:
read→ 允许(Role A allow,优先级 10)write→ 允许(Role A allow,优先级 10;Role B deny 被忽略)delete→ 拒绝(Role A deny,优先级 10;Role B allow 被忽略)admin→ 允许(仅 Role B 声明,优先级 5)
检查权限时: 先检查合并后的 deny 列表(命中则拒绝),再检查 allow 列表(命中则允许),都未命中则拒绝。
可见性与公开操作
角色默认是私有的——只有应用模式(Basic)可以操作。通过设置 visibility 位掩码,可以逐级开放给用户模式(Bearer):
公开权限查询开关
除了角色级别的可见性控制外,还有一个应用级别的总开关控制权限查询类 API 的访问:
| 开关 | 说明 |
|---|---|
| 公开权限查询 | 控制用户模式(Bearer)是否可以调用「获取合并权限」和「检查权限」接口 |
- 默认关闭:用户模式无法调用权限查询接口,仅可通过云函数
$permission.checkPermission()/$permission.getUserPermissions()访问 - 开启后:用户模式可以直接调用
GET /users/{uid}/permissions和GET /users/{uid}/check
💡 推荐做法:统一使用云函数处理权限验证逻辑,仅在确实需要客户端直接查询时开启此开关。
角色可见性位掩码
| 位 | 值 | 含义 |
|---|---|---|
VIS_PUBLIC | 1 | 公开角色:用户模式可以查看(列出、查询详情) |
VIS_EDIT_MEMBER | 2 | 公开编辑成员:用户模式可以分配/移除用户(受 publicMemberOp 限制) |
VIS_EDIT_PERM | 4 | 公开编辑权限:用户模式可以修改角色的 allow/deny(受 publicPermOp 限制) |
visibility 是位掩码的组合值:
| 值 | 含义 |
|---|---|
0 | 完全私有(默认) |
1 | 公开只读 |
3 | 公开 + 可编辑成员 |
7 | 公开 + 可编辑成员 + 可编辑权限 |
publicMemberOp 和 publicPermOp 用于进一步细化公开操作的范围:
| 值 | 含义 |
|---|---|
add | 仅允许添加操作 |
remove | 仅允许移除操作 |
all | 允许添加和移除操作 |
角色管理
角色通过开发者管理面板创建和管理。如需在运行时动态控制角色的分配和权限,推荐使用 Cloud Function 中的 $permission 桥接对象。
API 接口
列出所有角色
GET /permission/api/roles用户模式下仅返回
visibility包含VIS_PUBLIC的角色。
响应:
{
"status": "ok",
"roles": [
{
"name": "admin",
"description": "管理员",
"allow": ["user.read", "user.write", "post.manage"],
"deny": [],
"priority": 100,
"visibility": 0,
"publicMemberOp": "all",
"publicPermOp": "all"
},
{
"name": "member",
"description": "普通成员",
"allow": ["user.read", "post.read"],
"deny": ["post.delete"],
"priority": 0,
"visibility": 3,
"publicMemberOp": "add",
"publicPermOp": "all"
}
]
}获取角色详情
GET /permission/api/roles/{roleName}用户模式下角色必须公开(
visibility & 1)。
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
roleName | string | 角色名称 |
响应:
{
"status": "ok",
"role": {
"name": "admin",
"description": "管理员",
"allow": ["user.read", "user.write", "post.manage"],
"deny": [],
"priority": 100,
"visibility": 0,
"publicMemberOp": "all",
"publicPermOp": "all"
}
}列出角色成员
GET /permission/api/roles/{roleName}/members?page=1&limit=100用户模式下角色必须公开(
visibility & 1)。应用模式会返回邮箱、激活状态和分配者信息;用户模式只返回uid、rid和分配时间,避免泄露管理字段。
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
roleName | string | 角色名称 |
查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
page | number | 页码,默认 1 |
limit | number | 每页数量,默认 100,最大 200 |
响应:
{
"status": "ok",
"roleName": "member",
"total": 2,
"page": 1,
"limit": 100,
"members": [
{
"uid": "94f48446-57ab-11f1-aa60-00163e15156d",
"rid": "SmallMoonSet",
"email": "user@example.com",
"authState": 1,
"assignedByUid": null,
"assignedByRid": null,
"assignedAt": "1780219728000"
}
]
}获取用户的角色列表
GET /permission/api/users/{uid}/roles用户模式下仅返回公开角色。
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
uid | string | 用户 uid |
响应:
{
"status": "ok",
"roles": [
{
"name": "member",
"description": "普通成员",
"allow": ["user.read", "post.read"],
"deny": ["post.delete"],
"priority": 0,
"visibility": 3,
"assignedBy": "app",
"assignedAt": "2025-01-15T08:30:00.000Z"
}
]
}用户模式下额外返回
visibility、assignedBy(分配来源)、assignedAt(分配时间)字段。
获取用户的合并权限
返回用户所有角色经过优先级合并后的最终 allow 和 deny 列表。
🔒 访问控制:用户模式(Bearer)调用此接口需要开启「公开权限查询」开关。未开启时仅可通过云函数
$permission.getUserPermissions(uid)访问。
GET /permission/api/users/{uid}/permissions路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
uid | string | 用户 uid |
响应:
{
"status": "ok",
"allow": ["user.read", "user.write", "post.manage", "post.read"],
"deny": ["post.delete"]
}检查用户是否拥有指定权限
检查逻辑:合并后 deny 命中 → false,allow 命中 → true,都未命中 → false。
🔒 访问控制:用户模式(Bearer)调用此接口需要开启「公开权限查询」开关。未开启时仅可通过云函数
$permission.checkPermission(uid, permission)访问。
GET /permission/api/users/{uid}/check?permission={permission}路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
uid | string | 用户 uid |
查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
permission | string | 要检查的权限标识 |
响应:
{
"status": "ok",
"hasPermission": true
}给用户分配角色
用户模式下,角色必须设置
visibility & VIS_EDIT_MEMBER,且publicMemberOp为add或all。
POST /permission/api/users/{uid}/roles
Content-Type: application/json路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
uid | string | 用户 uid |
请求体:
{
"roleName": "member"
}响应:
{
"status": "ok"
}移除用户角色
用户模式下,角色必须设置
visibility & VIS_EDIT_MEMBER,且publicMemberOp为remove或all。
DELETE /permission/api/users/{uid}/roles/{roleName}路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
uid | string | 用户 uid |
roleName | string | 角色名称 |
响应:
{
"status": "ok"
}更新角色权限
用户模式下,角色必须设置
visibility & VIS_EDIT_PERM,且publicPermOp控制可执行的操作(添加/移除权限条目)。
PUT /permission/api/roles/{roleName}/permissions
Content-Type: application/json路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
roleName | string | 角色名称 |
请求体:
{
"allow": ["user.read", "post.read", "post.write"],
"deny": ["post.delete"]
}响应:
{
"status": "ok"
}错误响应
所有错误响应遵循统一格式:
{
"error": "error_code",
"error_description": "Human-readable error message"
}常见错误码:
| 错误码 | HTTP 状态码 | 说明 |
|---|---|---|
unauthorized | 401 | 缺少 Authorization 请求头 |
invalid_client | 401 | 无效的 Basic 凭据 |
invalid_token | 401 | 无效的 Bearer token |
forbidden | 403 | 用户模式下角色不可见或操作不允许 |
service_not_enabled | 403 | 应用未开通权限组服务 |
not_found | 404 | 角色不存在或用户未分配此角色 |
duplicate | 400 | 用户已拥有此角色 |
limit_exceeded | 400 | allow + deny 合计超过 200 条上限 |
使用示例
典型游戏权限设计
角色:admin (priority: 100, visibility: 0)
allow: player.ban, player.kick, inventory.manage, chat.moderate
deny: (无)
角色:moderator (priority: 50, visibility: 1)
allow: player.kick, chat.moderate
deny: player.ban
角色:muted (priority: 80, visibility: 3, publicMemberOp: add)
allow: (无)
deny: chat.send, chat.color
角色:vip (priority: 10, visibility: 1)
allow: chat.color, inventory.extra_slots
deny: (无)如果一个用户同时拥有 muted(priority 80)和 vip(priority 10)角色:
chat.color→ 拒绝(muted deny 优先级 80 > vip allow 优先级 10)inventory.extra_slots→ 允许(仅 vip 声明)chat.send→ 拒绝(muted deny)
在服务端检查权限
// 检查玩家是否有封禁权限
const res = await fetch(
`${API_BASE}/permission/api/users/${playerUid}/check?permission=player.ban`,
{ headers: { Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}` } }
)
const { hasPermission } = await res.json()
if (!hasPermission) {
throw new Error('You do not have permission to ban players')
}在云函数中使用 $permission
// 云函数内通过 $permission 桥接对象操作权限
const roles = await $permission.getUserRoles(uid)
const perms = await $permission.getUserPermissions(uid)
const canBan = await $permission.checkPermission(uid, 'player.ban')
// 更新角色权限(需要应用有权操作该角色)
await $permission.updateRolePermissions('moderator', {
allow: ['player.kick', 'chat.moderate', 'chat.pin'],
deny: ['player.ban']
})