最近在写一个权限管理系统,需要实现基于角色(RBAC)的 RESTful API 权限控制,经过一番调研后选择了基于 Gin + Casbin 的方案。
需求分析
对于一个标准的权限管理系统,通常需要解决以下几个问题:
- 角色的继承关系
- RESTful API 的路径匹配
- HTTP 方法级别的控制
- 平台模式(单租户/多租户)的支持
Casbin 作为一个功能成熟的权限管理框架,刚好满足了这些需求。
Casbin 模型定义
先来看看基本的权限模型定义:
[request_definition]
r = mod, sub, obj, act
[policy_definition]
p = mod, sub, obj, act, eft
[role_definition]
g = *, *
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == "root" || \
( g(r.sub, p.sub) && \
g(r.mod, p.mod) && \
(r.obj == p.obj || keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj) || p.obj == "*") && \
(r.act == p.act || p.act == "*") )
这个模型比较有意思的地方在于:
- 使用
mod
字段支持平台模式的继承 - 通过
keyMatch2
实现了 RESTful 路径的匹配 - 支持通配符
*
进行批量授权
权限规则配置
有了模型后,我们还需要定义具体的权限规则:
# 角色继承
g, admin, network_admin
g, network_admin, user
g, user, *
# 平台模式
g, MultiTenantCrossPlatform, MultiTenantSinglePlatform
g, MultiTenantSinglePlatform, SingleTenantCrossPlatform
g, SingleTenantCrossPlatform, SingleTenantSinglePlatform
# 无认证路由
p, SingleTenantSinglePlatform, *, /healths, GET, allow
p, SingleTenantSinglePlatform, *, /login, POST, allow
# 用户相关路由
p, MultiTenantSinglePlatform, root, /organizations/:organizationId/users, GET, allow
p, SingleTenantSinglePlatform, admin, /users, GET, allow
p, SingleTenantSinglePlatform, user, /user, GET, allow
Go 代码实现
首先将配置文件嵌入到代码中:
package routers
import "embed"
//go:embed model.conf policy.csv
var fs embed.FS
func MustAssetString(name string) string {
s, err := fs.ReadFile(name)
if err != nil {
panic(err)
}
return string(s)
}
然后实现 Gin 的认证中间件:
type Auth struct {
Enforcer *casbin.Enforcer
}
func NewAuth() *Auth {
m := model.NewModel()
err := m.LoadModelFromText(routers.MustAssetString("model.conf"))
if err != nil {
logrus.Panicf("load casbin model failed: %v", err)
}
enforcer, err := casbin.NewEnforcer(m, textadapter.NewAdapter(routers.MustAssetString("policy.csv")))
if err != nil {
logrus.Panicf("new enforcer failed: %v", err)
}
return &Auth{Enforcer: enforcer}
}
中间件的处理逻辑:
func (a *Auth) HandlerFunc() gin.HandlerFunc {
return func(c *gin.Context) {
user, valid, err := a.validateByAuthorization(c)
if err != nil {
c.Render(err)
c.Abort()
return
}
if !valid {
user, valid, err = a.validateByCookie(c)
if err != nil {
c.Render(err)
c.Abort()
return
}
}
// 权限检查
if valid {
if ok, _ := a.checkPermission(user, c.Request.URL.Path, c.Request.Method); ok {
c.Next()
return
}
} else {
if ok, _ := a.checkPublicAccess(c.Request.URL.Path, c.Request.Method); ok {
c.Next()
return
}
}
c.Render(401)
c.Abort()
}
}
func (a *Auth) checkPermission(user *models.User, path, method string) (bool, error) {
return a.Enforcer.Enforce(
systems.GetPlatformMode(),
fmt.Sprintf("%v", user.Role),
path,
method,
)
}
func (a *Auth) checkPublicAccess(path, method string) (bool, error) {
return a.Enforcer.Enforce(
systems.GetPlatformMode(),
"*",
path,
method,
)
}
一些小技巧
- 使用
embed
将配置文件编译到二进制中,避免部署时的文件管理问题 - 权限检查前先判断是否为公共接口,可以减少一次权限查询
- 对于常用的权限组合,可以使用角色继承来简化配置
- 使用
keyMatch2
支持路径参数的匹配,比如/users/:id
总结
Casbin + Gin 的组合可以很好地实现 RBAC 权限控制,关键是:
- 设计合理的权限模型
- 规划好角色继承关系
后续计划添加基于 Redis 的缓存层,以及支持动态更新权限规则的功能。
这是我的一些实践经验,如果你有更好的想法,欢迎一起探讨。