Cursor 能生成 CRUD,为什么 Go 后台权限还是会漏:RBAC 菜单和接口要一起验
Cursor 能把一个后台 CRUD 很快写出来,但权限经常不是“顺手就对”的东西。页面能打开、列表能查、新增能保存,只能说明业务链路通了;不能说明角色、菜单、按钮和接口已经绑定到同一套规则里。
我更愿意把 AI 生成的后台代码当成半成品。它能省掉重复文件,但权限边界必须单独验。最常见的误判是:前端菜单隐藏了,开发者就以为这个角色没有权限;实际接口还在,只要知道 URL,照样能打到后端。
这篇只看一个问题:AI 或代码生成器生成 Go 后台 CRUD 后,怎么检查 RBAC 权限有没有漏到接口层。示例用 GoFrame 写法,SQL 和 curl 可以直接换到自己的项目里跑。
先准备一个最小 RBAC 结构
后台权限至少要分清四类东西:用户、角色、菜单、接口动作。很多项目只做到前三个,问题就出在第四个。
CREATE TABLE admin_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE admin_role_menu (
role_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
PRIMARY KEY (role_id, menu_id)
);
CREATE TABLE admin_menu (
id BIGINT PRIMARY KEY,
parent_id BIGINT DEFAULT 0,
title VARCHAR(64) NOT NULL,
name VARCHAR(64) NOT NULL,
path VARCHAR(128) DEFAULT '',
api_perm VARCHAR(160) DEFAULT '',
type VARCHAR(16) NOT NULL
);
type 可以分成 menu 和 button。菜单负责路由显示,按钮负责具体动作。关键字段是 api_perm,比如:
GET /admin/user/list
POST /admin/user/save
DELETE /admin/user/delete
如果你的后台只靠前端路由控制,那就少了这层映射。AI 生成 CRUD 时也容易漏掉它,因为提示词通常会说“生成增删改查页面和接口”,很少明确说“把每个接口挂到角色菜单权限表上”。
一个很容易漏的场景
假设运营角色只能看用户列表,不能新增、不能删除。前端会把新增按钮隐藏掉,页面看起来没问题。但如果后端没有检查接口权限,下面这个请求仍然可能成功:
curl -i 'http://127.0.0.1:8000/admin/user/save' \
-H 'Authorization: Bearer <运营角色token>' \
-H 'Content-Type: application/json' \
--data '{"username":"test01","nickname":"测试用户"}'
你希望看到的是 403:
HTTP/1.1 403 Forbidden
{"code":403,"message":"无操作权限"}
如果返回 200,就不是前端按钮的问题了。这个角色已经绕过页面限制直接调用了接口。
GoFrame 中间件应该检查什么
后端不要只判断“登录了没有”。登录校验解决身份问题,权限校验解决这个身份能不能做当前动作。下面是一个简化版中间件,只保留核心逻辑:
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(r.Context(), perm)
if len(menuIds) == 0 {
r.Middleware.Next()
return
}
ok, err := userHasMenuPermission(r.Context(), user.Id, menuIds)
if err != nil {
g.Log().Warningf(r.Context(), "权限校验异常: %v", err)
r.Response.WriteStatusExit(500)
return
}
if !ok {
r.Response.WriteStatusExit(403)
return
}
r.Middleware.Next()
}
这里有两个点不能省。
第一,权限 key 用 METHOD + path,不要只用 path。GET /admin/user 和 POST /admin/user 往往不是同一个权限。
第二,没有找到权限配置时怎么处理,要按项目阶段定。内部后台早期可以放行,方便迁移;正式上线后更推荐记录日志并逐步收紧,否则新接口很容易裸奔。
用 SQL 反查角色到底有没有接口权限
调权限问题时,不要只看页面。直接查表更快。比如用户 1001 调 POST /admin/user/save,可以这样查:
SELECT arm.role_id, arm.menu_id, am.title, am.name, am.api_perm
FROM admin_user_role aur
JOIN admin_role_menu arm ON arm.role_id = aur.role_id
JOIN admin_menu am ON am.id = arm.menu_id
WHERE aur.user_id = 1001
AND am.api_perm = 'POST /admin/user/save';
有记录,说明角色拿到了这个接口动作;没有记录,就应该拦。再查一下接口本身有没有挂菜单:
SELECT id, title, name, type, api_perm
FROM admin_menu
WHERE api_perm = 'POST /admin/user/save';
如果这条也查不到,说明接口没有进入权限体系。AI 生成了 controller、service、dao,但没有补权限元数据,这就是典型缺口。
新增和编辑共用接口时要特别小心
很多后台会把新增和编辑合到一个 save 接口:有 id 就编辑,没有 id 就新增。页面上是两个按钮,后端却是同一个 URL。此时只用 POST /admin/user/save 映射一个权限,会出现“有编辑权限就顺便能新增”的问题。
一种做法是在中间件里根据参数区分:
func resolveAction(r *ghttp.Request, items []permItem) []uint64 {
if len(items) <= 1 {
return []uint64{items[0].MenuId}
}
id := r.Get("id")
target := "add"
if id != nil && !id.IsEmpty() && id.Int64() > 0 {
target = "edit"
}
for _, it := range items {
if strings.Contains(strings.ToLower(it.Name), target) {
return []uint64{it.MenuId}
}
}
return nil
}
这个判断看着小,但很实用。AI 生成 CRUD 时通常不会主动想到“同一个 save 接口背后对应两个按钮权限”,所以这类代码要人工补上,或者让代码生成器在模板层就固定生成。
发布前至少跑 4 个验证
我一般不相信“页面点过没问题”。后台权限要用反向验证。
| 验证项 | 怎么验 | 期望结果 |
|---|---|---|
| 未登录访问接口 | 不带 token 调接口 | 401 |
| 无菜单权限访问页面 | 用低权限角色登录 | 菜单不可见 |
| 无按钮权限访问接口 | 直接 curl 新增/删除接口 | 403 |
| 有权限角色访问接口 | 用管理员或授权角色调用 | 200 |
对应的 curl 可以写进项目脚本里:
TOKEN='<低权限角色token>'
BASE='http://127.0.0.1:8000'
curl -s -o /tmp/save.out -w '%{http_code}\n' \
"$BASE/admin/user/save" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
--data '{"username":"no_perm_user"}'
cat /tmp/save.out
如果状态码不是 403,就继续查 admin_menu.api_perm 和 admin_role_menu。不要先怀疑前端。
AI 和代码生成器各自适合做什么
AI 适合补样板代码,尤其是字段多、接口重复、前后端文件都要改的时候。但权限这种东西有上下文:哪个角色能做什么,按钮和接口是不是一一对应,新增和编辑是否共用 URL,接口缺配置时默认放行还是拒绝。这些不是“生成 CRUD”四个字能推出来的。
更稳的做法是让生成器负责固定结构,让 AI 负责局部修改和解释,让测试脚本负责兜底。比如在 XYGo Admin 的真实代码里,server/internal/middleware/admin_permission.go 已经把 METHOD + path、角色菜单表和新增/编辑分流放在后端检查;server/internal/dao/admin_role_menu.go、server/internal/model/entity/admin_role_menu.go 对应角色与菜单关系。需要看源码可以从这里进:
https://github.com/z312193608/xygo-admin
这不是说 AI 不能写后台。恰好相反,AI 写后台会越来越常见。只是权限不能停在“页面看起来对”。后台管理系统真正要验的是:看不到按钮的人,直接打接口也不能成功;没有角色关系的人,查表也找不到对应菜单动作;新增和编辑共用接口时,权限仍然能分开。
如果只验 CRUD 是否能跑,AI 生成的代码会显得很顺。如果把 RBAC、SQL、curl 和日志一起验,才知道这个后台能不能给真实用户用。
更多推荐


所有评论(0)