RBAC / 权限 总览待审核
历史包袱 · 两套接口

历史包袱:/api/admin/article/list vs /api/article/list

很多老系统里存在两套几乎一样的接口:普通接口 /api/article/list 只返回「自己的」, 管理接口 /api/admin/article/list 返回「全部」。它可以运行,但本质上是 把 data-scope(见 Scope)硬编码进了 URL——用「走哪个接口」来区分「看到哪些数据」。 这是一种典型的妥协方案。下面对照它和「统一接口 + scope + ScopeResolver(见 Server 层)」的差别。

当前用户:me(editor,A 组,scope=Group)。点不同接口看返回的数据行:

选一个接口

点上面三个接口对比。

😵 两套接口(历史包袱)

// scope 藏在 URL 前缀里
GET /api/article/list        → 写死 Self
GET /api/admin/article/list  → 写死 Full
  • 控制器逻辑复制两份,改一处容易遗漏另一处。
  • scope 只能「自己 / 全部」二选一;要 Group 就得再加第三个接口。
  • 权限判断变成「能不能访问 /admin/ 前缀」,脱离了 permission 模型。
  • 前端要记住「该调哪个」;调错就越权或缺数据
  • 审计困难:「谁能看全部」散落在路由表里,而非权限清单里。

🎯 统一接口 + ScopeResolver

// 一个接口, scope 由权限算出来
GET /api/article/list
  assert('article:read')
  where = ScopeResolver(
    scope('article:read'), me)
  • 一份控制器,scope 由生效权限算出,不写死在路径。
  • Full / Group / Self / Public 任意扩展,改权限即可,不动路由。
  • 权限回到模型:can / assert / scope 一致可查。
  • 前端永远调同一个,scope 后端说了算,不会调错
  • 「谁能看全部」= 谁的 article:read scope=Full,清单里一查便知。
这种写法当年出现的原因是实现成本低:早期只有「前台 / 后台」两个站点,各自一套接口最直接, 当时也没有抽象出 scope 概念。负担是后来累积的——需求从「自己 / 全部」分化成 「本组」「本店」「本区域」……每多一种范围就多一套接口,组合数量失控。 迁移路径:先让 /admin/ 接口内部改走同一个 ScopeResolver (把前缀降级成「请求 scope=Full」的语法糖),两套接口共享一份逻辑; 再逐步把前端切到统一接口;最后下线 /admin/ 前缀。 关键是先统一底层,再收口入口,而不是一次性大重写。 但也不必一概否定。当「后台由另一个团队维护、另一套 BFF、风险模型完全不同」时, 接口分离本身可能是合理的边界——只是不该用它来表达 data-scope。 边界归边界,scope 归 scope,是两件独立的事。