欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

基于 gin+casbin 实现的 RBAC 权限管理

最编程 2024-04-03 09:13:52
...

文档说明

基于gin+casbin实现rbac的权限管理系统

参考文档:casbin官方文档 gin文档

gin和casbin的基本使用,本文就不再叙述,本文主要介绍casbin在gin中的使用及代码实现

功能说明

本文gin+casbin使用gorm作为数据存储,在casbin中存储用户与角色的关联、角色与接口的关联

模型架构

image.png

项目结构

api/      // 接口文件
  role.go
conf/     // 配置文件
  casbin.conf
database/  // 数据库连接
  casbin.go
model/     // 模型文件
  user.go
  role.go
  api.go
libs/
  middleware.go

casbin规则配置文件

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[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 || isAdmin(r.sub)

isAdmin:自定义校验用户是否为超级管理员角色的函数

封装casbin handler

package database

import (
	"fmt"
	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	"os"
	"sync"
)

var (
	once   sync.Once
	Casbin *casbinHandler
)

type casbinHandler struct {
	syncedEnforcer *casbin.SyncedEnforcer
}

func (c *casbinHandler) init() {
	once.Do(func() {
		adapter, err := gormadapter.NewAdapterByDB(DB)
		if err != nil {
			panic(err)
		}
		c.syncedEnforcer, err = casbin.NewSyncedEnforcer("conf/rbac_model.conf", adapter)
		if err != nil {
			panic(err)
		}
	})
	c.syncedEnforcer.AddFunction("isAdmin", func(arguments ...interface{}) (interface{}, error) {
		// 获取用户名
		username := arguments[0].(string)
		// 检查用户名的角色是否为超级管理员
		return c.syncedEnforcer.HasRoleForUser(username, "role_1")
	})
	err := c.syncedEnforcer.LoadPolicy()
	if err != nil {
		panic(err)
	}
}

// Enforce 校验权限
func (c *casbinHandler) Enforce(user, uri, action string) (bool, error) {
	return c.syncedEnforcer.Enforce(user, uri, action)
}

// AddPolicy 添加策略
func (c *casbinHandler) AddPolicy(roleId int, uri, method string) (bool, error) {
	return c.syncedEnforcer.AddPolicy(c.MakeRoleName(roleId), uri, method)
}

// 拼接角色ID,为了防止角色与用户名冲突
func (c *casbinHandler) MakeRoleName(roleId int) string {
	return fmt.Sprintf("role_%d", roleId)
}

// AddPolicies 批量添加策略
func (c *casbinHandler) AddPolicies(rules [][]string) (bool, error) {
	return c.syncedEnforcer.AddPolicies(rules)
}

// DeleteRole 删除角色对应的用户和权限
func (c *casbinHandler) DeleteRole(roleId int) (bool, error) {
	return c.syncedEnforcer.DeleteRole(c.MakeRoleName(roleId))
}

// DeleteRolePolicy 删除角色下的权限
func (c *casbinHandler) DeleteRolePolicy(roleId int) (bool, error) {
	return c.syncedEnforcer.RemoveFilteredNamedPolicy("p", 0, c.MakeRoleName(roleId))
}

// DeleteRoleUser 删除添加用户
func (c *casbinHandler) DeleteRoleUser(roleId int) (bool, error) {
	return c.syncedEnforcer.RemoveFilteredNamedGroupingPolicy("g", 1, c.MakeRoleName(roleId))
}

// AddUserRole 添加角色和用户对应关系
func (c *casbinHandler) AddUserRole(user string, roleId int) (bool, error) {
	return c.syncedEnforcer.AddGroupingPolicy(user, c.MakeRoleName(roleId))
}

// AddUserRoles 批量添加角色和用户对应关联
func (c *casbinHandler) AddUserRoles(usernames []string, roleIds []int) (bool, error) {
	rules := make([][]string, 0)
	for _, u := range usernames {
		for _, r := range roleIds {
			rules = append(rules, []string{u, c.MakeRoleName(r)})
		}
	}
	return c.syncedEnforcer.AddGroupingPolicies(rules)
}

// DeleteUserRole 删除用户的角色信息
func (c *casbinHandler) DeleteUserRole(user string) (bool, error) {
	return c.syncedEnforcer.RemoveFilteredNamedGroupingPolicy("g", 0, user)
}

模型文件

gin对接casbin的增删改查大部分都写在gorm的hook里

用户模型

针对用户增删改查后的关联角色都在模型hook中实现

type TUser struct {
	Model
	Username string   `gorm:"column:username;type:varchar(50);unique_index" json:"username" binding:"required"`
	NameCn   string   `gorm:"column:name_cn" json:"name_cn" binding:"required"`
	Enabled  int      `gorm:"column:enabled" json:"enabled"`
	RoleIds  []int    `gorm:"-" json:"role_ids"`
	Roles    []string `gorm:"-" json:"roles"`
}

func (TUser) TableName() string {
	return "t_user"
}
// AfterFind 获取用户后,获取用户的角色信息
func (t *TUser) AfterFind(tx *gorm.DB) (err error) {
	if e := database.DB.Model(&TUserRole{}).Where("user_id = ?", t.Id).Pluck("role_id", &t.RoleIds).Error; e != nil {
		err = fmt.Errorf("根据权限ID<%d>获取菜单ID异常: <%s>", t.Id, e.Error())
		return
	}
	if e := database.DB.Model(&TRole{}).Where("id in ?", t.RoleIds).Pluck("name", &t.Roles).Error; e != nil {
		err = fmt.Errorf("根据菜单ID<%+v>获取菜单信息异常: <%s>", t.RoleIds, e.Error())
		return
	}
	return
}
// AfterCreate 添加用户后,创建用户与角色的关联
func (t *TUser) AfterCreate(tx *gorm.DB) (err error) {
	// 添加用户后,要添加用户角色并且添加到casbin
	if _, e := database.Casbin.AddUserRoles([]string{t.Username}, t.RoleIds); e != nil {
		err = fmt.Errorf("关联用户和角色到casbin异常: <%s>", e.Error())
		return
	}
	err = t.bulkCreateUserRole()
	return
}
// 批量创建用户与角色的关联
func (t *TUser) bulkCreateUserRole() (err error) {
	bulks := make([]*TUserRole, 0)
	for _, v := range t.RoleIds {
		bulks = append(bulks, &TUserRole{RoleId: v, UserId: t.Id})
	}
	if e := database.DB.Create(&bulks).Error; e != nil {
		err = fmt.Errorf("关联用户角色异常: <%s>", e.Error())
	}
	return err
}
// 删除用户与角色的关联
func (t *TUser) deleteUserRole() (err error) {
	if e := database.DB.Where("user_id = ?", t.Id).Delete(&TUserRole{}).Error; e != nil {
		err = fmt.Errorf("删除用户角色关联异常: <%s>", e.Error())
	}
	return
}

// BeforeUpdate 更新用户信息前,先清除用户角色关联,然后再重新添加
func (t *TUser) BeforeUpdate(tx *gorm.DB) (err error) {
	// 清除casbin用户和角色关联
	if _, e := database.Casbin.DeleteUserRole(t.Username); e != nil {
		err = fmt.Errorf("删除用户<%s>的casbin角色关联异常: <%s>", t.Username, e.Error())
		return
	}
	// 添加用户和角色关联
	if err = t.deleteUserRole(); err != nil {
		return
	}
	// 重新构建casbin用户和角色
	return t.AfterCreate(tx)
}
// BeforeDelete 删除用户前清除用户与角色的关联信息
func (t *TUser) BeforeDelete(tx *gorm.DB) (err error) {
	if err = t.deleteUserRole(); err != nil {
		return
	}
	return
}

角色模型

type TRole struct {
	Model
	Name        string `gorm:"column:name;type:varchar(50);unique_index" json:"name" binding:"required"`
	Description string `gorm:"column:description" json:"description"`
}

func (TRole) TableName() string {
	return "t_role"
}

// BeforeDelete 添加角色前,清除角色与用户的关联
func (t *TRole) BeforeDelete(tx *gorm.DB) (err error) {
	// 清除casbin用户与角色关联
	if _, e := database.Casbin.DeleteRole(t.Id); e != nil {
		err = fmt.Errorf("清除casbin角色权限异常: <%s>", e.Error())
	}
	// 清除数据库中用户与角色的关联
	return t.deleteRoleUser()
}

// 删除用户与角色的关联
func (t *TRole) deleteRoleUser() (err error) {
	if e := database.DB.Where("role_id = ?", t.Id).Delete(&TUserRole{}).Error; e != nil {
		err = fmt.Errorf("删除用户角色关联异常: <%s>", e.Error())
	}
	return
}

接口模型

增、删、改、查接口后的操作,接口与所属菜单的关系,本文就不描述菜单的模型

type TApi struct {
	Model
	Name    string   `gorm:"column:name" json:"name" binding:"required"`
	Uri     string   `gorm:"column:uri" json:"uri" binding:"required"`
	Method  string   `gorm:"column:method" json:"method"`
	MenuIds []int    `gorm:"-" json:"menu_ids" binding:"required"`
	Menus   []string `gorm:"-" json:"menus"`
	Enabled int      `gorm:"column:enabled" json:"enabled"`
	Select  bool     `gorm:"-" json:"select"`
}

func (TApi) TableName() string {
	return "t_api"
}

// AfterCreate 添加接口后,创建接口与菜单的关系
func (t *TApi) AfterCreate(tx *gorm.DB) (err error) {
	// 添加好权限后,添加菜单权限
	err = t.bulkCreateMenuApi()
	return
}

// BeforeUpdate 更新接口前,重新关联与菜单的关系
func (t *TApi) BeforeUpdate(tx *gorm.DB) (err error) {
	// 先清除权限历史菜单
	if err = t.deleteMenuApi(); err != nil {
		return
	}
	// 重新构建与菜单的关系
	err = t.bulkCreateMenuApi()
	return
}

// BeforeDelete 删除接口前,清除与菜单的关系
func (t *TApi) BeforeDelete(tx *gorm.DB) (err error) {
	err = t.deleteMenuApi()
	return
}

// 批量构建接口与菜单的关系
func (t *TApi) bulkCreateMenuApi() (err error) {
	menuAuths := make([]*TMenuApi, 0)
	for _, v := range t.MenuIds {
		menuAuths = append(menuAuths, &TMenuApi{MenuId: v, ApiId: t.Id})
	}
	if e := database.DB.Create(&menuAuths).Error; e != nil {
		err = fmt.Errorf("关联权限菜单异常: <%s>", e.Error())
	}
	return err
}

// 清除接口与菜单的关系
func (t *TApi) deleteMenuApi() (err error) {
	if e := database.DB.Where("api_id = ?", t.Id).Delete(&TMenuApi{}).Error; e != nil {
		err = fmt.Errorf("删除权限菜单异常: <%s>", e.Error())
	}
	return
}

关联模型

// 角色与接口的关联模型
type TRoleApi struct {
	Id     int    `gorm:"primary_key" json:"id"`
	RoleId int    `gorm:"role_id" json:"role_id"`
	ApiId  int    `gorm:"author_id" json:"author_id"`
	Uri    string `gorm:"-" json:"uri"`
}

func (TRoleApi) TableName() string {
	return "t_role_api"
}
// 用户与角色的关联模型
type TUserRole struct {
	Id     int `gorm:"primary_key" json:"id"`
	UserId int `gorm:"column:user_id" json:"user_id"`
	RoleId int `gorm:"column:role_id" json:"role_id"`
}

func (TUserRole) TableName() string {
	return "t_user_role"
}

更新角色的权限信息

更新角色与接口的关联,同时同步到casbin

// UpdatePermission 修改角色权限信息
// 接收数据: {"menu_list": [], "author_list": []}	// 里面存的是菜单ID和权限ID
// 执行逻辑: 删除角色原有的菜单和权限信息,根据选择的重新添加即可
func (h *Handler) UpdatePermission(request *gin.Context) {
	l := zap.L().With(zap.String("func", "PutRole"))
	params := struct {
		AuthorParams
		MenuIds []int `json:"menu_ids" binding:"required"`
		ApiIds  []int `json:"api_ids" binding:"required"`
	}{}
	l.Info(fmt.Sprintf("request_data: %+v, role_id: %d", params, params.RoleId))
	err := request.ShouldBindJSON(&params)
	if err != nil {
		libs.HttpParamsError(request, fmt.Sprintf("参数解析异常: <%s>", err.Error()))
		return
	}
	role, err := model.QueryRoleInfoById(params.RoleId)
	if err != nil {
		libs.HttpServerError(request, err.Error())
		return
	}
	l.Info(fmt.Sprintf("1. 删除角色权限信息.............."))
	// 删除角色权限信息
	tx := database.DB.Begin()
	if err := tx.Where("role_id = ?", params.RoleId).Delete(&model.TRoleApi{}).Error; err != nil {
		tx.Rollback()
		libs.HttpServerError(request, fmt.Sprintf("删除角色权限信息异常: "+err.Error()))
		return
	}
	l.Info("2. 删除角色菜单信息...................")
	// 删除角色菜单信息
	if err := tx.Where("role_id = ?", params.RoleId).Delete(&model.TRoleMenu{}).Error; err != nil {
		tx.Rollback()
		libs.HttpServerError(request, fmt.Sprintf("删除角色菜单信息异常: "+err.Error()))
		return
	}
	l.Info("3. 添加角色菜单信息....................")
	// 添加角色菜单信息
	roleMenuList := make([]model.TRoleMenu, 0)
	for _, item := range params.MenuIds {
		roleMenuList = append(roleMenuList, model.TRoleMenu{RoleId: params.RoleId, MenuId: item})
	}
	if err := tx.Create(&roleMenuList).Error; err != nil {
		tx.Rollback()
		libs.HttpServerError(request, fmt.Sprintf("添加角色菜单信息异常: "+err.Error()))
		return
	}
	l.Info("4. 添加角色权限信息....................")
	// 添加角色权限信息
	roleAuthorList := make([]model.TRoleApi, 0)
	rules := make([][]string, 0)
	for v, _ := range apisToMap(params.ApiIds) {
		api, _ := model.QueryApiInfoById(v)
		roleAuthorList = append(roleAuthorList, model.TRoleApi{RoleId: params.RoleId, ApiId: v})
		rules = append(rules, []string{database.Casbin.MakeRoleName(role.Id), api.Uri, api.Method})
	}
	if err := tx.Create(&roleAuthorList).Error; err != nil {
		tx.Rollback()
		libs.HttpServerError(request, fmt.Sprintf("添加角色权限信息异常: "+err.Error()))
		return
	}
	// 删除casbin权限
	if _, err := database.Casbin.DeleteRolePolicy(role.Id); err != nil {
		tx.Rollback()
		libs.HttpServerError(request, fmt.Sprintf("清除casbin角色权限异常: <%s>", err.Error()))
		return
	}
	// 添加新的casbin权限
	if ok, err := database.Casbin.AddPolicies(rules); !ok || err != nil {
		tx.Rollback()
		libs.HttpServerError(request, fmt.Sprintf("添加casbin角色权限异常: <%+v>", err))
		return
	}
	tx.Commit()
	request.JSON(http.StatusOK, libs.Success(nil, "ok"))
}

权限校验

casbin在中间件中的使用

// CasbinAuthor 使用casbin进行访问控制权限
func CasbinAuthor() gin.HandlerFunc {
	return func(request *gin.Context) {
		// 获取用户信息
		user, err := getUser(request)
		if err != nil {
			request.JSON(http.StatusOK, ServerError(err.Error()))
			request.Abort()
			return
		}
		// 获取请求接口和方法
		obj := strings.TrimRight(request.Request.URL.Path, "/")
		act := request.Request.Method
		// 排除不需要校验的权限
		if conf.Config.ExcludeAuth[act][obj] {
			request.Next()
			return
		}
		// 循环用户角色ID,如果有一个角色拥有权限则设置为true
		success, err := database.Casbin.Enforce(user.Username, obj, act)
		if err != nil {
			request.JSON(http.StatusOK, ServerError(err.Error()))
			request.Abort()
			return
		}
		if !success {
			request.JSON(http.StatusOK, AuthorError(fmt.Sprintf("用户<%s>无访问接口<%s>的权限", user.Username, obj)))
			request.Abort()
			return
		}
	}
}

以上代码就简单实现了基本的RBAC模型的权限管理,剩下的就是对接前端,维护下用户与角色的关系,角色与接口的关系即可。

推荐阅读