Skip to content

认证与 RBAC

本文档涵盖 Umoo 完整的认证与授权系统——从 JWT Token 结构与生命周期、服务端中间件链,到 Web UI 如何利用已解析的权限控制组件可见性。


目录

  1. 概述
  2. JWT Token 结构与生命周期
  3. RBAC 数据模型
  4. 系统角色
  5. 权限目录
  6. 角色 → 权限矩阵
  7. 服务端执行
  8. 权限解析与缓存
  9. Web UI 集成指南

1. 概述

Umoo 使用多租户 RBAC 模型。一个用户账号可以属于多个租户,并在每个租户中拥有不同的角色。权限根据调用方所在租户按请求进行解析。

关键设计决策:

  • Token 仅携带 user_id — JWT 中不嵌入租户、角色或权限信息。
  • 租户上下文为请求级 — 通过 X-Tenant-ID HTTP 请求头提供。
  • 权限在进程内缓存 — 60 秒 TTL,角色变更时失效。

2. JWT Token 结构与生命周期

Token Claims

json
{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "iat": 1700000000,
  "exp": 1700003600
}

Token 中不嵌入租户、角色或权限。 所有授权均在服务端通过 X-Tenant-ID 请求头按请求解析。

算法

默认 RS256(可配置为 RS512)。使用 RSA 私钥签名,使用对应公钥验证。

Token 生命周期

POST /api/v1/auth/login
  Body: { "email": "...", "password": "..." }

  ← 200: {
       "access_token":  "<jwt>",
       "refresh_token": "<opaque>",
       "expires_in":    3600
     }
POST /api/v1/auth/refresh
  Body: { "refresh_token": "..." }

  ← 200: { "access_token": "<new jwt>", "expires_in": 3600 }

Access Token 是短期有效的 JWT。Refresh Token 是存储在数据库(jwt_tokens 表)中的不透明字符串,用于在无需重新认证的情况下签发新的 Access Token。

发送请求

每个经过认证的 API 调用需要两个请求头:

http
Authorization: Bearer <access_token>
X-Tenant-ID: <tenant-uuid>

缺少任一请求头将返回 401 Unauthorized403 Forbidden


3. RBAC 数据模型

users
  └── user_tenant_roles
        ├── user_id    → users.id
        ├── tenant_id  → tenants.id  (NULL = 系统级 / super_admin)
        └── role_id    → roles.id
              └── role_permissions
                    └── permission_id → permissions.id
                                            ├── resource  (例如 "device")
                                            └── action    (例如 "read")

关键约束

  • 一个用户在每个租户中最多只有一个角色UNIQUE NULLS NOT DISTINCT (user_id, tenant_id))。
  • user_tenant_roles.tenant_id = NULL → 该用户是超级管理员,无租户边界限制。
  • 系统角色(is_system = true)不可删除,在所有租户间共享(roles.tenant_id = NULL)。
  • 租户可创建自定义角色(roles.tenant_id = <tenant-uuid>)。

4. 系统角色

迁移时预置了四个内置系统角色,不可修改。

角色说明
super_admin系统级管理员。绕过所有租户检查,不属于任何租户。
tenant_admin租户内完全控制——用户、设备、插件、发布。
operator设备运维——可读写设备、舰队、影子、发布;可读插件。
viewer设备、舰队、影子、发布和插件的只读访问。

5. 权限目录

5.1 平台权限

平台内置的所有 resource:action 对:

资源操作说明
tenantadmin租户级管理操作(创建/删除租户)
userread列出并查看租户内用户
userwrite创建和更新用户,分配角色
userdelete从租户中移除用户
deviceread列出并查看设备详情
devicewrite注册、更新和配置设备
devicedelete注销/移除设备
fleetread查看设备分组
fleetwrite创建和管理设备分组
shadowread查看设备期望/上报状态
shadowwrite更新设备期望状态
rolloutread查看发布任务和状态
rolloutwrite创建和管理发布任务
pluginread查看已安装插件和配置
pluginwrite安装和更新插件
plugindelete移除插件
claimwrite生成设备认领 Token
auditread读取不可变审计日志
apikeyread列出 API 密钥
apikeywrite创建和撤销 API 密钥
eventread读取租户事件流
eventwrite发送租户事件
networkadmin管理 WireGuard 子网和 VPN 对等节点
metricsread通过代理查询 Prometheus 指标

5.2 插件权限

插件权限以 {plugin-id}/{subresource} 格式命名,在启动时由 SDKPluginManager.Init() 自动写入。它们存储在同一 permissions 表中,由同一 PermissionCache 执行。

资源操作默认角色说明
terminal/sessionreadviewer, operator, tenant_admin列出活跃终端会话
terminal/sessionopenoperator, tenant_admin向设备开启 WebSocket Shell
wireguard/networkreadviewer, operator, tenant_admin查看 WireGuard 子网
wireguard/networkcreatetenant_admin创建 WireGuard 子网
wireguard/networkdeletetenant_admin删除 WireGuard 子网
wireguard/peerreadviewer, operator, tenant_admin查看 VPN 对等节点
wireguard/peeraddoperator, tenant_admin添加 VPN 对等节点
wireguard/peerremoveoperator, tenant_admin移除 VPN 对等节点
wireguard/relaymanagetenant_admin配置中继服务器
wireguard/device_configpushoperator, tenant_admin向设备推送 WireGuard 配置
telemetry/metricsreadviewer, operator, tenant_admin查询设备遥测数据
logs/streamreadviewer, operator, tenant_admin查询和流式传输设备日志

6. 角色 → 权限矩阵

6.1 平台权限

权限super_admintenant_adminoperatorviewer
tenant:admin
user:read
user:write
user:delete
device:read
device:write
device:delete
fleet:read
fleet:write
shadow:read
shadow:write
rollout:read
rollout:write
plugin:read
plugin:write
plugin:delete
claim:write
audit:read
apikey:read
apikey:write
event:read
event:write
network:admin
metrics:read

6.2 插件权限

权限super_admintenant_adminoperatorviewer
terminal/session:read
terminal/session:open
wireguard/network:read
wireguard/network:create
wireguard/network:delete
wireguard/peer:read
wireguard/peer:add
wireguard/peer:remove
wireguard/relay:manage
wireguard/device_config:push
telemetry/metrics:read
logs/stream:read

7. 服务端执行

中间件链

每个管理 API 请求按顺序经过以下链路:

RequestLogging
  → RateLimitMiddleware
    → AuthMiddleware
      → TenantAuthMiddleware
        → [路由处理器]

         RequirePermission("resource", "action")  ← 可选的每路由包装器

1. AuthMiddlewareports/middleware/auth.go

  • 提取 Authorization: Bearer <token> 请求头。
  • 使用 RSA 公钥验证 JWT 签名和过期时间。
  • 从 Claims 中解析 user_id
  • user_id 存入 UserIDKey 的请求上下文。
  • Token 缺失、无效或过期时返回 401

2. TenantAuthMiddlewareports/middleware/tenant_auth.go

  • 读取 X-Tenant-ID 请求头。
  • 检查用户是否为 super_adminuser_tenant_roles 中 tenant_id = NULL)——若是,设置 IsSuperAdmin = true 并跳过所有权限检查。
  • 非超级管理员:从 PermissionCache 解析 UserTenantPermissions(见第 8 节)。
  • TenantIDKeyPermissionsKey 存入请求上下文。
  • 用户在请求的租户中无成员资格时返回 403

3. RequirePermission(resource, action)ports/middleware/tenant_auth.go

应用于特定处理器的每路由中间件包装器:

go
mux.Handle("/grpc.umoo.v1.admin.AdminService/DeleteDevice",
    RequirePermission("device", "delete")(adminHandler))
  • 从上下文读取 PermissionsKey
  • 对已解析的 UserTenantPermissions 调用 HasPermission(resource, action)
  • 超级管理员始终通过(IsSuperAdmin = true 绕过检查)。
  • 权限不存在时返回 403

8. 权限解析与缓存

PermissionCachemodules/auth/permission_cache.go

"userID:tenantID"(超级管理员为 "userID:system")为键的进程内缓存。

  • TTL:60 秒
  • 缓存未命中:通过 ACLRepository 查询数据库——跨 user_tenant_roles → roles → role_permissions → permissions 的 JOIN
  • 失效PermissionCache.Invalidate(userID) — 用户角色变更时调用

UserTenantPermissionsdomain/permission.go

每个请求存储在上下文中的已解析对象:

go
type UserTenantPermissions struct {
    UserID      uuid.UUID
    TenantID    uuid.UUID
    Role        string       // 例如 "operator"
    Permissions []Permission // 所有授予的 resource:action 对
    IsSuperAdmin bool
}

func (p *UserTenantPermissions) HasPermission(resource, action string) bool {
    if p.IsSuperAdmin {
        return true
    }
    for _, perm := range p.Permissions {
        if perm.Resource == resource && perm.Action == action {
            return true
        }
    }
    return false
}

9. Web UI 集成指南

步骤 1:登录

http
POST /api/v1/auth/login
Content-Type: application/json

{ "email": "alice@example.com", "password": "secret" }

存储返回的 access_tokenrefresh_token。Access Token 是 JWT,但不要解码它来获取权限——它只包含 user_id

步骤 2:获取租户成员资格

登录后立即(在渲染任何受保护 UI 之前)调用:

http
GET /api/v1/auth/me/tenants
Authorization: Bearer <access_token>

响应:

json
[
  {
    "tenant_id": "550e8400-...",
    "tenant_name": "Acme Corp",
    "role_name": "operator",
    "permissions": [
      { "resource": "device", "action": "read" },
      { "resource": "device", "action": "write" },
      { "resource": "fleet",  "action": "read" },
      { "resource": "fleet",  "action": "write" },
      { "resource": "shadow", "action": "read" },
      { "resource": "shadow", "action": "write" },
      { "resource": "rollout","action": "read" },
      { "resource": "rollout","action": "write" },
      { "resource": "plugin", "action": "read" }
    ],
    "is_super_admin": false
  }
]

步骤 3:在前端状态中存储权限

typescript
interface Permission {
  resource: string;
  action: string;
}

interface TenantMembership {
  tenant_id: string;
  tenant_name: string;
  role_name: string;
  permissions: Permission[];
  is_super_admin: boolean;
}

// 构建 Set 以实现 O(1) 查找
function buildPermissionSet(membership: TenantMembership): Set<string> {
  if (membership.is_super_admin) {
    return new Set(['*']); // 超级管理员拥有一切权限
  }
  return new Set(
    membership.permissions.map(p => `${p.resource}:${p.action}`)
  );
}

function can(permSet: Set<string>, resource: string, action: string): boolean {
  return permSet.has('*') || permSet.has(`${resource}:${action}`);
}

步骤 4:控制 UI 组件可见性

使用 can() 辅助函数(或等效的 React context / hook)条件性渲染组件:

tsx
// 仅当用户有 device:delete 权限时渲染"删除设备"按钮
{can(perms, 'device', 'delete') && (
  <Button onClick={handleDeleteDevice}>删除设备</Button>
)}

// 仅对 tenant_admin 及以上角色显示用户管理区域
{can(perms, 'user', 'read') && <UserManagementPanel />}

// 仅对 tenant_admin+ 显示插件管理
{can(perms, 'plugin', 'write') && <InstallPluginButton />}

// 仅当用户可认领时显示认领设备流程
{can(perms, 'claim', 'write') && <ClaimDeviceWizard />}

步骤 5:切换租户

用户切换租户时,所有后续 API 调用需包含 X-Tenant-ID

http
GET /grpc.umoo.v1.admin.AdminService/ListDevices
Authorization: Bearer <access_token>
X-Tenant-ID: <selected-tenant-id>

从已存储的成员资格列表中重新派生所选租户的权限集——无需额外 API 调用(步骤 2 已获取所有租户成员资格)。

组件可见性参考

UI 组件 / 操作所需权限
查看设备列表device:read
注册/编辑设备device:write
删除设备device:delete
查看设备分组fleet:read
创建/编辑设备分组fleet:write
查看设备影子(期望/上报状态)shadow:read
编辑设备影子(期望状态)shadow:write
查看发布任务rollout:read
创建/管理发布rollout:write
查看已安装插件plugin:read
安装/更新插件plugin:write
移除插件plugin:delete
认领未认领设备claim:write
查看租户内用户user:read
邀请/编辑用户user:write
移除用户user:delete
租户管理tenant:admin(仅超级管理员)
查看审计日志audit:read
查看/创建 API 密钥apikey:read + apikey:write
查看事件event:read
创建事件event:write
查看/管理 WireGuard 网络network:admin
查看监控仪表板metrics:read
查看活跃终端会话terminal/session:read
打开设备终端terminal/session:open
查看 WireGuard 子网wireguard/network:read
创建/删除 WireGuard 子网wireguard/network:create / wireguard/network:delete
查看 VPN 对等节点wireguard/peer:read
添加/移除 VPN 对等节点wireguard/peer:add / wireguard/peer:remove
查看设备遥测telemetry/metrics:read
查看/流式传输设备日志logs/stream:read

Token 刷新

Access Token 会过期(默认 1 小时)。静默刷新:

typescript
async function refreshAccessToken(refreshToken: string): Promise<string> {
  const res = await fetch('/api/v1/auth/refresh', {
    method: 'POST',
    body: JSON.stringify({ refresh_token: refreshToken }),
  });
  const { access_token } = await res.json();
  return access_token;
}

收到任意 API 调用的 401 响应时,刷新 Token 并重试一次。


附录:超级管理员行为

超级管理员(role = "super_admin")无租户边界:

  • 数据库中 user_tenant_roles.tenant_id = NULL
  • /me/tenants 响应的每条记录均返回 is_super_admin: true
  • 服务端:IsSuperAdmin = true 完全绕过所有 RequirePermission 检查。
  • UI:将 is_super_admin: true 视为拥有所有权限。显示所有控件,包括跨租户管理面板。

Umoo — IoT Device Management Platform