后台管理系统 RBAC 排障:隐藏菜单后接口为什么还能调用

做后台权限时,很多问题不是出在 RBAC 这个词本身,而是把几种权限混在了一起:菜单能不能看到、按钮能不能点、字段能不能展示、接口能不能访问。它们看起来都叫"权限",落到代码里却不是一层东西。

最容易出错的是前端路由和按钮控制。页面上把"删除"按钮藏掉,用户界面确实干净了,但接口并不会因此安全。只要浏览器里还有 token,或者有人从 Network 面板复制请求,照样可以绕过按钮直接打接口。

这篇只讨论一个具体问题:Go 后台管理系统里,为什么隐藏菜单以后,接口还是可能被调用,以及怎么把接口中间件、按钮权限和字段权限拆开。

先分清四类权限

权限类型 控制什么 常见位置 不能替代谁
菜单权限 用户能不能看到某个页面入口 路由、侧边栏、菜单表 不能替代接口校验
按钮权限 页面里能不能看到新增、编辑、删除按钮 前端指令、路由 meta、按钮子菜单 不能替代接口校验
接口权限 某个 HTTP 请求能不能执行 后端中间件、路由权限点 不能替代字段权限
字段权限 某个字段能不能看、只读或编辑 表单、列表、导出、接口响应 不能替代菜单权限

菜单权限偏"入口",按钮权限偏"操作提示",接口权限才是服务端边界。字段权限更细,它关心的是同一张表里不同角色看到什么字段、能不能改某个字段。

如果只做前端控制,权限会变成"防误点",不是"防越权"。这点在后台系统里很常见,尤其是删除、导出、改状态这类接口。

一个绕过前端按钮的反例

假设页面上没有删除按钮,但接口还是这样暴露:

bash 复制代码
curl 'https://admin.example.com/admin/user/delete' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  --data '{"id": 1001}'

前端按钮是否存在,对这条请求没有任何约束。服务端如果只判断 token 有效,不判断这个用户是否有 `DELETE /admin/user/delete` 对应的菜单或按钮权限,请求就可能继续往下走。

所以后端至少要有一层接口权限校验。它不一定要写得很复杂,但必须独立存在。

后端中间件应该查什么

我更倾向于把权限点挂在菜单或按钮配置上,再由中间件把当前请求转成一个稳定的 key,例如:

go 复制代码
perm := strings.ToUpper(r.Method) + " " + r.URL.Path
// 例子:DELETE /admin/user/delete

然后查这个接口权限点关联了哪些菜单或按钮,再判断当前用户的角色是否拥有这些菜单 ID。伪代码大概是这样:

go 复制代码
func AdminPermission(r *ghttp.Request) {
    user := contexts.GetUser(r.Context())
    if user == nil {
        r.Middleware.Next()
        return
    }

    if consts.IsSuperRole(user.RoleKey) {
        r.Middleware.Next()
        return
    }

    perm := strings.ToUpper(r.Method) + " " + r.URL.Path
    menuIds := findMenuIdsByPerm(perm)
    if len(menuIds) == 0 {
        r.Middleware.Next()
        return
    }

    if !userHasMenuPermission(r.Context(), user.Id, menuIds) {
        r.SetError(gerror.NewCode(consts.CodeNoPermission, "无操作权限"))
        return
    }

    r.Middleware.Next()
}

这里有个细节:同一个保存接口可能同时处理新增和编辑。接口路径一样,但请求里的 `id` 为空时是新增,不为空时是编辑。这个时候不要只按路径判断,要结合请求参数把 `add` 和 `edit` 拆开,否则一个"编辑"权限可能误放行"新增"。

XYGo Admin 的 `server/internal/middleware/admin_permission.go` 里就是类似思路:用 `METHOD + path` 做权限 key,再通过 `resolveMenuIds` 根据请求里的 `id` 区分 add/edit。这个实现不神秘,但方向是对的:按钮和接口最终要在服务端重新对齐一遍。

前端指令只负责用户体验

前端仍然要做按钮控制,不然普通用户会看到一堆点了也报错的按钮,体验很差。Vue3 里常见写法是做一个 `v-auth` 指令:

vue 复制代码
<el-button v-auth="'add'">新增</el-button>
<el-button v-auth="'edit'">编辑</el-button>
<el-button v-auth="'delete'">删除</el-button>

指令内部从当前路由的 `meta.authList` 里取权限标识。没有对应 `authMark` 时,把 DOM 节点移除:

ts 复制代码
const authList = router.currentRoute.value.meta.authList || []
const hasPermission = authList.some(item => item.authMark === binding.value)

if (!hasPermission) {
  el.parentNode?.removeChild(el)
}

这段逻辑的边界要说清楚:它只能减少误操作,不能当安全边界。安全边界在后端中间件。前端指令和后端中间件应该使用同一套权限配置,至少要能从同一个菜单/按钮模型里推出来,不要两边各写一套字符串。

字段权限不要塞进菜单权限里

字段权限经常被低估。比如财务字段、手机号、成本价、内部备注,不一定要按页面隔离。有些角色可以进同一个页面,但只能看部分字段,或者只能编辑一部分字段。

字段权限可以单独建模,至少要包含这些信息:

sql 复制代码
CREATE TABLE admin_field_perm (
  id bigint PRIMARY KEY,
  role_id bigint NOT NULL,
  module varchar(64) NOT NULL,
  resource varchar(128) NOT NULL,
  field_name varchar(128) NOT NULL,
  field_label varchar(128) NOT NULL,
  perm_type tinyint NOT NULL COMMENT '0=不可见,1=只读,2=可编辑',
  status tinyint NOT NULL DEFAULT 1
);

这类表不要和菜单表硬绑死。菜单回答"能不能进入用户管理页",字段权限回答"进入以后能不能看手机号、能不能改余额"。两个问题不一样。

XYGo Admin 里有 `server/internal/model/entity/admin_field_perm.go`,字段包括 `role_id`、`module`、`resource`、`field_name`、`perm_type`。如果你在做 GoFrame + Vue3 后台,可以把这种模型当成参考:字段权限独立存,页面和接口按资源再去消费。

排查清单

遇到"菜单隐藏了,接口还能调用"时,我通常按这个顺序查:

  1. 后端有没有认证中间件,只判断登录还是也判断权限。
  2. 接口权限点是否保存成稳定格式,例如 `POST /admin/user/save`。
  3. 菜单、按钮和接口权限点有没有关联,还是只在前端路由里写了按钮列表。
  4. 新增和编辑是否共用接口,如果共用,是否按 `id` 或业务动作拆权限。
  5. 字段权限是否单独处理,还是误以为菜单权限能覆盖字段展示和编辑。

验证也很简单。先用低权限账号登录,确认页面上按钮不可见;再复制同一个接口,用 curl 带 token 请求。如果接口返回"无操作权限",说明服务端边界存在。如果接口成功了,前端按钮隐藏得再漂亮也没用。

放到工程里的建议

权限链路可以按这个顺序落地:

text 复制代码
登录认证中间件
  -> 注入当前用户
  -> 接口权限中间件
  -> 查询接口权限点关联的菜单/按钮
  -> 判断角色是否拥有这些菜单/按钮
  -> 进入具体业务逻辑

前端拿到菜单和按钮权限后,只做展示控制。后端每个敏感接口都重新校验。字段权限单独存,读列表、导出、编辑表单时再按资源和字段过滤。

这样写会多一些表和中间件,但后面排查问题会轻很多。看到某个接口被放行,可以直接查接口权限点、角色菜单关系和字段权限表,而不是在前端路由、按钮组件、后端 handler 里来回猜。

如果想看一套 GoFrame + Vue3 项目里怎么把菜单、按钮和字段权限拆开,可以看 XYGo Admin 里的权限中间件、前端 `v-auth` 指令和字段权限模型。重点看拆分方式,不要只看页面长什么样。

仓库: GitHub - z312193608/xygo-admin: Open-source admin framework built with GoFrame v2 and Vue 3, featuring RBAC, full-stack CRUD code generation, MySQL/PostgreSQL, plugins, and single-binary deployment. · GitHub

相关推荐
Wang's Blog3 小时前
Go-Zero框架上手前奏2: 微服务核心要素 —— 拆分、通信与无状态设计
开发语言·微服务·golang
Generalzy21 小时前
从本地 Demo 到生产级检索:Milvus 学习笔记(2)
golang·milvus
小高Baby@21 小时前
单链表的删操作
数据结构·算法·golang
张3231 天前
Go语言基础 Map 函数值 闭包
开发语言·golang
北冥you鱼1 天前
Go Modules 使用指南:从入门到精通
开发语言·后端·golang
灯澜忆梦1 天前
【dp_1】爬楼梯 | 斐波那契数 | 第 N 个泰波那契数 | 三步问题
算法·golang
geovindu1 天前
go: Floyd-Warshall Algorithms
开发语言·后端·算法·golang
大侠锅锅2 天前
第 9 篇:状态机实践——状态环 + 步进表替代 if-else 地狱
golang·边缘计算·状态机
灯澜忆梦2 天前
iota枚举
golang