Go項(xiàng)目中使用Casbin實(shí)現(xiàn)RBAC權(quán)限管理教程
?? 前言
在構(gòu)建企業(yè)級 Go 管理后臺系統(tǒng)時,權(quán)限管理是一個核心功能。Casbin 是一個強(qiáng)大的、開源的訪問控制庫,支持多種訪問控制模型(ACL、RBAC、ABAC 等)。本文將詳細(xì)介紹如何在 Go 項(xiàng)目中使用 Casbin 實(shí)現(xiàn)完整的 RBAC(基于角色的訪問控制)權(quán)限管理系統(tǒng)。
本文基于開源項(xiàng)目 gin-layout 的實(shí)際實(shí)現(xiàn),這是一個功能完整的 Go Admin 后臺管理系統(tǒng)框架,提供了開箱即用的權(quán)限管理解決方案。
?? 什么是 Casbin?
Casbin 是一個強(qiáng)大的、開源的訪問控制庫,支持多種訪問控制模型:
- ACL (Access Control List) - 訪問控制列表
- RBAC (Role-Based Access Control) - 基于角色的訪問控制
- ABAC (Attribute-Based Access Control) - 基于屬性的訪問控制
- RESTful - RESTful 風(fēng)格的訪問控制
Casbin 的核心思想是將訪問控制模型與策略分離,通過配置文件定義訪問控制模型,通過適配器(Adapter)存儲策略規(guī)則。
?? 項(xiàng)目依賴
首先,我們需要安裝 Casbin 相關(guān)的依賴包:
go get github.com/casbin/casbin/v2 go get github.com/casbin/gorm-adapter/v3
?? 1. Casbin 模型配置
Casbin 使用模型文件(Model)來定義訪問控制規(guī)則。在項(xiàng)目中,我們創(chuàng)建了 rbac_model.conf 文件:
[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) && keyMatch3(r.obj, p.obj) && regexMatch(r.act, p.act))
配置說明
- request_definition: 定義請求格式,
r = sub, obj, act表示請求包含主體(subject)、對象(object)和操作(action) - policy_definition: 定義策略格式,
p = sub, obj, act表示策略包含主體、對象和操作 - role_definition: 定義角色繼承關(guān)系,
g = _, _表示角色繼承關(guān)系(如g, user:1, role:1表示用戶1繼承角色1) - policy_effect: 定義策略效果,
e = some(where (p.eft == allow))表示只要有一個策略允許就允許訪問 - matchers: 定義匹配規(guī)則
g(r.sub, p.sub): 檢查請求主體是否繼承策略主體(角色繼承)keyMatch3(r.obj, p.obj): 使用keyMatch3函數(shù)匹配路徑(支持*通配符)regexMatch(r.act, p.act): 使用正則表達(dá)式匹配 HTTP 方法
?? 2. Casbin 初始化
在項(xiàng)目中,我們封裝了一個 Casbin 工具包 internal/pkg/utils/casbin/casbin.go:
package casbinx
import (
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/gorm"
)
type CasbinEnforcer struct {
*casbin.Enforcer
errInit error
tx *gorm.DB
model model.Model
}
var casbinx = &CasbinEnforcer{}
// InitEnforcer 初始化 Casbin Enforcer(僅執(zhí)行一次)
func InitEnforcer() error {
once.Do(func() {
// 1. 加載模型文件
modelPath, err := getModelPath()
if err != nil {
casbinx.errInit = err
return
}
m, err := model.NewModelFromFile(modelPath)
if err != nil {
casbinx.errInit = fmt.Errorf("加載模型失敗: %w", err)
return
}
casbinx.model = m
// 2. 創(chuàng)建 GORM 適配器
db := data.MysqlDB()
gormadapter.TurnOffAutoMigrate(db)
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
casbinx.errInit = fmt.Errorf("創(chuàng)建適配器失敗: %w", err)
return
}
// 3. 創(chuàng)建 Enforcer
enforcer, err := casbin.NewEnforcer(m, adapter)
if err != nil {
casbinx.errInit = fmt.Errorf("創(chuàng)建 Enforcer 失敗: %w", err)
return
}
// 4. 啟用自動保存
enforcer.EnableAutoSave(true)
casbinx.Enforcer = enforcer
})
return casbinx.errInit
}
// GetEnforcer 返回已初始化的 Enforcer 實(shí)例
func GetEnforcer() *CasbinEnforcer {
if casbinx.Enforcer == nil {
if err := InitEnforcer(); err != nil {
return nil
}
}
return casbinx
}
關(guān)鍵點(diǎn)說明
- 單例模式: 使用
sync.Once確保 Enforcer 只初始化一次 - GORM 適配器: 使用
gorm-adapter將策略存儲在 MySQL 數(shù)據(jù)庫中 - 自動保存:
EnableAutoSave(true)確保策略變更后自動保存到數(shù)據(jù)庫 - 模型加載: 從配置文件加載訪問控制模型
?? 3. 權(quán)限檢查中間件
在 Gin 框架中,我們通過中間件來實(shí)現(xiàn)權(quán)限檢查:
// internal/middleware/admin_auth.go
func AdminAuthHandler() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 獲取用戶信息
uid := c.GetUint("uid")
if uid == 0 {
response.Fail(c, e.NotLogin, "請先登錄")
c.Abort()
return
}
adminUser := getUserFromContext(c)
if adminUser == nil {
response.Fail(c, e.NotLogin, "登錄已失效,請重新登錄")
c.Abort()
return
}
// 2. 超級管理員跳過權(quán)限檢查
if !isSuperAdmin(adminUser) {
// 3. 檢查接口權(quán)限
if err := checkPermission(c, adminUser); err != nil {
if businessErr, ok := err.(*e.BusinessError); ok {
response.Fail(c, businessErr.GetCode(), businessErr.GetMessage())
} else {
response.Fail(c, e.ServerErr, "權(quán)限驗(yàn)證失敗")
}
c.Abort()
return
}
}
c.Next()
}
}
// checkPermission 檢查接口權(quán)限
func checkPermission(c *gin.Context, adminUser *model.AdminUser) error {
enforcer := casbinx.GetEnforcer()
if enforcer.Error() != nil {
log.Logger.Error("權(quán)限驗(yàn)證初始化失敗", zap.Error(enforcer.Error()))
return e.NewBusinessError(e.ServerErr, "權(quán)限驗(yàn)證初始化失敗")
}
// 構(gòu)建權(quán)限檢查的 key
userKey := fmt.Sprintf("%s%s%d", global.CasbinAdminUserPrefix, global.CasbinSeparator, adminUser.ID)
path := c.Request.URL.Path
method := c.Request.Method
// 檢查權(quán)限
ok, err := enforcer.Enforce(userKey, path, method)
if err != nil {
log.Logger.Error("權(quán)限驗(yàn)證失敗", zap.Error(err))
return e.NewBusinessError(e.ServerErr, "權(quán)限驗(yàn)證失敗")
}
// 如果沒有權(quán)限,檢查接口是否需要授權(quán)
if !ok {
if model.NewApi().CheckoutRouteIsAuth(path, method) {
return e.NewBusinessError(e.AuthorizationErr, "暫無接口操作權(quán)限")
}
}
return nil
}
權(quán)限檢查流程
- 獲取用戶信息: 從 JWT Token 中解析用戶 ID
- 構(gòu)建用戶標(biāo)識: 使用
adminUser:1格式標(biāo)識用戶(1 為用戶 ID) - 調(diào)用 Enforce: 使用
enforcer.Enforce(userKey, path, method)檢查權(quán)限 - 處理結(jié)果: 如果沒有權(quán)限且接口需要授權(quán),返回權(quán)限不足錯誤
?? 4. 策略管理
4.1 策略存儲格式
在數(shù)據(jù)庫中,策略存儲在 casbin_rule 表中,表結(jié)構(gòu)如下:
CREATE TABLE `casbin_rule` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `ptype` varchar(100) DEFAULT NULL COMMENT '策略類型:p(策略) 或 g(角色繼承)', `v0` varchar(100) DEFAULT NULL COMMENT '主體(subject)', `v1` varchar(100) DEFAULT NULL COMMENT '對象(object)', `v2` varchar(100) DEFAULT NULL COMMENT '操作(action)', `v3` varchar(100) DEFAULT NULL, `v4` varchar(100) DEFAULT NULL, `v5` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `idx_casbin_rule` (`ptype`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 策略類型
P 策略(權(quán)限策略)
格式:[p, subject, object, action]
示例:
- [p, menu:1, /api/v1/user/list, GET] - 菜單1可以訪問 /api/v1/user/list 的 GET 方法
- [p, role:1, /api/v1/user/*, *] - 角色1可以訪問所有 /api/v1/user/* 路徑的所有方法
G 策略(角色繼承)
格式:[g, user, role]
示例:
- [g, adminUser:1, role:1] - 用戶1繼承角色1的所有權(quán)限
- [g, role:1, menu:1] - 角色1繼承菜單1的所有權(quán)限
- [g, dept:1, role:1] - 部門1繼承角色1的所有權(quán)限
4.3 編輯權(quán)限策略
項(xiàng)目中提供了 EditPolicyPermissions 方法來編輯權(quán)限策略:
// EditPolicyPermissions 編輯策略權(quán)限
// 策略格式:[p, 菜單ID, 接口路徑, 接口方法]
func (e *CasbinEnforcer) EditPolicyPermissions(user string, policy [][]string) error {
return e.WithTransaction(func(enforcer casbin.IEnforcer) error {
// 1. 刪除用戶的所有權(quán)限
_, err := enforcer.DeletePermissionsForUser(user)
if err != nil {
return err
}
if len(policy) == 0 {
return nil
}
// 2. 構(gòu)建完整的策略規(guī)則
var policies [][]string
for _, p := range policy {
if len(p) > 0 {
// 策略格式:[user, route, method]
policies = append(policies, append([]string{user}, p...))
}
}
// 3. 批量添加所有策略
ok, err := enforcer.AddPolicies(policies)
if err != nil {
return err
}
if !ok {
return errors.New("添加權(quán)限失敗")
}
return nil
})
}
4.4 編輯角色繼承
項(xiàng)目中提供了 EditPolicyRoles 方法來編輯角色繼承關(guān)系:
// EditPolicyRoles 編輯策略角色
// 策略格式:[g, user, role]
func (e *CasbinEnforcer) EditPolicyRoles(user string, policy []string) error {
return e.WithTransaction(func(enforcer casbin.IEnforcer) error {
// 1. 刪除用戶的所有角色
_, err := enforcer.DeleteRolesForUser(user)
if err != nil {
return err
}
if len(policy) == 0 {
return nil
}
// 2. 構(gòu)建完整的角色繼承規(guī)則
var rules [][]string
for _, role := range policy {
if role != "" {
// 策略格式:[user, role]
rules = append(rules, []string{user, role})
}
}
// 3. 批量添加所有角色繼承關(guān)系
ok, err := enforcer.AddGroupingPolicies(rules)
if err != nil {
return err
}
if !ok {
return errors.New("添加權(quán)限失敗")
}
return nil
})
}
??? 5. 實(shí)際應(yīng)用場景
5.1 菜單權(quán)限管理
在管理后臺系統(tǒng)中,菜單通常與 API 接口關(guān)聯(lián)。當(dāng)用戶編輯菜單權(quán)限時,需要更新 Casbin 策略:
// internal/service/permission/menu.go
// UpdateMenuPermissions 更新菜單權(quán)限
func (s *MenuService) UpdateMenuPermissions(menu *model.Menu, apiList []uint, tx ...*gorm.DB) error {
// 1. 查詢 API 信息
apis := model.List(model.NewApi(), "id IN ?", []any{apiList}, model.ListOptionalParams{
SelectFields: []string{"id", "route", "method"},
})
// 2. 構(gòu)建策略規(guī)則
policy := lo.Map(apis, func(api *model.Api, _ int) []string {
return []string{api.Route, api.Method}
})
// 3. 更新 Casbin 策略
menuName := fmt.Sprintf("%s%s%d", global.CasbinMenuPrefix, global.CasbinSeparator, menu.ID)
enforcer := casbinx.GetEnforcer()
if len(tx) > 0 {
enforcer.SetDB(tx[0])
}
return enforcer.EditPolicyPermissions(menuName, policy)
}
5.2 用戶角色分配
當(dāng)為用戶分配角色時,需要更新角色繼承關(guān)系:
// internal/service/permission/admin_user.go
// EditUserRoles 編輯用戶角色
func (s *AdminUserService) EditUserRoles(uid uint, roleIds []uint, tx ...*gorm.DB) error {
// 1. 構(gòu)建角色標(biāo)識列表
roleList := lo.Map(roleIds, func(roleId uint, _ int) string {
return fmt.Sprintf("%s%s%d", global.CasbinRolePrefix, global.CasbinSeparator, roleId)
})
// 2. 更新角色繼承關(guān)系
userName := fmt.Sprintf("%s%s%d", global.CasbinAdminUserPrefix, global.CasbinSeparator, uid)
enforcer := casbinx.GetEnforcer()
if len(tx) > 0 {
enforcer.SetDB(tx[0])
}
return enforcer.EditPolicyRoles(userName, roleList)
}
5.3 角色權(quán)限管理
角色可以繼承菜單的權(quán)限,也可以繼承其他角色的權(quán)限:
// internal/service/permission/role.go
// EditRoleMenus 編輯角色菜單
func (s *RoleService) EditRoleMenus(roleId uint, menuIds []uint, tx ...*gorm.DB) error {
// 1. 構(gòu)建菜單標(biāo)識列表
menuList := lo.Map(menuIds, func(menuId uint, _ int) string {
return fmt.Sprintf("%s%s%d", global.CasbinMenuPrefix, global.CasbinSeparator, menuId)
})
// 2. 更新角色繼承關(guān)系
roleName := fmt.Sprintf("%s%s%d", global.CasbinRolePrefix, global.CasbinSeparator, roleId)
enforcer := casbinx.GetEnforcer()
if len(tx) > 0 {
enforcer.SetDB(tx[0])
}
return enforcer.EditPolicyRoles(roleName, menuList)
}
?? 6. 事務(wù)支持
在實(shí)際應(yīng)用中,權(quán)限更新通常需要與業(yè)務(wù)數(shù)據(jù)更新在同一個事務(wù)中。項(xiàng)目提供了事務(wù)支持:
// WithTransaction 執(zhí)行事務(wù)中的操作
func (e *CasbinEnforcer) WithTransaction(fc func(e casbin.IEnforcer) error) (err error) {
a, ok := e.GetAdapter().(*gormadapter.Adapter)
if !ok {
return errors.New("適配器類型錯誤")
}
if e.tx != nil {
if !isInTransaction(e.tx) {
return errors.New("請先通過 GORM 開啟事務(wù)后傳入 SetDB")
}
defer func() {
// 操作完成后,重置適配器
e.SetAdapter(a.Copy())
e.tx = nil
}()
// 創(chuàng)建事務(wù)適配器
gormadapter.TurnOffAutoMigrate(e.tx)
txAdapter, err := gormadapter.NewAdapterByDB(e.tx)
if err != nil {
return err
}
e.SetAdapter(txAdapter)
}
err = fc(e.Enforcer)
return
}
使用示例:
// 在 GORM 事務(wù)中使用 Casbin
db.Transaction(func(tx *gorm.DB) error {
// 1. 設(shè)置事務(wù)
enforcer := casbinx.GetEnforcer().SetDB(tx)
// 2. 更新業(yè)務(wù)數(shù)據(jù)
// ... 業(yè)務(wù)邏輯 ...
// 3. 更新權(quán)限策略(在事務(wù)中)
menuName := fmt.Sprintf("menu:%d", menuId)
return enforcer.EditPolicyPermissions(menuName, policy)
})
?? 7. 策略重新加載
當(dāng)策略更新后,需要重新加載策略到內(nèi)存中:
// 重新加載策略
enforcer := casbinx.GetEnforcer()
if err := enforcer.LoadPolicy(); err != nil {
log.Logger.Error("重新加載策略失敗", zap.Error(err))
}
注意:雖然啟用了 EnableAutoSave(true),但這只保證策略保存到數(shù)據(jù)庫,不會自動加載到內(nèi)存。在策略更新后,需要手動調(diào)用 LoadPolicy() 重新加載。
?? 8. 權(quán)限標(biāo)識規(guī)范
項(xiàng)目中定義了統(tǒng)一的權(quán)限標(biāo)識前綴:
// internal/global/auth.go
const (
CasbinAdminUserPrefix = "adminUser" // 用戶前綴
CasbinRolePrefix = "role" // 角色前綴
CasbinMenuPrefix = "menu" // 菜單前綴
CasbinDeptPrefix = "dept" // 部門前綴
CasbinSeparator = ":" // 分隔符
)
權(quán)限標(biāo)識格式:
- 用戶:
adminUser:1(用戶 ID 為 1) - 角色:
role:1(角色 ID 為 1) - 菜單:
menu:1(菜單 ID 為 1) - 部門:
dept:1(部門 ID 為 1)
?? 9. 常見問題與解決方案
9.1 權(quán)限更新后不生效
問題:更新策略后,權(quán)限檢查仍然使用舊策略。
解決方案:在策略更新后,調(diào)用 LoadPolicy() 重新加載策略:
enforcer := casbinx.GetEnforcer() // 更新策略 enforcer.EditPolicyPermissions(userName, policy) // 重新加載策略 enforcer.LoadPolicy()
9.2 事務(wù)回滾后策略未恢復(fù)
問題:在事務(wù)中更新策略,事務(wù)回滾后策略未恢復(fù)。
解決方案:確保在事務(wù)提交或回滾后,重新加載策略:
db.Transaction(func(tx *gorm.DB) error {
enforcer := casbinx.GetEnforcer().SetDB(tx)
// 更新策略
return enforcer.EditPolicyPermissions(userName, policy)
})
// 事務(wù)提交或回滾后,重新加載策略
enforcer := casbinx.GetEnforcer()
enforcer.LoadPolicy()
9.3 路徑匹配問題
問題:使用 keyMatch3 函數(shù)時,路徑匹配不符合預(yù)期。
解決方案:
keyMatch3支持*通配符,如/api/v1/user/*可以匹配/api/v1/user/list、/api/v1/user/detail等- 如果需要更精確的匹配,可以使用
keyMatch或regexMatch
?? 10. 最佳實(shí)踐
- 統(tǒng)一權(quán)限標(biāo)識格式:使用統(tǒng)一的前綴和分隔符,便于管理和維護(hù)
- 事務(wù)一致性:權(quán)限更新與業(yè)務(wù)數(shù)據(jù)更新應(yīng)在同一事務(wù)中
- 策略重新加載:策略更新后及時重新加載,確保權(quán)限檢查使用最新策略
- 錯誤處理:完善的錯誤處理和日志記錄,便于問題排查
- 性能優(yōu)化:策略加載到內(nèi)存后,權(quán)限檢查速度很快,但策略更新后需要重新加載
?? 總結(jié)
本文詳細(xì)介紹了如何在 Go 項(xiàng)目中使用 Casbin 實(shí)現(xiàn) RBAC 權(quán)限管理,包括:
- ? Casbin 模型配置
- ? Enforcer 初始化
- ? 權(quán)限檢查中間件
- ? 策略管理(權(quán)限策略和角色繼承)
- ? 事務(wù)支持
- ? 實(shí)際應(yīng)用場景
- ? 常見問題與解決方案
基于本文的實(shí)現(xiàn),你可以快速構(gòu)建一個功能完整的權(quán)限管理系統(tǒng)。如果你需要完整的項(xiàng)目代碼,可以參考開源項(xiàng)目 gin-layout,這是一個功能完整的 Go Admin 后臺管理系統(tǒng)框架,提供了開箱即用的權(quán)限管理解決方案。
?? 相關(guān)資源
到此這篇關(guān)于Go項(xiàng)目中使用Casbin實(shí)現(xiàn)RBAC權(quán)限管理教程的文章就介紹到這了,更多相關(guān)Go RBAC 權(quán)限管理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Goland 同一個package中函數(shù)互相調(diào)用的問題
這篇文章主要介紹了解決Goland 同一個package中函數(shù)互相調(diào)用的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05
Golang通道阻塞情況與通道無阻塞實(shí)現(xiàn)小結(jié)
本文主要介紹了Golang通道阻塞情況與通道無阻塞實(shí)現(xiàn)小結(jié),詳細(xì)解析了通道的類型、操作方法以及垃圾回收機(jī)制,從基礎(chǔ)概念到高級應(yīng)用,具有一定的參考價值,感興趣的可以了解一下2024-03-03
go語言中的json與map相互轉(zhuǎn)換實(shí)現(xiàn)
本文主要介紹了go語言中的json與map相互轉(zhuǎn)換實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08

