Go语言企业级权限管理系统设计与实现
最近跟着学长再写河南师范大学附属中学图书馆的项目,学长交给了我一个任务,把本项目的权限管理给吃透,然后应用到下一个项目上。
我当然是偷着乐呐,因为读代码的时候,总是莫名给我一种公费旅游的感觉。
本来就想去了解图书管理这个项目的全貌。但一直腾不出时间。
现在正巧,我要写一个权限管理,正好可以拐回来细细品读图书管理系统的代码( ̄﹃ ̄)。
熟悉的配方,本项目使用的是RBAC模型来管理权限。
目录
一、RBAC
1、传统方案
2、如何通过RBAC改进?
3、如何设计代码?
二、中间件设计
1、理论设计方式
2、图书馆项目的设计
a.跳过重复路径
b.获取JWT凭证
可以拓展一下(AI):
三、基于图书馆的权限树设计
1、权限树设计
2、权限层级映射:
四、图书馆项目
1、整体架构:
2、核心组件解析
a、 用户信息结构 (UserInfo)
b、角色模型 (Role)
c、权限树结构 (Permission)
3、权限设计层级
a、三级权限结构:
五、前端如何进行权限控制
现实场景:
完整权限控制
首先从后端获取权限数据
第一层防护:菜单不显示
第二层防护:系统管理子菜单过滤
第三层防护:权限指令控制
第四层防护:组合式函数权限检查
第五层防护:直接URL访问
收获:
一、RBAC
为什么需要RBAC来管理项目的呢
大家可以想象这样一个场景:
想象你是一所大学图书馆的IT负责人。新学期开始了,
图书馆迎来了以下用户:
学生小王:只想借书还书,查看自己的借阅记录
老师张三:除了借书,还需要帮学生查询图书,管理班级借阅情况
管理员李四:需要添加新书、管理用户账号、查看所有借阅统计
系统管理员王五:拥有系统的完全控制权,包括备份数据、修改系统配置
假设没有权限管理,可能会发生什么事情呢?
学生小王误点了\"删除所有图书\"按钮
老师张三想查看其他班级的借阅情况被拒绝了
管理员李四无法访问系统设置,找你求助
......
所以权限管理,是非常必要的!!
现在问题来了:在咱们项目中,如何为他们添加权限?
1、传统方案
传统的解决方式是什么?在代码里写死:
// 传统方式:硬编码权限检查func DeleteBook(userType string) { if userType == \"student\" { return // 学生不能删除 } if userType == \"teacher\" { return // 老师也不能删除 } if userType == \"admin\" { // 只有管理员能删除 deleteBook() }}
每次有新角色加入,你都要修改代码...
这样写有什么问题?
1. 新增一个\"图书管理员\"角色,要改遍所有函数
2. 权限规则散落在各处,难以维护
3. 想临时给某个老师管理员权限?改代码重新部署!
我在面向对象的七大设计原则一文中提到,接口的设计中的开闭原则中的,闭原则,就是为了解决解决每次有新改动,就要修改原有的代码。
所以直接把代码写死,极其不合理,那该如何解决?
2、如何通过RBAC改进?
什么是RBAC呢?
大家可以想象到这样一种场景:
公司的门禁卡系统
- 员工卡:只能进办公区
- 管理卡:能进办公区+会议室
- 主管卡:能进所有区域
这就是灵感:能不能给用户分配\"权限卡\"?
咱们可以这样设计系统:
用户(User) ←→ 角色(Role) ←→ 权限(Permission)
这里的角色就相当于上方公司的门禁卡。
具体来说:
- 小王 → 学生角色 → [借书, 还书, 查看个人记录]
- 张三 → 教师角色 → [借书, 还书, 查看班级记录, 推荐图书]
- 李四 → 管理员角色 → [所有学生权限 + 添加图书 + 用户管理]
3、如何设计代码?
第一步:定义角色和权限
// 不再硬编码,而是用数据库存储1、定义角色type Role struct { ID uint `json:\"id\"` Name string `json:\"name\"` // \"学生\", \"教师\", \"管理员\" Description string `json:\"description\"` // \"普通学生用户\"}2、定义权限type Permission struct { ID uint `json:\"id\"` Name string `json:\"name\"` // \"book:borrow\", \"user:create\" Action string `json:\"action\"` // \"借阅图书\", \"创建用户\"}
第二步:新方式如何检查权限
// 现在的权限检查func DeleteBook(userID uint) error { if !permission.HasPermission(userID, \"book:delete\") { return errors.New(\"权限不足:您无法删除图书\") } return deleteBook()} 新增角色?只需要配置数据,无需改代码!// 2. 权限检查 - 如何工作的?func (r *RoleService) HasPermission(roleID uint, permission string) bool { // 具体的权限验证逻辑 // 为什么这样设计?}
第三步:前后对比
// 传统方式:硬编码权限if userType == \"teacher\" { // 教师相关操作} else if userType == \"student\" { // 学生相关操作}// 问题:新增角色需要修改代码// 你的方案:动态权限if permission.HasRole(user.RoleID, \"teacher\") { // 教师相关操作}// 优势:新增角色只需要配置数据
哈哈,这就像及了接口(interface)的设计方式。
让人感觉赏心悦目。
二、中间件设计
虽然在引入RBAC后,确实能优化代码,但是又遇到了新问题。
每个API都要手动检查权限,代码重复。
如下,这里是调用DeleteBook,需要permission认证
// 现在的权限检查func DeleteBook(userID uint) error { if !permission.HasPermission(userID, \"book:delete\") { return errors.New(\"权限不足:您无法删除图书\") } return deleteBook()}
如果咱们调用其他不同的函数,你都需要在每个函数上添加如下这段代码:
if !permission.HasPermission(userID, \"book:delete\") { return errors.New(\"权限不足:您无法删除图书\") }
是不是特别麻烦(~ ̄▽ ̄)~
1、理论设计方式
咱们可以设计成如下,通过middleware中间件:
// 展示如何在路由中应用权限中间件router.POST(\"/role\", middleware.RequirePermission(\"role:create\"), roleHandler.CreateRole)router.GET(\"/role\", middleware.RequirePermission(\"role:read\"), roleHandler.GetRoles)
2、图书馆项目的设计
// - 1. 路径跳过检查 - 检查当前请求路径是否在跳过列表中,如果是则直接放行// - 2. 获取用户信息 - 从请求头 X-Userinfo 中获取 Base64 编码的用户信息// - 3. 解码和反序列化 - 将 Base64 字符串解码后,反序列化为 UserInfo 结构体// - 4. 租户ID处理 - 清理租户ID格式(移除前导斜杠),从请求头获取目标租户ID// - 5. 权限验证 - 检查用户是否有权限访问请求的租户(用户的租户列表中是否包含目标租户)// - 6. 设置上下文 - 验证通过后,将用户信息和租户ID设置到 Gin 上下文中供后续使用func Auth() gin.HandlerFunc {return AuthWithConfig(AuthConfig{})}func AuthWithConfig(config AuthConfig) gin.HandlerFunc {notAuth := config.SkipPathsvar skip map[string]struct{}if len(notAuth) > 0 {skip = make(map[string]struct{})for _, path := range notAuth {skip[path] = struct{}{}}}return func(c *gin.Context) {if _, ok := skip[c.FullPath()]; ok {c.Next()return}userInfos := c.Request.Header.Get(\"X-Userinfo\")if userInfos == \"\" {err := errs.NewUnauthorizedError(\"missing user information\")response.BuildErrorResponse(err, c)c.Abort()return}userProfile := &userModel.UserInfo{}user, err := base64.StdEncoding.DecodeString(userInfos)if err != nil {logrus.Error(\"x-userinfo base64 decoding failed\", err)err = errs.NewUnauthorizedError(\"invalid user info encoding\")response.BuildErrorResponse(err, c)c.Abort()return}err = json.Unmarshal(user, &userProfile)if err != nil {logrus.Error(\"x-userinfo json unmarshal failed\", err)err = errs.NewUnauthorizedError(\"invalid user info format\")response.BuildErrorResponse(err, c)c.Abort()return}// Remove the leading slash from each tenant IDfor i, tenantId := range userProfile.TenantIds {if len(tenantId) > 0 && tenantId[0] == \'/\' {userProfile.TenantIds[i] = tenantId[1:]}}// Get tenantId from request headerrequestTenantId := c.GetHeader(\"tenantId\")c.Set(\"tenantId\", requestTenantId)if requestTenantId == \"\" && len(userProfile.TenantIds) > 0 {requestTenantId = userProfile.TenantIds[0]c.Set(\"tenantId\", requestTenantId)}if requestTenantId == \"\" {logrus.Error(\"Missing tenantId in request header\")err = errs.NewUnauthorizedError(\"missing tenantId in request header\")response.BuildErrorResponse(err, c)c.Abort()return}// Check if the requested tenantId is in user\'s tenant listauthorized := falsefor _, tenantId := range userProfile.TenantIds {if tenantId == requestTenantId {authorized = truebreak}}if !authorized {logrus.Warnf(\"User attempted to access unauthorized tenant: %s\", requestTenantId)err = errs.NewUnauthorizedError(\"unauthorized tenant access\")response.BuildErrorResponse(err, c)c.Abort()return}// Authorized, continuec.Set(\"user\", userProfile)c.Next()}}
咱们在这里详细解释一下代码:
a.跳过重复路径
// 从配置中获取,需要跳过的路径// 通过map存储实现O(1)查询notAuth := config.SkipPathsvar skip map[string]struct{}if len(notAuth) > 0 {skip = make(map[string]struct{})for _, path := range notAuth {skip[path] = struct{}{}}}// 跳过if _, ok := skip[c.FullPath()]; ok {c.Next()return}
b.获取JWT凭证
// 1. 获取网关传递的用户信息 userInfos := c.Request.Header.Get(\"X-Userinfo\")....// 2. Base64解码userProfile := &userModel.UserInfo{}user, err := base64.StdEncoding.DecodeString(userInfos).... // 3. JSON反序列化为用户对象err = json.Unmarshal(user, &userProfile)....// 4. 租户权限验证 ....
认证的思路如下:
客户端 → 网关/认证服务 → 业务服务 ↓ JWT验证/登录 ↓ 生成用户信息 ↓ Base64编码后放入Header ↓ 转发到后端服务
这里的采用的是第三方验证身份,并且采用Keycloak解决问题
Keycloak 是一个开源的身份和访问管理(IAM)解决方案
可以拓展一下(AI):
1.单点登录(SSO)
- 用户只需登录一次,即可访问多个应用系统
- 支持SAML 2.0、OpenID Connect、OAuth 2.0等标准协议
2.身份认证- 用户名密码认证
- 多因素认证(MFA)
- 社交登录(Google、Facebook、GitHub等)
- LDAP/Active Directory集成
3.授权管理- 基于角色的访问控制(RBAC)
- 细粒度权限控制
- 资源和策略管理
4.用户管理- 用户注册、密码重置
- 用户组织和角色分配
- 用户会话管理
图书馆项目生成用于验证的JWT的方式
本项目JWT令牌的生成方式通过对项目代码的深入分析,我发现本项目的JWT令牌生成采用了以下架构:JWT令牌生成流程1. Keycloak作为JWT令牌签发中心- 项目使用 `keycloak.go` 中的 `GetAdminToken` 方法- 通过调用 k.client.LoginAdmin() 向Keycloak服务器请求JWT令牌- 使用配置文件中的管理员账户(AdminUser/AdminPass)进行认证2. JWT令牌的具体生成过程```token, err := k.client.LoginAdmin(k.ctx, global.Config.Keycloak.AdminUser, global.Config.Keycloak.AdminPass, \"master\")```3. 令牌使用场景- 管理操作 :在用户创建、更新、删除等管理操作中使用- 权限验证 :通过 `auth.go` 中间件验证用户身份- API调用 :所有需要认证的API都通过JWT令牌进行权限控制
JWT是在创建角色的时候生成的,有兴趣的可以了解一下:
// CreateUser 在 Keycloak 中创建新用户// 实现了完整的用户创建流程,包括权限分配和事务回滚func (k *KeycloakService) CreateUser(req *UserCreateRequest) (string, error) { // 步骤1: 获取 Keycloak 管理员访问令牌 token, err := k.GetAdminToken() if err != nil { logrus.Error(err) return \"\", err } // 步骤2: 检查用户名(身份证号)是否已存在 exists, err := k.CheckUsernameExists(req.IdNumber) if err != nil { logrus.Error(err) return \"\", err } if exists { logrus.Errorf(\"User with idNumber %s already exists\", req.IdNumber) return \"\", errors.NewResourceAlreadyExistError(\"身份证重复!\") } // 步骤3: 设置默认密码(如果未提供) if len(req.Password) == 0 { req.Password = \"Aa123456\" // 建议:提取为配置项 } // 步骤4: 构建 Keycloak 用户对象 keycloakUser := gocloak.User{ Username: gocloak.StringP(req.IdNumber), // 使用身份证作为用户名 Enabled: gocloak.BoolP(true), // 启用用户 LastName: gocloak.StringP(req.Name), // 设置姓名 Credentials: &[]gocloak.CredentialRepresentation{ { Type: gocloak.StringP(\"password\"), Value: gocloak.StringP(req.Password), Temporary: gocloak.BoolP(false), // 非临时密码 }, }, } // 步骤5: 在 Keycloak 中创建用户 userID, err := k.client.CreateUser(k.ctx, token.AccessToken, k.realm, keycloakUser) if err != nil { logrus.Errorf(\"Failed to create user %s in realm %s: %v\", req.Name, k.realm, err) return \"\", err } // 步骤6: 添加用户到指定组(带事务回滚) err = k.AddUserToGroup(userID, req.GroupName) if err != nil { logrus.Errorf(\"Failed to add user %s to group %s: %v\", userID, req.GroupName, err) // 回滚:删除已创建的用户 if rollbackErr := k.DeleteUser(userID); rollbackErr != nil { logrus.Errorf(\"Rollback failed: %v\", rollbackErr) } return \"\", err } // 步骤7: 为用户分配角色(带事务回滚) err = k.AddRoleToUser(userID, req.Role) if err != nil { logrus.Errorf(\"Failed to add role %s to user %s: %v\", req.Role, userID, err) // 回滚:删除已创建的用户 if rollbackErr := k.DeleteUser(userID); rollbackErr != nil { logrus.Errorf(\"Rollback failed: %v\", rollbackErr) } return \"\", err } return userID, nil}
三、基于图书馆的权限树设计
1、权限树设计
// Permission 权限结构体type Permission struct {Key string `json:\"key\"`Title string `json:\"title\"`Children []Permission `json:\"children,omitempty\"`}// DefaultPermissions 默认权限树结构var DefaultPermissions = []Permission{{Key: \"home\",Title: \"首页\",},{Key: \"bookshelf\",Title: \"个人书架\",},{Key: \"borrow-history\",Title: \"借阅记录\",},{Key: \"activity-center\",Title: \"活动中心\",},{Key: \"message-center\",Title: \"消息中心\",},{Key: \"system-manage\",Title: \"系统管理\",Children: []Permission{{Key: \"book-manage\",Title: \"图书管理\",Children: []Permission{{Key: \"book-entry\", Title: \"图书录入\"},{Key: \"book-list\", Title: \"图书列表\"},{Key: \"book-recommend\", Title: \"图书推荐\"},{Key: \"book-check\", Title: \"图书清查\"},},},{Key: \"borrow-manage\",Title: \"借阅管理\",Children: []Permission{{Key: \"book-borrow\", Title: \"图书借阅\"},{Key: \"book-return\", Title: \"图书归还\"},{Key: \"flow-approve\", Title: \"漂流审批\"},{Key: \"reserve-list\", Title: \"候补列表\"},{Key: \"borrow-record\", Title: \"借阅记录\"},},},{Key: \"activity-manage\",Title: \"活动管理\",Children: []Permission{{Key: \"activity-create\", Title: \"活动创建\"},{Key: \"activity-approve\", Title: \"活动审批\"},{Key: \"activity-list\", Title: \"活动列表\"},},},{Key: \"notice-manage\",Title: \"通知管理\",Children: []Permission{{Key: \"notice-create\", Title: \"通知创建\"},{Key: \"notice-list\", Title: \"通知列表\"},},},{Key: \"system-setting\",Title: \"系统设置\",Children: []Permission{{Key: \"user-manage\", Title: \"读者管理\"},{Key: \"role-manage\", Title: \"角色配置\"},{Key: \"system-configure\", Title: \"系统配置\"},{Key: \"grade-configure\", Title: \"年级配置\"},{Key: \"venue-configure\", Title: \"馆场地配置\"},{Key: \"activity-configure\", Title: \"活动配置\"},},},},},}
2、权限层级映射:
大白话来说就是能快速找到子节点与父节点之间的关系
// BuildPermissionParentMap 从DefaultPermissions构建权限层级关系映射func BuildPermissionParentMap() map[string]string {parentMap := make(map[string]string)buildParentMapRecursive(DefaultPermissions, \"\", parentMap)return parentMap}// buildParentMapRecursive 递归构建权限父子关系映射// 能够快速找到子权限的父权限func buildParentMapRecursive(permissions []Permission, parentKey string, parentMap map[string]string) {for _, perm := range permissions {if parentKey != \"\" {parentMap[perm.Key] = parentKey}if len(perm.Children) > 0 {buildParentMapRecursive(perm.Children, perm.Key, parentMap)}}}
四、图书馆项目
1、整体架构:
用户(User) → 角色(Role) → 权限(Permission) → 资源(Resource) ↓ ↓ ↓ ↓ 身份认证 角色分配 权限控制 资源访问
2、核心组件解析
a、 用户信息结构 (UserInfo)
type UserInfo struct { Name string // 用户姓名 Username string // 用户名 AccountId string // 账户ID Roles []string // 用户角色列表 TenantIds []string // 租户ID列表(多租户支持) // ... 其他字段}
b、角色模型 (Role)
type Role struct { Name string // 角色名称 Description string // 角色描述 BorrowLimit int // 借阅数量限制 BorrowDays int // 借阅天数限制 TenantId string // 租户ID Permissions string // 权限配置JSON Status enum.Status // 状态}
c、权限树结构 (Permission)
type Permission struct { Key string // 权限标识 Title string // 权限名称 Children []Permission // 子权限}
3、权限设计层级
a、三级权限结构:
1. 一级权限 :模块级别(如:系统管理)
2.二级权限 :功能级别(如:图书管理)
3.三级权限 :操作级别(如:图书录入、图书列表)
系统管理 (system-manage)├── 图书管理 (book-manage)│ ├── 图书录入 (book-entry)│ ├── 图书列表 (book-list)│ └── 图书推荐 (book-recommend)├── 借阅管理 (borrow-manage)│ ├── 图书借阅 (book-borrow)│ └── 图书归还 (book-return)└── 系统设置 (system-setting) ├── 读者管理 (user-manage) └── 角色配置 (role-manage)
五、前端如何进行权限控制
现实场景:
假设:
一个普通读者(角色:student)
他的权限只有 [\"home\", \"personal-bookshelf\", \"borrow-record\", \"activity-center\", \"message-center\"] ,想要访问\"读者管理\"页面。
完整权限控制
用户登录 ↓后端返回用户权限列表: [\"home\", \"personal-bookshelf\", \"borrow-record\", \"activity-center\", \"message-center\"] ↓前端存储权限到 Pinia Store ↓菜单渲染时过滤权限 ↓系统管理菜单不显示(因为没有任何系统管理权限) ↓用户无法通过正常途径访问读者管理页面 ↓即使通过直接URL访问,组件内部也会进行权限检查 ↓最终被拒绝访问或跳转到403页面
首先从后端获取权限数据
当用户登录后,前端会调用 `user.ts` 中的 fetchAndSetStaffInfo() 方法:
async fetchAndSetStaffInfo() { try { this.isLoading = true; const response = await getCurrentStaff(); // 调用后端API获取用户信息 if (response && (response as any).data && (response as any).code === 0) { const staffData = (response as any).data; // 设置用户权限 this.permissions = staffData.permissions || []; // 普通读者只有基础权限 } } catch (error) { console.error(\'获取用户信息失败:\', error); }}
结果:
普通读者的 permissions 数组为: [\"home\", \"personal-bookshelf\", \"borrow-record\", \"activity-center\", \"message-center\"] , 不包含 \"user-manage\" 权限。
第一层防护:菜单不显示
在 `index.vue` 中,菜单会根据权限进行过滤:
const filterRoute = (routeList: TRouter[], currentPermissions: string[]) => { // 检查用户是否有系统管理权限 const hasSystemManagePermission = systemManagePermissions.some((permission) => currentPermissions.includes(permission), ); for (let i = routeList.length - 1; i >= 0; i--) { const route = routeList[i]; const routeName = route.name as string; // 特殊处理系统管理菜单 if (routeName === \'SystemManage\') { if (!hasSystemManagePermission) { routeList.splice(i, 1); // 移除系统管理菜单 } } }};
结果:
由于普通读者没有任何系统管理相关权限(如 user-manage 、 role-manage 等),整个\"系统管理\"菜单都不会显示在导航栏中。
第二层防护:系统管理子菜单过滤
即使用户通过某种方式进入了系统管理页面,在 `layout.vue` 中还有二级权限过滤:
// 菜单权限映射const menuPermissionMap = { \'user-manage\': \'user-manage\', \'role-manage\': \'role-manage\', // ... 其他权限映射};// 根据权限过滤菜单组const filteredMenuGroups = computed(() => { return menuGroups .map((group) => ({ ...group, items: group.items.filter((item) => { const requiredPermission = menuPermissionMap[item.key]; return !requiredPermission || permissions.value.includes(requiredPermission); }), })) .filter((group) => group.items.length > 0); // 过滤掉没有可用菜单项的组});
结果:
\"读者管理\" 菜单项不会出现在系统管理的侧边栏中。
第三层防护:权限指令控制
在具体的页面组件中,还可以使用权限指令 `permission.ts` 来控制元素显示:
读者管理
权限指令的实现:
const permission: Directive = { mounted(el: HTMLElement, binding) { const { value } = binding; const user = useUserStore(); const { permissions } = user; if (value) { let hasPermission = false; if (typeof value === \'string\') { hasPermission = permissions.includes(value); // 检查是否有该权限 } if (!hasPermission) { el.style.display = \'none\'; // 没有权限则隐藏元素 } } },};
结果: 任何带有 v-permission=\"\'user-manage\'\" 指令的元素都会被隐藏。
第四层防护:组合式函数权限检查
在组件逻辑中,可以使用 `usePermission.ts` 进行权限检查:
export function usePermission() { const user = useUserStore(); // 检查是否有指定权限 const hasPermission = (permission: string): boolean => { return user.hasPermission(permission); }; return { hasPermission, // ... 其他权限检查方法 };}
在组件中使用:
import { usePermission } from \'@/hooks/usePermission\';const { hasPermission } = usePermission();// 检查权限if (!hasPermission(\'user-manage\')) { // 没有权限,执行相应逻辑 router.push(\'/403\'); // 跳转到无权限页面}
第五层防护:直接URL访问
如果用户直接在浏览器地址栏输入 /systemManage/user-manage :
1、路由存在 :路由配置中确实有这个路径
2、组件加载 :UserManage 组件会被加载
3、权限检查 :组件内部会进行权限检查
4、访问被拒绝 :如果没有权限,会显示无权限提示或跳转到403页面
收获:
在学习权限控制的时候,由于我需要专门设计一套简单的权限控制,我专门找来我们的前端。
想要深入了解一下,我后端传递数据到前端后,前端进行的权限控制流程。
浏览器上的页面是静态页面,当点击发送url时,会被前端拦截(Vue Router)的工作原理;
然后经过代码书写的一系列操作之后,在传递到后端,
后端返回的具体数据,是先返回到前端,
经前端处理,才最终到显示的页面。
网站:
1、活动广场 - 河南师范大学附属中学图书馆