AI 生成 Go 后台接口后,没人补测试会留下哪些权限坑

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 后台工程里这些边界怎么落地,可以直接看源码:

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

我更建议把它当成一个检查样本,而不是照抄模板。重点不是“用了哪个项目”,而是生成后的模块有没有这些验收动作。

一份上线前检查表

最后给一份我会在 AI 生成后台模块后跑的检查表:

检查项怎么验失败后果
未登录接口curl 不带 token应该 401,不能返回业务数据
普通角色危险操作curl 带普通角色 token 调删除/审核接口应该 403,不能只靠按钮隐藏
管理员正常操作go test + curl避免权限拦截写过头
菜单和按钮权限查询角色权限 SQL页面权限和后端权限不能脱节
二次生成字段对比 model、query、table、form防止字段只同步一半
操作日志看 method/path/user/result出事时能追到谁调了哪个接口

这张表不复杂,甚至有点笨。但后台系统不是 demo,笨办法有时最可靠。AI 负责把草稿写快,人负责把边界验清楚。尤其是权限这种问题,宁可发布前多跑几条 curl,也别等用户教你哪里漏了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值