第 5 章 路由、菜单与权限标识
本文导航

第 5 章 路由、菜单与权限标识
Owl 的路由不是直接写 gin.GET,而是通过 RouterInfoBuilder 注册。这样做同时产生四样东西:
- Gin 路由;
- 运行时路由表(权限中间件用它判断访问级别);
- 权限标识(形如
app:module:方法名); - 菜单节点(页面菜单与按钮菜单)。
Handler 契约
每个业务控制器实现 router.Handler:
type Handler interface {
ModuleName() (en string, zh string)
}
type OrderHandle struct{}
func (i *OrderHandle) ModuleName() (string, string) {
return "order", "订单管理"
}
en 会进入权限标识,zh 用作菜单标题默认值。一旦上线,不要随意改 en,否则权限标识变化,已有角色需要重新授权。
注册一条路由
func InitApi(app foundation.Application, appName string) {
err := app.Invoke(func(
engine *gin.Engine,
orderHandle *v1.OrderHandle,
) {
gv1 := engine.Group("/api/v1")
r := router.NewRouteInfoBuilder(appName, orderHandle, gv1, router.MenuOption{
ComponentName: "OrderList", // 前端组件名,也是菜单 Name
Path: "/order/list", // 前端路由地址
Icon: "ep:list",
})
r.Get("/orders", router.AccessAuthorized, orderHandle.Retrieve).
Name("订单列表").
Build()
r.Post("/orders", router.AccessAuthorized, orderHandle.Create).
Name("创建订单").
Build()
r.Get("/orders/:id", router.AccessAuthorized, orderHandle.Detail).
Name("订单详情").
Build()
})
owl.PanicIf(err)
}
Build() 会在内部做这些事:
- 从
handle的函数名推导权限标识; - 生成按钮菜单(仅
AccessAuthorized); - 调用
router.Handle注册真实路由; - 把路由信息写入全局
registeredRoutes。
权限标识怎么生成
func (i *RouterInfoBuilder) getPermissionStr(funcName string) string {
return i.appName + ":" + i.moduleEn + ":" + funcName
}
以 owl-admin 的用户模块为例:
| 路由 | 方法名 | 权限标识 |
|---|---|---|
POST /users |
Create |
admin:user:Create |
GET /users |
Retrieve |
admin:user:Retrieve |
DELETE /users/:id |
Delete |
admin:user:Delete |
注意最后一段是方法名,不是路由的 Name("创建用户")。重命名 Handler 方法会改变权限标识。
四种访问级别
router.AccessPublic // 开放接口,不校验令牌
router.AccessAuthenticated // 需要登录,令牌有效即可
router.AccessAuthorized // 需要授权,走 Casbin 校验权限标识
router.AccessSuperAdmin // 仅超管
它们的语义见《owl-admin 权限子系统使用手册》第 3 章。这里只记一条:需要授权的接口才会自动生成按钮菜单,也才会进入 Casbin 校验。
依赖权限:Deps
一个页面动作往往不止一个接口。例如「批量分配角色」页面加载时还需要「角色选项」接口。如果只给用户分配了「批量分配角色」,没给「角色选项」,页面就会缺数据。
Deps 用来声明这种依赖,框架会把依赖接口的权限一起挂到按钮菜单上:
r.Put("/users/batch-roles", router.AccessAuthorized, userHandle.BatchAssignRoles).
Deps(
router.Dep{Handler: roleHandle, Method: roleHandle.RoleOptions},
).
Name("批量分配用户角色").
Build()
依赖的权限标识会用 Dep.AppName 推导;为空时使用当前路由的 appName。
菜单
MenuOption 决定页面菜单:
type MenuOption struct {
ComponentName string // 前端组件名
Path string // 前端路由地址
Icon string // 图标
}
RegisterMenus() 返回的是菜单根节点。通常把 RouterInfoBuilder.GetMenu() 存到包级变量,再在 InitMenu() 里组装:
var orderMenu *router.Menu
// 注册路由时
orderMenu = r.GetMenu()
// 组装菜单
func InitMenu() []*router.Menu {
return []*router.Menu{
{
Path: "/order",
Name: "Order",
Meta: router.Meta{
Title: "订单管理",
Icon: "ep:shopping-cart",
ShowParent: true,
ShowLink: true,
},
MenuType: router.MenuTypeDir,
Children: []*router.Menu{orderMenu},
},
}
}
菜单类型:
MenuTypeDir:目录,只做分组;MenuTypeMenu:页面菜单;MenuTypeBtn:按钮,由Build()自动生成,用于权限分配。
菜单归属与按子系统过滤
框架会给每个 RegisterMenus() 返回的根菜单写上 SubApp:
menu.SubApp = app.Name()
前端请求 /api/v1/users/me/menus?app=admin 时,后端用 FilterMenusBySubApp 只保留根菜单 SubApp 匹配的项。多个子系统用逗号分隔:
GET /api/v1/users/me/menus?app=admin,cms
子节点不单独判断,跟随根节点。
运行时路由表
所有通过 RouterInfoBuilder.Build() 注册的路由,都会进入全局表:
routes := router.GetAllRoutes()
权限中间件遍历这张表,用「请求方法 + 路径」匹配路由,读取 AccessLevel 和 Permission。这意味着:
- 没注册到路由表里的路径,中间件会返回 404;
- 直接写
gin.GET绕过了 Builder,路由不会进入权限表,行为取决于中间件实现; - 修改数据库里的「接口表」不会改变权限,权限以运行时路由表为准。
操作日志开关
Build() 默认对非 GET 请求记录操作日志:
r.Post("/orders", router.AccessAuthorized, orderHandle.Create).
Name("创建订单").
WithoutOperateLog(). // 不记录
Build()
WithOperateLog() 可以显式打开。
自检
- [ ] Handler 实现了
ModuleName(); - [ ] 每条路由都通过
NewRouteInfoBuilder注册; - [ ]
AccessLevel与业务意图一致; - [ ] 需要多个接口配合的按钮用了
Deps; - [ ] 菜单
ComponentName与前端页面组件的name一致; - [ ] 没在
RegisterMenus()里返回 nil 菜单节点。
评论
加载评论中…