首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go语言企业级权限管理系统设计与实现

Go语言企业级权限管理系统设计与实现

作者头像
十二.
发布2025-10-22 14:10:00
发布2025-10-22 14:10:00
9200
代码可运行
举报
运行总次数:0
代码可运行

最近跟着学长再写河南师范大学附属中学图书馆的项目,学长交给了我一个任务,把本项目的权限管理给吃透,然后应用到下一个项目上。

我当然是偷着乐呐,因为读代码的时候,总是莫名给我一种公费旅游的感觉。 本来就想去了解图书管理这个项目的全貌。但一直腾不出时间。 现在正巧,我要写一个权限管理,正好可以拐回来细细品读图书管理系统的代码( ̄﹃ ̄)。

熟悉的配方,本项目使用的是RBAC模型来管理权限。


一、RBAC

为什么需要RBAC来管理项目的呢

大家可以想象这样一个场景:

想象你是一所大学图书馆的IT负责人。新学期开始了, 图书馆迎来了以下用户:

学生小王:只想借书还书,查看自己的借阅记录 老师张三:除了借书,还需要帮学生查询图书,管理班级借阅情况 管理员李四:需要添加新书、管理用户账号、查看所有借阅统计 系统管理员王五:拥有系统的完全控制权,包括备份数据、修改系统配置

假设没有权限管理,可能会发生什么事情呢?

学生小王误点了"删除所有图书"按钮 老师张三想查看其他班级的借阅情况被拒绝了 管理员李四无法访问系统设置,找你求助 ......

所以权限管理,是非常必要的!!

现在问题来了:在咱们项目中,如何为他们添加权限?

1、传统方案

传统的解决方式是什么?在代码里写死:

代码语言:javascript
代码运行次数:0
运行
复制
// 传统方式:硬编码权限检查
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、如何设计代码?

第一步:定义角色权限

代码语言:javascript
代码运行次数:0
运行
复制
// 不再硬编码,而是用数据库存储
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"` // "借阅图书", "创建用户"
}

第二步:新方式如何检查权限

代码语言:javascript
代码运行次数:0
运行
复制
// 现在的权限检查
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 {
    // 具体的权限验证逻辑
    // 为什么这样设计?
}

第三步:前后对比

代码语言:javascript
代码运行次数:0
运行
复制
// 传统方式:硬编码权限
if userType == "teacher" {
    // 教师相关操作
} else if userType == "student" {
    // 学生相关操作
}
// 问题:新增角色需要修改代码

// 你的方案:动态权限
if permission.HasRole(user.RoleID, "teacher") {
    // 教师相关操作
}
// 优势:新增角色只需要配置数据

哈哈,这就像及了接口(interface)的设计方式。 让人感觉赏心悦目。

二、中间件设计

虽然在引入RBAC后,确实能优化代码,但是又遇到了新问题。

每个API都要手动检查权限,代码重复。

如下,这里是调用DeleteBook,需要permission认证

代码语言:javascript
代码运行次数:0
运行
复制
// 现在的权限检查
func DeleteBook(userID uint) error {
    if !permission.HasPermission(userID, "book:delete") {
        return errors.New("权限不足:您无法删除图书")
    }
    return deleteBook()
}

如果咱们调用其他不同的函数,你都需要在每个函数上添加如下这段代码:

代码语言:javascript
代码运行次数:0
运行
复制
 if !permission.HasPermission(userID, "book:delete") {
        return errors.New("权限不足:您无法删除图书")
    }

是不是特别麻烦(~ ̄▽ ̄)~

1、理论设计方式

咱们可以设计成如下,通过middleware中间件:

代码语言:javascript
代码运行次数:0
运行
复制
// 展示如何在路由中应用权限中间件
router.POST("/role", middleware.RequirePermission("role:create"), roleHandler.CreateRole)
router.GET("/role", middleware.RequirePermission("role:read"), roleHandler.GetRoles)
2、图书馆项目的设计
代码语言:javascript
代码运行次数:0
运行
复制
// - 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.SkipPaths
	var 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 ID
		for i, tenantId := range userProfile.TenantIds {
			if len(tenantId) > 0 && tenantId[0] == '/' {
				userProfile.TenantIds[i] = tenantId[1:]
			}
		}

		// Get tenantId from request header
		requestTenantId := 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 list
		authorized := false
		for _, tenantId := range userProfile.TenantIds {
			if tenantId == requestTenantId {
				authorized = true
				break
			}
		}

		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, continue
		c.Set("user", userProfile)
		c.Next()
	}
}

咱们在这里详细解释一下代码:

a.跳过重复路径
代码语言:javascript
代码运行次数:0
运行
复制
// 从配置中获取,需要跳过的路径	
// 通过map存储实现O(1)查询
notAuth := config.SkipPaths
	var 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凭证
代码语言:javascript
代码运行次数:0
运行
复制
// 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. 租户权限验证
        ....

认证的思路如下:

代码语言:javascript
代码运行次数:0
运行
复制
客户端 → 网关/认证服务 → 业务服务
       ↓
   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的方式

代码语言:javascript
代码运行次数:0
运行
复制
本项目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是在创建角色的时候生成的,有兴趣的可以了解一下:

代码语言:javascript
代码运行次数:0
运行
复制
// 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、权限树设计
代码语言:javascript
代码运行次数:0
运行
复制
// 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、权限层级映射:

大白话来说就是能快速找到子节点父节点之间的关系

代码语言:javascript
代码运行次数:0
运行
复制
// 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、整体架构:
代码语言:javascript
代码运行次数:0
运行
复制
用户(User) → 角色(Role) → 权限(Permission) → 资源(Resource)
     ↓           ↓           ↓              ↓
  身份认证    角色分配    权限控制      资源访问
2、核心组件解析
a、 用户信息结构 (UserInfo)
代码语言:javascript
代码运行次数:0
运行
复制
type UserInfo struct {
    Name        string   // 用户姓名
    Username    string   // 用户名
    AccountId   string   // 账户ID
    Roles       []string // 用户角色列表
    TenantIds   []string // 租户ID列表(多租户支持)
    // ... 其他字段
}
b、角色模型 (Role)
代码语言:javascript
代码运行次数:0
运行
复制
type Role struct {
    Name        string      // 角色名称
    Description string      // 角色描述
    BorrowLimit int         // 借阅数量限制
    BorrowDays  int         // 借阅天数限制
    TenantId    string      // 租户ID
    Permissions string      // 权限配置JSON
    Status      enum.Status // 状态
}
c、权限树结构 (Permission)
代码语言:javascript
代码运行次数:0
运行
复制
type Permission struct {
    Key      string       // 权限标识
    Title    string       // 权限名称
    Children []Permission // 子权限
}
3、权限设计层级
a、三级权限结构:

1. 一级权限 :模块级别(如:系统管理) 2.二级权限 :功能级别(如:图书管理) 3.三级权限 :操作级别(如:图书录入、图书列表)

代码语言:javascript
代码运行次数:0
运行
复制
系统管理 (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"] ,想要访问"读者管理"页面。

完整权限控制
代码语言:javascript
代码运行次数:0
运行
复制
用户登录
    ↓
后端返回用户权限列表: ["home", "personal-bookshelf", "borrow-record", "activity-center", "message-center"]
    ↓
前端存储权限到 Pinia Store
    ↓
菜单渲染时过滤权限
    ↓
系统管理菜单不显示(因为没有任何系统管理权限)
    ↓
用户无法通过正常途径访问读者管理页面
    ↓
即使通过直接URL访问,组件内部也会进行权限检查
    ↓
最终被拒绝访问或跳转到403页面
首先从后端获取权限数据

当用户登录后,前端会调用 `user.ts` 中的 fetchAndSetStaffInfo() 方法:

代码语言:javascript
代码运行次数:0
运行
复制
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` 中,菜单会根据权限进行过滤:

代码语言:javascript
代码运行次数:0
运行
复制
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` 中还有二级权限过滤:

代码语言:javascript
代码运行次数:0
运行
复制
// 菜单权限映射
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` 来控制元素显示:

代码语言:javascript
代码运行次数:0
运行
复制
<!-- 在任何组件中使用权限指令 -->
<a-button v-permission="'user-manage'" type="primary">
  读者管理
</a-button>

权限指令的实现:

代码语言:javascript
代码运行次数:0
运行
复制
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` 进行权限检查:

代码语言:javascript
代码运行次数:0
运行
复制
export function usePermission() {
  const user = useUserStore();

  // 检查是否有指定权限
  const hasPermission = (permission: string): boolean => {
    return user.hasPermission(permission);
  };

  return {
    hasPermission,
    // ... 其他权限检查方法
  };
}

在组件中使用:

代码语言:javascript
代码运行次数:0
运行
复制
<script setup>
import { usePermission } from '@/hooks/usePermission';

const { hasPermission } = usePermission();

// 检查权限
if (!hasPermission('user-manage')) {
  // 没有权限,执行相应逻辑
  router.push('/403'); // 跳转到无权限页面
}
</script>
第五层防护:直接URL访问

如果用户直接在浏览器地址栏输入 /systemManage/user-manage :

1、路由存在 :路由配置中确实有这个路径 2、组件加载 :UserManage 组件会被加载 3、权限检查 :组件内部会进行权限检查 4、访问被拒绝 :如果没有权限,会显示无权限提示或跳转到403页面

收获:

在学习权限控制的时候,由于我需要专门设计一套简单的权限控制,我专门找来我们的前端。 想要深入了解一下,我后端传递数据到前端后,前端进行的权限控制流程

浏览器上的页面是静态页面,当点击发送url时,会被前端拦截(Vue Router)的工作原理; 然后经过代码书写的一系列操作之后,在传递到后端, 后端返回的具体数据,是先返回到前端, 经前端处理,才最终到显示的页面。

六、访问控制Casbin

Casbin是一个强大的,高效的开源访问控制框架,其权限管理机制支持多种访问控制模型,各个编程语言都对casbin有支持,而我后续应用rbac,还需casbin的帮忙。

先行导包:

代码语言:javascript
代码运行次数:0
运行
复制
go get github.com/casbin/casbin/v2

casbin的运转,离不开配置文件规则集

再次复习回来的我,请记住一句话:先把demo跑通。

基本使用

目录:

配置文件:

代码语言:javascript
代码运行次数:0
运行
复制
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

[policy_effect]
e = some(where (p.eft == allow))

规则集:

代码语言:javascript
代码运行次数:0
运行
复制
p, zhangsan, /index, GET
p, zhangsan, /home, GET
p, zhangsan, /users, GET
p, zhangsan, /users, POST
p, wangwu, /index, GET
p, wangwu, /home, GET

demo.go文件

代码语言:javascript
代码运行次数:0
运行
复制
package main

import (
	"fmt"
	"github.com/casbin/casbin/v2"
	"log"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
	ok, _ := e.Enforce(sub, obj, act)
	if ok {
		fmt.Printf("%s CAN %s %s\n", sub, act, obj)
	} else {
		fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
	}
}

func main() {
	e, err := casbin.NewEnforcer("casbin/model.pml", "casbin/policy.csv")
	if err != nil {
		log.Fatalf("NewEnforecer failed:%v\n", err)
	}

	check(e, "zhangsan", "/index", "GET")
	check(e, "zhangsan", "/home", "GET")
	check(e, "zhangsan", "/users", "POST")
	check(e, "wangwu", "/users", "POST")
}

此外还可以增加、删除规则:

代码语言:javascript
代码运行次数:0
运行
复制
// 增加
e.AddPolicy("wangwu", "/users", "POST") // 增加
e.SavePolicy()    // 落库
// 删除
e.RemovePolicy("wangwu", "/users", "POST")
e.SavePolicy()
基础知识

配置文件解释:

代码语言:javascript
代码运行次数:0
运行
复制
# ==============================================
# Casbin 权限模型配置文件详解
# ==============================================

# [request_definition] 部分:定义权限检查请求的结构
# 这部分规定了在执行权限检查时(enforce方法),需要提供哪些参数
[request_definition]
# 定义了一个请求(r)包含三个元素:
# - sub (subject): 访问主体,通常是用户ID、用户名或角色
# - obj (object): 访问客体,即要访问的资源或对象
# - act (action): 操作动作,如读(read)、写(write)、删除(delete)等
r = sub, obj, act

# [policy_definition] 部分:定义权限策略的存储结构
# 这部分规定了在数据库或策略文件中存储的权限规则格式
[policy_definition]
# 定义了一个策略(p)包含三个元素:
# - sub: 被授权的主体
# - obj: 被授权的资源
# - act: 被允许的操作
p = sub, obj, act

# [matchers] 部分:定义请求与策略的匹配规则
# 这部分是Casbin的核心,决定了如何将请求与存储的策略进行匹配
[matchers]
# 匹配规则说明:
# - r.sub == p.sub: 请求的主体必须与策略的主体完全匹配
# - r.obj == p.obj: 请求的资源必须与策略的资源完全匹配
# - r.act == p.act: 请求的操作必须与策略的操作完全匹配
# 只有当这三个条件都满足时,策略才会匹配成功
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

# [policy_effect] 部分:定义多个匹配策略时的效果组合规则
# 这部分决定了当多个策略匹配请求时,如何组合这些策略的效果
[policy_effect]
# 效果规则说明:
# - some(where (p.eft == allow)): 只要有一个匹配的策略允许访问,最终结果就是允许
# - 这意味着这是一个"一票通过"系统:只要有一条规则允许,即使有其他规则拒绝,也会允许访问
e = some(where (p.eft == allow))

规则集解释:

代码语言:javascript
代码运行次数:0
运行
复制
# ==============================================
# Casbin 策略规则详解
# ==============================================

# 策略规则格式: p, 主体(sub), 资源(obj), 操作(act)
# 每条规则表示"哪个主体可以对哪个资源执行什么操作"

# 用户"zhangsan"的权限规则:
p, zhangsan, /index, GET    # 允许zhangsan访问首页(GET请求)
p, zhangsan, /home, GET     # 允许zhangsan访问主页(GET请求)
p, zhangsan, /users, GET    # 允许zhangsan查看用户列表(GET请求)
p, zhangsan, /users, POST   # 允许zhangsan创建新用户(POST请求)

# 用户"wangwu"的权限规则:
p, wangwu, /index, GET      # 允许wangwu访问首页(GET请求)
p, wangwu, /home, GET       # 允许wangwu访问主页(GET请求)

# ==============================================
# 权限分析:
# ==============================================

# zhangsan 拥有以下权限:
# - 可以访问首页 (/index)
# - 可以访问主页 (/home)
# - 可以查看用户列表 (/users - GET)
# - 可以创建新用户 (/users - POST)
# 说明: zhangsan 是一个有较高权限的用户,可以管理用户

# wangwu 拥有以下权限:
# - 可以访问首页 (/index)
# - 可以访问主页 (/home)
# 说明: wangwu 是一个普通用户,只有基本的浏览权限

# 注意: 这两个用户都没有删除用户的权限(如 DELETE /users/123)
# 也没有访问其他可能存在的资源(如 /admin, /settings 等)的权限

当然啦,上方模型实现的是一个简单的ACL(访问控制列表)权限系统,特点是:

  1. 权限直接分配给用户(而非角色)
  2. 精确匹配:主体、资源和操作都必须完全一致
  3. 一票通过制:只要有一条允许规则就允许访问

简单的说,ACL最大的特点就是,直接将权限分配给用户个人。

RBAC+Casbin

其实就是将基于用户管理, 变成了基于角色管理。

这样就可以为每一个新登入的用户,分配一个角色权限,而非单独的用户权限。

所以,对应的mode.pml文件与policy.csv文件,也会改变。

配置文件:

代码语言:javascript
代码运行次数:0
运行
复制
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

# 新增角色定义,配合下方[matchers]中 g(r.sub, p.sub)
[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

# 匹配机制
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

规则集:

代码语言:javascript
代码运行次数:0
运行
复制
p, admin, /index, GET
p, admin, /home, GET
p, admin, /users, GET
p, admin, /users, POST
p, yunwei, /index, GET
p, yunwei, /home, GET
g, zhangsan, admin
g, wangwu, yunwei

# p 开头的是对应角色权限,一般用户可分为3类(游客-普通用户-管理员)
# g 开头的是为用户分配的角色。

这样,就可以喽。

gorm+Casbin

其实基于上面的rbac,就可以做一个角色访问控制功能了,但是目前整个规则集是存储在文件中的。

但实际应用中,一般还是会将规则集(policy.csv)放在数据库中的。

老规矩!导入

代码语言:javascript
代码运行次数:0
运行
复制
go get github.com/casbin/gorm-adapter/v3

先初始化一下gorm

代码语言:javascript
代码运行次数:0
运行
复制
package core

import (
  "fmt"
  "github.com/sirupsen/logrus"
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "gorm.io/gorm/logger"
  "time"
)

func InitGorm() *gorm.DB {
  dsn := "root:root@tcp(127.0.0.1:3306)/rule_db?charset=utf8mb4&parseTime=True&loc=Local"

  var mysqlLogger logger.Interface
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger:                                   mysqlLogger,
    DisableForeignKeyConstraintWhenMigrating: true,
  })
  if err != nil {
    logrus.Error(fmt.Sprintf("[%s] mysql连接失败", dsn))
    panic(err)
  }
  sqlDB, _ := db.DB()
  sqlDB.SetMaxIdleConns(10)               // 最大空闲连接数
  sqlDB.SetMaxOpenConns(100)              // 最多可容纳
  sqlDB.SetConnMaxLifetime(time.Hour * 4) // 连接最大复用时间,不能超过mysql的wait_timeout
  return db
}

迁移表的时候,把CasbinRule一起生成出来。

代码语言:javascript
代码运行次数:0
运行
复制
gormadapter "github.com/casbin/gorm-adapter/v3"

global.DB.AutoMigrate(
  &gormadapter.CasbinRule{},
)

casbin连接关联gorm

代码语言:javascript
代码运行次数:0
运行
复制
// 初始化GORM数据库连接,并将连接实例赋值给全局变量global.DB
// 这里假设core.InitGorm()已经配置好数据库连接参数并返回一个*gorm.DB实例
global.DB = core.InitGorm()

// 使用GORM数据库连接创建Casbin适配器
// 适配器负责将Casbin策略持久化到数据库中(使用GORM作为ORM工具)
// 第二个返回值是错误对象,这里使用_忽略错误检查(实际项目中不建议这样做)
a, _ := gormadapter.NewAdapterByDB(global.DB)

// 从文件加载Casbin模型配置
// "./testdata/model.pml"是模型定义文件的路径,包含请求定义、策略定义等配置
// 如果加载失败,会返回错误信息
m, err := model.NewModelFromFile("./testdata/model.pml")
if err != nil {
  // 如果模型加载失败,记录错误日志并返回
  // 使用logrus.Error记录错误级别日志,包含错误详情
  logrus.Error("字符串加载模型失败!", err)
  return
}

// 创建带缓存的Casbin执行器
// 与标准执行器不同,缓存执行器会将策略缓存到内存中,提高权限检查性能
// 第二个返回值是错误对象,这里使用_忽略错误检查(实际项目中不建议这样做)
e, _ := casbin.NewCachedEnforcer(m, a)

// 设置缓存过期时间为3600秒(1小时)
// 这意味着策略缓存将在1小时后失效,需要重新加载
e.SetExpireTime(60 * 60)

// 从适配器(数据库)加载策略到执行器中
// 这是必须的一步,否则执行器不知道任何权限规则
// 错误对象被忽略(实际项目中应该检查错误)
_ = e.LoadPolicy()
代码语言:javascript
代码运行次数:0
运行
复制
e.AddPolicy("admin", "/api/users", "GET")
e.AddRoleForUser("zhangsan", "admin")
check(e, "zhangsan", "/api/users", "GET")
e.RemoveGroupingPolicy("zhangsan", "admin")
e.RemovePolicy("admin", "/api/users", "GET")
check(e, "zhangsan", "/api/users", "GET")
e.SavePolicy()
check(e, "zhangsan", "/api/users", "GET")

七、我在项目中遇到的问题

我在学长的项目中,遇到过这种问题: 想要从casbin中移除,角色用户关系。但是并没有遵守一致性原则。 这种是不对的。

代码语言:javascript
代码运行次数:0
运行
复制
	// 从数据库中移除关联
	if err = roleRepo.RemoveRoleFromUser(ctx, userID, roleID); err != nil {
		return fmt.Errorf("数据库移除角色失败: %w", err)
	}

	// 从Casbin中移除用户角色关系
	userIDStr := strconv.FormatUint(uint64(userID), 10)
	_, err = p.casbinSvc.Enforcer.DeleteRoleForUser(userIDStr, role.Code)
	if err != nil {
		return fmt.Errorf("Casbin移除用户角色失败: %w", err)
	}

而解决这种问题,并且不改变项目结构框架啥的。 最好方式,就是采用补偿性原则

代码语言:javascript
代码运行次数:0
运行
复制
	// 从数据库中移除关联
	if err = roleRepo.RemoveRoleFromUser(ctx, userID, roleID); err != nil {
		return fmt.Errorf("数据库移除角色失败: %w", err)
	}

	// 从Casbin中移除用户角色关系
	userIDStr := strconv.FormatUint(uint64(userID), 10)
	_, err = p.casbinSvc.Enforcer.DeleteRoleForUser(userIDStr, role.Code)
	if err != nil {
        // 这里采用补偿性原则
        roleRepo.AddRoleFromUser(ctx, userID, roleID);
		return fmt.Errorf("Casbin移除用户角色失败: %w", err)
	}

_, err = p.casbinSvc.Enforcer.DeleteRoleForUser(userIDStr, role.Code) if err != nil { return fmt.Errorf("Casbin移除用户角色失败: %w", err) } 在这里,采用补偿性原则 roleRepo.AddRoleFromUser(ctx, userID, roleID);


历程:

2025/8/14:发布博客,记录了RBAC、中间件与权限树设计 2025/8/14:更新博客,宏观上了解图书馆项目,并了解前端是如何运转 2025/9/19:加入了manpao项目,对其技术栈casbin感兴趣,故来此博客补充了casbin相关知识 2025/10/15:无聊的时,翻了翻之前项目的代码,发现了casbin不遵循一致性,特来此添加


网站:

1、活动广场 - 河南师范大学附属中学图书馆


借鉴:

1、学习casbin的时候,得多谢枫枫老师了,带我轻松入门


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、RBAC
    • 1、传统方案
    • 2、如何通过RBAC改进?
    • 3、如何设计代码?
  • 二、中间件设计
    • 1、理论设计方式
    • 2、图书馆项目的设计
      • a.跳过重复路径
      • b.获取JWT凭证
      • 可以拓展一下(AI):
  • 三、基于图书馆的权限树设计
    • 1、权限树设计
    • 2、权限层级映射:
  • 四、图书馆项目
    • 1、整体架构:
    • 2、核心组件解析
      • a、 用户信息结构 (UserInfo)
      • b、角色模型 (Role)
      • c、权限树结构 (Permission)
    • 3、权限设计层级
      • a、三级权限结构:
  • 五、前端如何进行权限控制
    • 现实场景:
    • 完整权限控制
      • 首先从后端获取权限数据
      • 第一层防护:菜单不显示
      • 第二层防护:系统管理子菜单过滤
      • 第三层防护:权限指令控制
      • 第四层防护:组合式函数权限检查
      • 第五层防护:直接URL访问
    • 收获:
  • 六、访问控制Casbin
    • 基本使用
    • 基础知识
    • RBAC+Casbin
    • gorm+Casbin
  • 七、我在项目中遇到的问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档