AI 生成 Go 后台接口后,没人补测试会留下哪些权限坑
Cursor、Codex 或别的 Agent 把 CRUD 写出来以后,很多人第一反应是去点页面:列表能打开,新增能保存,编辑能回显,差不多就算过了。
后台系统最危险的地方恰好不在这里。页面能跑,只说明前端链路通了;权限、鉴权、按钮、接口和数据范围有没有对齐,通常要到联调、提测甚至上线后才暴露。到那时再补测试,成本比生成阶段高得多。
我现在更倾向于把 AI 生成的后台模块当成一份“可运行草稿”,不是可上线代码。草稿可以很快,但上线前至少要补三类检查:接口鉴权、RBAC 操作权限、字段和菜单的二次同步。下面用一个 Go 后台的最小例子把这件事拆开。
先看一个最常见的漏点
假设有一个订单模块,AI 生成了这些接口:
GET /admin/order/list
POST /admin/order/save
POST /admin/order/delete
GET /admin/order/detail
前端页面里,普通运营账号只能看到“查看”和“导出”,没有“删除”按钮。看起来权限生效了。
但如果后端没有单独拦截 `POST /admin/order/delete`,用户只要拿到 token,就可以绕过页面直接请求接口:
curl -i 'https://demo.example.com/admin/order/delete' \
-H 'Authorization: Bearer eyJhbGciOi...' \
-H 'Content-Type: application/json' \
--data '{"id": 1001}'
预期结果应该是 403 或业务错误码,比如:
HTTP/1.1 403 Forbidden
{"code":403,"message":"无操作权限"}
如果返回 200,说明“按钮隐藏”只是前端效果,后端权限没有兜住。AI 生成后台时很容易漏这一步,因为模型通常会把权限理解成路由菜单或页面状态,而不是每个 API 的强制校验。
表结构要能回答两个问题
RBAC 表可以设计得很复杂,但上线前至少要能回答两个问题:
1. 这个角色能看到哪些菜单?
2. 这个角色能调用哪些接口或按钮动作?
一个简化版表结构可以这样写:
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY,
role_key VARCHAR(64) NOT NULL,
name VARCHAR(64) NOT NULL
);
CREATE TABLE sys_menu (
id BIGINT PRIMARY KEY,
parent_id BIGINT NOT NULL DEFAULT 0,
title VARCHAR(64) NOT NULL,
path VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, -- menu/button/api
permission VARCHAR(128) DEFAULT ''
);
CREATE TABLE sys_role_menu (
role_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
PRIMARY KEY (role_id, menu_id)
);
这里的关键不是字段名,而是 `type` 和 `permission` 要能覆盖按钮和接口。只有菜单没有接口权限,最后还是会变成“页面看不到,接口还能调”。
可以用一条 SQL 找出角色拥有的权限点:
SELECT r.role_key, m.type, m.title, m.permission
FROM sys_role r
JOIN sys_role_menu rm ON rm.role_id = r.id
JOIN sys_menu m ON m.id = rm.menu_id
WHERE r.role_key = 'operator'
ORDER BY m.type, m.id;
这条查询不要只在初始化时看一次。AI 或代码生成器二次生成模块后,也要再跑一遍。很多权限问题不是第一次建表时出现的,而是后面加按钮、改接口、移动菜单时留下的。
Go 中间件不能只判断登录态
很多 AI 生成的 Go 后台会先写一个 JWT 中间件。这个中间件只能回答“用户有没有登录”,不能回答“用户能不能删订单”。
一个更稳的链路是两层:
func AdminAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if token == "" {
http.Error(w, "未登录", http.StatusUnauthorized)
return
}
user, err := ParseToken(token)
if err != nil {
http.Error(w, "登录已失效", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func AdminPermission(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
perm := strings.ToUpper(r.Method) + " " + r.URL.Path
ok, err := UserHasPermission(r.Context(), user.ID, perm)
if err != nil {
http.Error(w, "权限校验失败", http.StatusInternalServerError)
return
}
if !ok {
http.Error(w, "无操作权限", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
这里有个容易被忽略的细节:权限 key 最好用 `METHOD + PATH`,不要只用路径。`GET /admin/order/detail` 和 `POST /admin/order/delete` 的风险完全不同。只存路径,后面迟早会遇到误放行。
用 go test 补一层底线
页面测试当然有用,但权限这种东西最好先用接口级测试压住。下面是一个最小测试,目标不是覆盖所有业务,而是先把“无权限不能删”这条底线钉住。
func TestOperatorCannotDeleteOrder(t *testing.T) {
token := loginAs(t, "operator")
body := strings.NewReader(`{"id":1001}`)
req := httptest.NewRequest(http.MethodPost, "/admin/order/delete", body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("operator should not delete order, got status=%d body=%s", rr.Code, rr.Body.String())
}
}
再补一个正向用例,避免权限拦截写过头:
func TestAdminCanDeleteOrder(t *testing.T) {
token := loginAs(t, "admin")
req := httptest.NewRequest(http.MethodPost, "/admin/order/delete", strings.NewReader(`{"id":1001}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("admin should delete order, got status=%d body=%s", rr.Code, rr.Body.String())
}
}
这类测试不复杂,但很值。AI 生成代码以后,你不一定能马上看出哪里漏了;测试会直接告诉你这个角色到底能不能碰某个接口。
再用 curl 做一次黑盒验收
`go test` 过了以后,我还会用 curl 跑一遍接近真实环境的请求。原因很简单:测试里的 router、middleware、配置加载方式,可能和实际部署不完全一致。
# 1. 未登录访问,应该返回 401
curl -i 'https://demo.example.com/admin/order/list'
# 2. 普通运营删除订单,应该返回 403
curl -i 'https://demo.example.com/admin/order/delete' \
-H 'Authorization: Bearer <operator-token>' \
-H 'Content-Type: application/json' \
--data '{"id":1001}'
# 3. 管理员删除订单,应该返回 200 或业务成功码
curl -i 'https://demo.example.com/admin/order/delete' \
-H 'Authorization: Bearer <admin-token>' \
-H 'Content-Type: application/json' \
--data '{"id":1001}'
验收日志最好能看到方法、路径、用户、角色和结果:
[auth] method=POST path=/admin/order/delete user=1002 role=operator result=deny code=403
[auth] method=POST path=/admin/order/delete user=1 role=admin result=allow code=200
没有日志也能跑,但出问题时会很难查。尤其是 AI 改过路由或重构过中间件以后,日志是最快的定位线索。
生成器二次同步也要验
AI 生成后台还有一个常见坑:第一次生成很顺,第二次改字段就乱。
比如订单表新增 `audit_status`:
ALTER TABLE biz_order ADD COLUMN audit_status TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态';
这时至少要检查四处:
1. 后端 input/model 是否新增 audit_status
2. 列表查询是否允许按 audit_status 筛选
3. 前端表格是否展示审核状态
4. 新增/编辑表单是否处理默认值和权限
如果生成器只同步了 model,没同步查询条件;或者前端多了一列,后端没有返回字段,页面就会出现“看起来是前端问题,其实是生成链路不完整”的情况。
XYGo Admin 里有 RBAC 和 CRUD 生成器的真实实现,仓库最近也处理过生成器导入兜底、Pinia 状态树合并这类问题。想看 GoFrame + Vue3 后台工程里这些边界怎么落地,可以直接看源码:
我更建议把它当成一个检查样本,而不是照抄模板。重点不是“用了哪个项目”,而是生成后的模块有没有这些验收动作。
一份上线前检查表
最后给一份我会在 AI 生成后台模块后跑的检查表:
| 检查项 | 怎么验 | 失败后果 |
| 未登录接口 | curl 不带 token | 应该 401,不能返回业务数据 |
| 普通角色危险操作 | curl 带普通角色 token 调删除/审核接口 | 应该 403,不能只靠按钮隐藏 |
| 管理员正常操作 | go test + curl | 避免权限拦截写过头 |
| 菜单和按钮权限 | 查询角色权限 SQL | 页面权限和后端权限不能脱节 |
| 二次生成字段 | 对比 model、query、table、form | 防止字段只同步一半 |
| 操作日志 | 看 method/path/user/result | 出事时能追到谁调了哪个接口 |
这张表不复杂,甚至有点笨。但后台系统不是 demo,笨办法有时最可靠。AI 负责把草稿写快,人负责把边界验清楚。尤其是权限这种问题,宁可发布前多跑几条 curl,也别等用户教你哪里漏了。

576

被折叠的 条评论
为什么被折叠?



