文档目录
glen
· 预计阅读 4 分钟

第 5 章 路由、菜单与权限标识

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

第 5 章 路由、菜单与权限标识

Owl 的路由不是直接写 gin.GET,而是通过 RouterInfoBuilder 注册。这样做同时产生四样东西:

  1. Gin 路由;
  2. 运行时路由表(权限中间件用它判断访问级别);
  3. 权限标识(形如 app:module:方法名);
  4. 菜单节点(页面菜单与按钮菜单)。

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 菜单节点。

评论

加载评论中…

上一篇 第 4 章 Service Provider 与依赖注入 下一篇 第 6 章 配置、路径与运行目录

BitLabs © 2026 · 论坛讨论 · 博客长文 · 文档手册