第 8 章 新模块接入权限与排查手册
本文导航

第 8 章 新模块接入权限与排查手册
这一章把前面的知识落到「给 owl-admin 新增一个业务模块」上:需要写哪些代码、权限从哪来、怎么验证、出问题怎么查。
接入步骤
假设新增一个「公告」模块,路由前缀 /notices。
1. Handler 声明模块名
type NoticeHandle struct {
svc *service.NoticeService
}
func (i *NoticeHandle) ModuleName() (string, string) {
return "notice", "公告管理"
}
notice 会进入权限标识:admin:notice:Create。
2. 注册路由
在 app/route/api.go 的 InitApi 里:
{
r := router.NewRouteInfoBuilder(appName, noticeHandle, gv1, router.MenuOption{
ComponentName: "SystemNotice",
Path: "/system/notice/index",
Icon: "ep:bell",
})
r.Post("/notices", router.AccessAuthorized, noticeHandle.Create).Name("创建公告").Build()
r.Get("/notices", router.AccessAuthorized, noticeHandle.Retrieve).Name("公告列表").Build()
r.Put("/notices/:id", router.AccessAuthorized, noticeHandle.Update).Name("更新公告").Build()
r.Delete("/notices/:id", router.AccessAuthorized, noticeHandle.Delete).Name("删除公告").Build()
noticeMenu = r.GetMenu()
}
需要登录但不需要授权的接口用 AccessAuthenticated;公开接口用 AccessPublic。
3. 挂到菜单树
{
Path: "/system",
Name: "System",
// ...
Children: []*router.Menu{
// ...
noticeMenu,
},
}
noticeMenu 的 Name 是 SystemNotice,前端页面组件必须同名:
<script setup lang="ts">
defineOptions({ name: "SystemNotice" });
</script>
否则多标签和 keep-alive 会异常。
4. 注册 Binds
func (i *SubAppAdmin) Binds() []any {
return []any{
// ...
v1.NewNoticeHandle,
service.NewNoticeService,
repository.NewNoticeRepository,
}
}
5. 迁移模型
把 &model.Notice{} 加进 database.Models(),确保 RegisterMigrate() 返回它。
6. 给角色授权
重启服务后:
GET /menus/assignable能看到「公告管理」页面和四个按钮;PUT /roles/:id/menus勾选页面与按钮;- 普通用户重新登录,验证:
- 菜单出现;
me/permissions包含admin:notice:*;- 有权限的接口 200,没分配的接口 403。
权限标识一览
| 路由 | 权限标识 |
|---|---|
POST /notices |
admin:notice:Create |
GET /notices |
admin:notice:Retrieve |
PUT /notices/:id |
admin:notice:Update |
DELETE /notices/:id |
admin:notice:Delete |
如果某个按钮页面还需要调用公告详情,用 Deps 把详情权限带上:
r.Put("/notices/:id", router.AccessAuthorized, noticeHandle.Update).
Deps(router.Dep{Handler: noticeHandle, Method: noticeHandle.Detail}).
Name("更新公告").
Build()
排查表
401 未授权
| 检查 | 说明 |
|---|---|
Authorization 头 |
必须是 Bearer <token> |
| 令牌是否过期 | conf/jwt.yaml 的 expire |
signing-key |
多实例必须一致 |
| 路由级别 | AccessPublic 不会解析令牌,不会返回 401 |
403 未授权
| 检查 | 说明 |
|---|---|
| 路由级别 | 是否误设为 AccessSuperAdmin |
| 权限标识 | me/permissions 是否包含该接口 |
| 角色菜单 | admin_role_menu 是否有对应按钮 |
| Casbin p 行 | casbin_rule 是否有 p, roleID, permission |
| Casbin g 行 | 是否有 g, userID, roleID |
| 用户是否超管 | 超管应直接放行,若超管也 403,检查 JWT 里的 isSuperAdmin |
菜单不显示
| 检查 | 说明 |
|---|---|
RegisterMenus() |
是否返回了该菜单,且不是 nil |
根菜单 SubApp |
是否被 ?app= 过滤 |
| 角色 | 是否分配了页面菜单 |
Meta.ShowLink |
是否为 true |
| 前端组件 | name 是否与菜单 Name 一致 |
按钮不显示
| 检查 | 说明 |
|---|---|
| 权限标识 | 前端写的字符串是否与后端完全一致 |
| 权限缓存 | localStorage 的 user-permissions-cache 是否过期 |
| 数组语义 | hasPerms([a,b]) 要求全部满足 |
| 超管通配 | permissions 是否为 *:*:* |
接口 404
| 检查 | 说明 |
|---|---|
RegisterRouters() |
是否被调用(ConsoleShell 不会注册路由) |
InitApi |
是否真的执行 |
| 路由表 | 是否通过 Builder 注册 |
| 路径 | 是否少了 /api/v1 前缀或多了斜杠 |
权限改了不生效
| 检查 | 说明 |
|---|---|
| 是否走了接口 | 直接改表不会同步 Casbin |
| 事件监听 | listener.Init 是否执行 |
| 后端 | 每次请求 LoadPolicy,应即时生效 |
| 前端 | 权限缓存 24 小时,需要刷新或重新登录 |
上线前自检
- [ ] 新模块的 Handler 实现了
ModuleName(); - [ ] 所有路由通过
RouteInfoBuilder注册,级别正确; - [ ] 需要多个接口的按钮用了
Deps; - [ ] 页面已加入
InitMenu,根菜单有SubApp; - [ ] 前端组件
name与菜单Name一致; - [ ] 按钮使用
hasPerms/Perms,标识与后端一致; - [ ] 用普通用户完成一次「有权限 200 / 无权限 403」验证;
- [ ] 用超管验证没有误拦截;
- [ ] 不需要授权的公开接口没有泄露敏感数据。
写在最后
权限系统的复杂度集中在「三处一致性」:
- 路由的权限标识与角色菜单里的按钮一致;
- Casbin 的
p/g行与数据库关联一致; - 前端
hasPerms的字符串与后端权限标识一致。
任何一处对不上,表现都是「菜单/按钮/接口有一个不对」。按这三条逐一核对,绝大多数问题都能定位。
暂无下一篇
评论
加载评论中…