https://rodert.github.io/ShiyuAdmin/做后台管理系统最开始大家通常关注的是用户管理增删改查登录数据库页面布局但真正做到后面你会发现后台管理系统真正麻烦的地方其实是权限。比如张三只能查看用户不能删除用户。李四可以管理订单但是看不到系统配置。部门管理员只能管理自己部门的数据。超级管理员则拥有整个系统的全部权限。当系统规模越来越大以后如果还在代码里写ifusernameadmin{// 允许访问}那基本很快就会失控。这也是为什么绝大多数成熟后台系统都会引入RBAC Role-Based Access Control 基于角色的访问控制最近我把自己之前做的一个通用后台管理系统ShiyuAdmin重新整理并开源了。这个项目本身就是一个比较完整的前后端分离后台脚手架Go Gin Gorm React Ant Design Pro RBAC项目内置用户、角色、菜单、部门、动态菜单、接口权限、JWT 登录认证、操作日志、Redis 管理、系统监控等模块。今天不打算把整个项目所有功能都讲一遍。我们只拆其中一个非常重要的知识点RBAC 权限系统到底是怎么实现的一、先认识一下 ShiyuAdminShiyuAdmin 是一个前后端分离的通用后台管理系统。整体目录大致是ShiyuAdmin ├── backend/shiyu-admin-backend ├── frontend/shiyu-admin-web ├── site ├── docs ├── img ├── docker-compose.yml └── README.md其中backend/shiyu-admin-backend是 Go 后端。frontend/shiyu-admin-web则是 React Ant Design Pro 前端。后端主要技术栈为Go Gin Gorm Viper JWT Redis前端则使用React Umi Max Ant Design Pro TypeScript ECharts数据库目前项目提供 PostgreSQL、MySQL、SQLite 等部署方式同时项目仓库中还提供了 SQL Server 对应的 Compose 配置。而今天要讲的 RBAC就贯穿了整个后台系统。二、为什么后台管理系统一定需要权限系统假设我们现在开发一个商城后台。系统一开始只有一个管理员admin那么事情很简单。所有页面用户管理 商品管理 订单管理 财务管理 系统配置admin 全部都可以访问。于是我们甚至不需要权限系统。但后来公司有了 10 个员工。销售人员只能看订单订单管理 客户管理运营人员负责商品管理 活动管理 优惠券财务人员负责订单 退款 财务统计技术管理员则负责用户 角色 权限 系统配置如果按照用户一个一个授权张三 - 订单权限 张三 - 客户权限 李四 - 商品权限 李四 - 优惠券权限 王五 - 财务权限 王五 - 退款权限用户一多这件事情就很难维护。于是就出现了Role也就是角色我们不再直接用户 - 权限而是用户 ↓ 角色 ↓ 权限例如张三 ↓ 销售 ↓ 订单查看 客户查看于是User - Role - Permission就成为 RBAC 最基础的思想。三、ShiyuAdmin 的 RBAC 模型ShiyuAdmin 当前权限关系的核心可以简化成User │ │ 多对多 ▼ Role │ │ 多对多 ▼ Menu │ ▼ Permission项目后端文档也明确说明目前采用的是用户 ↔ 角色 ↔ 菜单的多对多权限关系权限中间件位于internal/middleware/permission.go菜单与权限标识则由菜单实体维护。为什么 Permission 会和 Menu 联系到一起因为后台管理系统中的菜单通常不仅仅代表一个导航项。一个 Menu 还可以携带路由 权限标识 父子关系 排序 菜单类型例如用户管理可以拥有权限system:user:list新增用户system:user:create修改用户system:user:update删除用户system:user:delete于是权限系统就有了一个非常常见的表达方式模块:资源:操作例如system:user:list system:user:create system:user:update system:user:delete这种设计非常适合后台系统。四、为什么不能只做“前端权限”这是很多刚开始做后台的人容易踩的坑。假设前端判断if (hasPermission(system:user:delete)) { return Button删除/Button }没有权限的人就看不到删除按钮。看起来好像已经完成权限控制了。但实际上完全不够。因为用户可以绕过前端直接请求DELETE /api/v1/system/users/123比如使用Postman curl Apifox Python都可以直接调用接口。所以前端权限主要负责用户体验后端权限才是真正的安全边界。正确架构应该是┌─────────────┐ │ 用户 │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ React 前端 │ └──────┬──────┘ │ JWT Token│ ▼ ┌─────────────┐ │ Gin API │ └──────┬──────┘ │ JWT Middleware │ ▼ Permission Middleware │ ▼ RBAC │ ▼ Service / DB也就是说前端控制显示什么 后端决定你到底能不能做五、第一层JWT 解决“你是谁”RBAC 之前还有一个问题系统怎么知道当前请求到底是谁发出来的ShiyuAdmin 这里使用的是 JWT。也就是JSON Web Token用户登录成功以后后端生成 Token。项目中的 JWT Claims 大致包含typeClaimsstruct{UserCodestringjson:user_codeUsernamestringjson:usernameIsSuperAdminbooljson:is_super_adminjwt.RegisteredClaims}也就是说 Token 里面除了用户标识还携带user_code username is_super_admin其中IsSuperAdmin是一个非常值得讲的设计。整个请求流程可以理解成用户名 密码 ↓ 登录接口 ↓ 校验数据库 ↓ 生成 JWT ↓ 返回给前端以后每一次请求都携带Authorization: Bearer xxxxx后端解析 JWTJWT ↓ UserCode ↓ 当前用户于是解决了第一个问题Authentication也就是认证你是谁六、认证和授权其实是两回事这里顺便讲一个很多新手容易混淆的知识。Authentication认证Authorization授权它们完全不是一个东西。认证解决你是谁授权解决你能干什么例如张三成功登录只说明Authentication OK但张三能不能删除用户DELETE /users/100还需要Authorization所以整个体系应该是登录 ↓ JWT Authentication ↓ 用户身份 ↓ RBAC Authorization ↓ 接口权限这就是后台权限系统最核心的两层。七、第二层Permission Middleware有了用户身份以后下一步就是这个用户有没有访问当前接口的权限ShiyuAdmin 将这一层放到了 Gin Middleware 中。例如internal/middleware/permission.go项目里提供了类似RequirePermission()RequireAnyPermission()RequireAllPermissions()这样的权限检查逻辑。我们可以理解成下面这种形式funcRequirePermission(permissionstring)gin.HandlerFunc{returnfunc(c*gin.Context){claims:GetClaims(c)ifclaimsnil{c.AbortWithStatus(401)return}ifclaims.IsSuperAdmin{c.Next()return}if!permissionService.HasPermission(claims.UserCode,permission,){c.AbortWithStatus(403)return}c.Next()}}实际思想非常简单。流程请求 API ↓ JWT Middleware ↓ 获得当前用户 ↓ Permission Middleware ↓ 是否超级管理员 ↓ 否 ↓ 查询用户权限 ↓ 是否拥有 permission ↓ YES → API NO → 403这样权限控制就彻底从业务代码中抽出来了。八、为什么 Middleware 非常适合做权限假设不用 Middleware。那么每个接口可能都要写funcDeleteUser(c*gin.Context){user:GetCurrentUser(c)if!HasPermission(user,system:user:delete){return}// 删除用户}然后funcCreateUser(c*gin.Context){user:GetCurrentUser(c)if!HasPermission(user,system:user:create){return}}几百个接口以后权限代码 权限代码 权限代码 权限代码全部散落在 Controller 里面。非常难维护。Middleware 则可以变成router.DELETE(/users/:id,RequirePermission(system:user:delete),DeleteUser,)ControllerfuncDeleteUser(c*gin.Context){id:c.Param(id)err:userService.Delete(id)iferr!nil{// handle error}}业务 Controller 完全不关心权限。这就是Separation of Concerns 关注点分离九、ShiyuAdmin 中一个比较有意思的设计超级管理员实际开发 RBAC 时很快会遇到一个特殊用户Super Admin超级管理员通常需要不受角色限制 不受菜单限制 不受接口权限限制最笨的方法是ifusernameadmin{returntrue}但这样会导致用户名和权限体系强耦合ShiyuAdmin 没有简单依赖用户名判断而是在用户模型里面设计了IsSuperAdminbool类似typeUserstruct{IDint64UserCodestringUsernamestringStatusintIsSuperAdminbool}项目当前用户模型中Status用于控制账号是否启用而IsSuperAdmin用来标识系统级超级管理员。于是admin只是一个用户名。真正决定权限的是is_super_admin true这样设计明显更加合理。因为以后你完全可以创建admin root ops_admin emergency_admin然后分别设置is_super_admin true权限体系不需要关心用户名。十、超级管理员为什么直接绕过 RBAC权限中间件里面有一个非常直接的逻辑ifclaims.IsSuperAdmin{c.Next()return}然后普通用户才继续User ↓ Role ↓ Menu ↓ PermissionShiyuAdmin 当前的RequirePermission、RequireAnyPermission、RequireAllPermissions都会优先检查超级管理员标识如果为超级管理员就直接放行不再继续执行普通 RBAC 权限计算。这其实是比较常见的后台设计。因为否则超级管理员也需要关联100 个角色 500 个菜单 1000 个权限完全没有意义。所以通常SuperAdmin ↓ PASS Normal User ↓ RBAC十一、但是这里有一个值得进一步思考的问题ShiyuAdmin 当前会把is_super_admin写入 JWT。这样的好处是快每次请求不用再次查询数据库ifclaims.IsSuperAdmin{returntrue}但是它也会产生一个经典问题假设管理员正在登录。JWT{username:admin,is_super_admin:true}然后我们在数据库里修改is_super_admin false那么已经签发出去的 JWT 怎么办如果 JWT 有效期是一小时那么旧 Token 在过期之前仍然可能is_super_admin true这就是 JWT 权限设计中特别值得注意的问题Token 权限缓存JWT 本质上可以理解成一种客户端持有的身份快照因此对于安全要求特别高的系统可以进一步增加Token Version Redis Session 权限版本号 黑名单 短 Token Refresh Token例如JWT ↓ user_code token_version ↓ Redis ↓ 当前 token_version一旦管理员权限被撤销token_version旧 Tokenversion 3数据库version 4于是旧 Token 立即失效。这是在现有项目结构上继续升级权限系统的一个方向。十二、第三层动态菜单权限系统还有一个问题后端告诉你你不能访问用户管理但如果前端依然显示用户管理用户一点403体验显然不好。所以成熟后台系统通常还会做Dynamic Menu也就是动态菜单流程用户登录 ↓ 获得 JWT ↓ 请求菜单 API ↓ 后端获取用户角色 ↓ Role ↓ Menu ↓ 返回菜单树 ↓ React 动态生成路由ShiyuAdmin 的菜单树接口会根据当前用户角色和菜单关联关系过滤菜单超级管理员直接拿完整菜单普通用户则获得自己的授权菜单并补全需要的父菜单节点。假设数据库系统管理 ├── 用户管理 ├── 角色管理 ├── 菜单管理 └── 部门管理运营人员只有用户管理那么接口可能返回[{name:系统管理,children:[{name:用户管理,path:/system/user}]}]前端就只渲染系统管理 └── 用户管理这才是完整的权限体验。十三、为什么还要自动补全父菜单这是动态菜单里面一个很细但非常实用的问题。例如系统管理 └── 用户管理用户只有用户管理对应权限。那么数据库查询出来可能只有用户管理但如果直接返回用户管理前端菜单树就可能丢掉系统管理这一层。所以 ShiyuAdmin 在普通用户菜单过滤时还会补全父级菜单以形成完整菜单树。这就是后台开发里非常典型的看起来简单实现时才会发现有很多边界细节。十四、账号禁用也是权限系统的一部分ShiyuAdmin 的用户模型里还有Statusint项目约定1 启用 0 禁用登录时会检查ifuser.Status!1{returnnil,errors.New(账号已停用)}也就是说Status的优先级实际上比 RBAC 更早。完整流程Username Password ↓ 用户是否存在 ↓ 密码是否正确 ↓ Status 1 ? ↓ 生成 JWT ↓ RBAC如果Status 0连 Token 都不会生成。而且这个限制连超级管理员同样适用。这个设计我认为很有必要。因为权限系统里一定要存在一个紧急封禁能力。比如员工离职不用删除账号直接UPDATEsys_usersSETstatus0WHEREuser_codeU10001;即可。这样历史日志 操作记录 数据关联 审计信息全部仍然保留。十五、从数据库角度看 RBAC如果我们自己重新设计一套大概至少需要sys_users sys_roles sys_menus sys_user_roles sys_role_menus用户表CREATETABLEsys_users(idBIGINTPRIMARYKEY,user_codeVARCHAR(32),usernameVARCHAR(64),passwordVARCHAR(255),statusINT,is_super_adminBOOLEAN);角色CREATETABLEsys_roles(idBIGINTPRIMARYKEY,role_codeVARCHAR(64),role_nameVARCHAR(128));菜单CREATETABLEsys_menus(idBIGINTPRIMARYKEY,menu_codeVARCHAR(64),menu_nameVARCHAR(128),parent_idBIGINT,pathVARCHAR(255),permsVARCHAR(255));用户角色关系CREATETABLEsys_user_roles(user_idBIGINT,role_idBIGINT);角色菜单关系CREATETABLEsys_role_menus(role_idBIGINT,menu_idBIGINT);于是查询权限SELECTDISTINCTm.permsFROMsys_users uJOINsys_user_roles urONu.idur.user_idJOINsys_roles rONr.idur.role_idJOINsys_role_menus rmONr.idrm.role_idJOINsys_menus mONm.idrm.menu_idWHEREu.id?整个 RBAC 的本质其实就是关系建模 权限查询 中间件拦截十六、RBAC 为什么适合大多数后台管理系统因为现实中的组织本身就是按照角色工作的。例如管理员 运营 销售 财务 客服 开发 产品所以权限建模天然就是用户 ↓ 角色 ↓ 资源而不是每个人单独配置几百个权限假设有1000 个用户但角色可能只有20 个权限维护成本会大幅下降。十七、RBAC 也不是万能的继续往复杂系统发展以后会出现RBAC 不够用的情况。例如张三和李四都是销售经理角色一样。但是张三只能看华北区数据 李四只能看华南区数据这时候Role已经不能完全表达权限。这就进入Data Permission数据权限。例如角色权限 部门权限 数据范围可以扩展全部数据 本部门数据 本部门及子部门 本人数据 自定义部门查询 SQL 时增加WHEREdept_codeIN(...)甚至进一步发展成RBAC ABACABACAttribute-Based Access Control也就是基于属性的访问控制例如user.department order.department或者user.region resource.region再复杂一点甚至可以user.level resource.required_level所以一个后台权限系统通常会经历RBAC ↓ RBAC Data Scope ↓ RBAC ABAC ↓ Policy Engine十八、如果继续升级 ShiyuAdmin我会怎么做基于现在这套结构我觉得可以继续往几个方向扩展。首先是按钮级权限例如system:user:list system:user:create system:user:update system:user:delete system:user:export system:user:reset-password这样页面不仅控制菜单用户管理还可以控制新增 编辑 删除 导出 重置密码其次是数据权限增加data_scope例如ALL DEPT DEPT_AND_CHILD SELF CUSTOM然后是权限缓存用户每次请求都去User Role Menu Permission查数据库没有必要。可以做Redis例如permission:user:U10001内容[system:user:list,system:user:create,system:role:list]权限修改删除 Redis下一次请求重新加载这种方式比较适合请求量更大的后台系统。十九、一个完整的企业后台权限体系最终完整一点的架构可能会变成用户 │ ▼ JWT 登录认证 │ ▼ Authentication │ ▼ 当前用户 User │ ▼ IsSuperAdmin? │ │ YES NO │ │ ▼ ▼ PASS Role │ ▼ Menu │ ▼ Permission │ ▼ Data Scope │ ▼ API前端JWT ↓ 获取菜单 ↓ 动态菜单 ↓ 动态路由 ↓ 按钮权限后端JWT Middleware ↓ Permission Middleware ↓ Data Permission ↓ Service ↓ Database这样才算形成一个比较完整的后台权限闭环。二十、ShiyuAdmin 这个项目适合拿来干什么我觉得主要有三个用途。第一直接作为后台脚手架如果你要开发SaaS 后台 AI 平台后台 商城后台 CRM ERP 内容管理系统 内部运营系统都可以基于这种通用后台继续开发。第二学习 Go Web 工程化因为这里不是简单的Gin CRUD Demo而是包括JWT RBAC Redis Gorm Middleware 日志 系统监控 Docker 前后端分离这些实际项目中的东西。第三学习权限系统设计尤其是User Role Menu Permission JWT Middleware Dynamic Menu Super Admin这一整条链路。ShiyuAdmin README 目前也把它定位为一个适合快速搭建中后台、学习 RBAC 权限模型以及作为新业务系统基础脚手架的开源项目。最后很多人学习后台管理系统时会觉得用户管理 角色管理 菜单管理不就是几个 CRUD 吗实际上真正有价值的并不是这些 CRUD。而是它们背后的用户是谁 用户拥有什么角色 角色拥有哪些资源 当前接口需要什么权限 前端菜单如何动态生成 超级管理员如何绕过权限 账号禁用如何立即生效 权限变化后 Token 怎么处理这些问题。真正把这一整套想明白之后再去开发SaaS ERP CRM AI 管理平台 商城后台 内部管理系统基本都会遇到同样的设计思想。这也是我认为ShiyuAdmin这个项目比较适合拿出来拆解学习的地方。它并不只是一个Go React 的后台页面更重要的是可以通过一个真实开源项目理解一个通用后台系统到底应该怎么做认证、授权和权限管理。项目地址GitHubhttps://github.com/Rodert/ShiyuAdmin项目名称ShiyuAdmin 仕宇通用管理后台技术栈Go Gin Gorm React Ant Design Pro JWT RBAC Redis Docker Compose作者王仕宇JavaPub