目标:把你关心的三件事讲清楚 显式 RBAC AutoGuard 单权限角色对比测试 + 清理
在具体路由上明确挂权限中间件,直接声明 permission + level + scope,例如:
requireAnyPermission(
['reports:schedule', 'admin:manage'],
{ requiredLevel: 'manage', getCenterId: resolveScopeCenterId }
)
在 authenticateToken() 内做的全局兜底守卫:按 method + path 从映射表解析权限规则,没映射或没权限直接 403。
resolveApiPermissionRule(path, method)
=> { permissions, requiredLevel, ruleId }
backend/server.js 在 /api 入口统一挂 authenticateToken()。backend/auth/auth-middleware.js 的 authenticateToken() 内调用 enforceAutoPermissionGuard()。enforceAutoPermissionGuard() 基于 method + path 查询 backend/config/api-permission-map.js,未命中映射或权限不足即 403(fail-closed)。| 方案 | 优点 | 代价 / 风险 | 适用阶段 |
|---|---|---|---|
| AutoGuard 兜底(映射表) | 全局兜底快,能防止“漏挂显式 RBAC”导致裸奔;默认 fail-closed。 | 规则来源偏隐式,调试时需回看映射表;与认证中间件耦合较高。 | 迁移期的安全基线 |
| 显式 RBAC(路由级) | 权限要求可读、可审计、可按业务精细化(permission/level/scope 都写在路由处)。 | 依赖团队纪律;若漏挂中间件,需要额外 gate 阻止回归。 | 长期主路径 |
| 当前三层过渡(JWT + AutoGuard + 显式RBAC) | 安全性高,兼容历史路由,能边迁移边运行。 | 存在双重权限判断,链路更长,维护复杂度更高。 | 当前状态 |
| 目标两层(JWT + 显式RBAC) | 职责清晰,鉴权语义统一,代码和测试更直接。 | 前提是显式覆盖率与 CI 门禁足够强,否则会有漏检风险。 | 迁移完成后 |
requirePermission*。结论:先完成“显式化 + 测试收口 + CI 门禁收口”,再下线 AutoGuard,风险最低。
/api 非公开请求
-> authenticateToken() // 只做身份认证,不做权限决策
-> requirePermission*() // 每条业务路由显式声明权限
-> handler
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
| 维度 | 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 |
通过 getCenterId 或 requirePermissionWithScope 指定 |
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
GET /api/reports/schedules/stats/api/reports*,GET 需要读权限(reports:view* / admin:view 等),level= view。backend/api/reports/schedules.js 对这个端点用了 requireScheduleManagePermission,要求 reports:schedule 或 admin:manage 且 level= manage。| 用户权限 | AutoGuard | 显式RBAC | 最终结果 |
|---|---|---|---|
只有 reports:view_schedules (view) |
通过 | 拒绝 | 403 |
有 reports:schedule (manage) |
通过 | 通过 | 进入 handler |
这就是“显式 RBAC 比 AutoGuard 更细、更严”的典型场景。
403。status != 403)。
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
AutoGuard 解决“有没有配权限规则”,显式 RBAC 解决“这条路由到底该多严格”,单权限对照测试 解决“规则是否真实生效且可回收无污染”。