文档说明
基于gin+casbin实现rbac的权限管理系统
参考文档:casbin官方文档 gin文档
gin和casbin的基本使用,本文就不再叙述,本文主要介绍casbin在gin中的使用及代码实现
功能说明
本文gin+casbin使用gorm作为数据存储,在casbin中存储用户与角色的关联、角色与接口的关联
模型架构
项目结构
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(¶ms)
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模型的权限管理,剩下的就是对接前端,维护下用户与角色的关系,角色与接口的关系即可。