← 返回 Notes 首页

RBAC 请求链路与单权限测试(实战版)

目标:把你关心的三件事讲清楚 显式 RBAC AutoGuard 单权限角色对比测试 + 清理

显式 RBAC 是什么

在具体路由上明确挂权限中间件,直接声明 permission + level + scope,例如:

requireAnyPermission(
  ['reports:schedule', 'admin:manage'],
  { requiredLevel: 'manage', getCenterId: resolveScopeCenterId }
)

AutoGuard 是什么

authenticateToken() 内做的全局兜底守卫:按 method + path 从映射表解析权限规则,没映射或没权限直接 403

resolveApiPermissionRule(path, method)
=> { permissions, requiredLevel, ruleId }
二者关系:AutoGuard 负责“所有非公开 API 都不能裸奔”;显式 RBAC 负责“具体业务路由可以更严格”。

0) 设计方式与取舍(当前三层过渡 -> 目标两层)

AutoGuard 现在在哪一层实现

方案 优点 代价 / 风险 适用阶段
AutoGuard 兜底(映射表) 全局兜底快,能防止“漏挂显式 RBAC”导致裸奔;默认 fail-closed。 规则来源偏隐式,调试时需回看映射表;与认证中间件耦合较高。 迁移期的安全基线
显式 RBAC(路由级) 权限要求可读、可审计、可按业务精细化(permission/level/scope 都写在路由处)。 依赖团队纪律;若漏挂中间件,需要额外 gate 阻止回归。 长期主路径
当前三层过渡(JWT + AutoGuard + 显式RBAC) 安全性高,兼容历史路由,能边迁移边运行。 存在双重权限判断,链路更长,维护复杂度更高。 当前状态
目标两层(JWT + 显式RBAC) 职责清晰,鉴权语义统一,代码和测试更直接。 前提是显式覆盖率与 CI 门禁足够强,否则会有漏检风险。 迁移完成后

为什么不能一步“全量切两层”

结论:先完成“显式化 + 测试收口 + CI 门禁收口”,再下线 AutoGuard,风险最低。

迁移完成后的两层形态(目标)

/api 非公开请求
  -> authenticateToken()   // 只做身份认证,不做权限决策
  -> requirePermission*()  // 每条业务路由显式声明权限
  -> handler

1) 请求在项目里的完整处理链路

sequenceDiagram
  autonumber
  participant C as Client
  participant S as Express /api 入口
  participant A as authenticateToken()
  participant G as AutoGuard(映射表)
  participant R as 路由显式RBAC
  participant H as Handler

  C->>S: 请求 /api/xxx
  S->>S: isPublicApiPath?
  alt 公开接口
    S-->>H: 直接放行(不做权限)
  else 非公开接口
    S->>A: 校验 JWT
    alt token 无效
      A-->>C: 401
    else token 有效
      A->>G: resolveApiPermissionRule(path, method)
      alt 未命中映射
        G-->>C: 403 (missing_permission_mapping)
      else 命中映射
        G->>G: hasAnyPermission(user, permissions, {requiredLevel, centerId})
        alt 权限不足
          G-->>C: 403
        else 通过
          G-->>R: 进入具体路由
          alt 路由有显式RBAC
            R->>R: requirePermission/requireAnyPermission
            alt 显式RBAC失败
              R-->>C: 403
            else 显式RBAC通过
              R-->>H: 执行业务
            end
          else 路由无显式RBAC(历史路由)
            R-->>H: 依赖 AutoGuard 基线放行
          end
        end
      end
    end
  end
      

2) permission / level / scope 在哪里决定

维度 AutoGuard(全局) 显式 RBAC(路由级)
permission backend/config/api-permission-map.js 按 path+method 显式映射 在路由中间件中直接写,如 requireAnyPermission([...])
level 默认 GET=view / POST=edit / DELETE=manage,可规则覆写 显式传 requiredLevel,可比全局更严格
scope 从 params/body/query/header 提取 centerId 通过 getCenterIdrequirePermissionWithScope 指定
flowchart TD
  A["进入 /api 请求"] --> B{"公开接口?"}
  B -- 是 --> P["跳过权限系统(走公开安全边界)"]
  B -- 否 --> C["authenticateToken()"]
  C --> D{"JWT 有效?"}
  D -- 否 --> E["401 Unauthorized"]
  D -- 是 --> F["resolveApiPermissionRule(path, method)"]
  F --> G{"规则存在?"}
  G -- 否 --> H["403 Missing Mapping (Fail Closed)"]
  G -- 是 --> I["检查 permissions + requiredLevel + centerId"]
  I --> J{"AutoGuard 通过?"}
  J -- 否 --> K["403 Forbidden"]
  J -- 是 --> L{"有显式RBAC中间件?"}
  L -- 否 --> M["进入 Handler(历史路由)"]
  L -- 是 --> N["执行显式RBAC (可更严格)"]
  N --> O{"显式RBAC通过?"}
  O -- 否 --> Q["403 Forbidden"]
  O -- 是 --> R["进入 Handler"]

  style H fill:#542333,stroke:#ff8ea3,color:#ffe8ee
  style K fill:#542333,stroke:#ff8ea3,color:#ffe8ee
  style E fill:#53311d,stroke:#ffbc6a,color:#fff3e2
  style R fill:#163925,stroke:#6de4a5,color:#dcffe9
  style M fill:#2c2f4d,stroke:#8ea8ff,color:#e6ebff
      

3) 真实例子(建议你从这个端点开始观察)

例子:GET /api/reports/schedules/stats

用户权限 AutoGuard 显式RBAC 最终结果
只有 reports:view_schedules (view) 通过 拒绝 403
reports:schedule (manage) 通过 通过 进入 handler

这就是“显式 RBAC 比 AutoGuard 更细、更严”的典型场景。

4) 你提的测试方法:单权限角色对照 + 清理

测试目标

sequenceDiagram
  autonumber
  participant T as Jest Test
  participant H as rbac-test-helper
  participant DB as tenant_schema
  participant API as Protected API

  T->>H: createPermissionComparisonActors("employees:view")
  H->>DB: 创建允许角色(单权限) + 禁止角色(0权限) + 两个临时员工
  H-->>T: allowedToken + deniedToken + cleanup()

  T->>API: deniedToken 请求同一端点
  API-->>T: 403 (预期)

  T->>API: allowedToken 请求同一端点
  API-->>T: !=403 (预期)

  T->>H: cleanup()
  H->>DB: 删除 employee_roles / role_permissions / employees / roles
  H->>DB: verifyActorCleanup()
  DB-->>T: 残留计数全 0
      

当前落地的测试文件

文件 作用
backend/tests/integration/rbac/permission-comparison.test.js “有权限 vs 无权限”对照断言
backend/tests/integration/rbac/cleanup-verification.test.js 验证 cleanup 后 roles/role_permissions/employee_roles/employees 无残留
backend/tests/helpers/rbac-test-helper.js 创建单权限临时角色、生成 token、严格清理与残留校验

推荐跑法

cd backend
npm run test -- tests/integration/rbac/permission-comparison.test.js
npm run test -- tests/integration/rbac/cleanup-verification.test.js

5) 一句话记忆

AutoGuard 解决“有没有配权限规则”,显式 RBAC 解决“这条路由到底该多严格”,单权限对照测试 解决“规则是否真实生效且可回收无污染”。