历史包袱:/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:readscope=Full,清单里一查便知。
/admin/ 接口内部改走同一个 ScopeResolver
(把前缀降级成「请求 scope=Full」的语法糖),两套接口共享一份逻辑;
再逐步把前端切到统一接口;最后下线 /admin/ 前缀。
关键是先统一底层,再收口入口,而不是一次性大重写。