casbin
Contents
Casbin
一、介绍
Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型,Casbin只负责访问控制。
其功能有:
- 支持自定义请求的格式,默认的请求格式为
{subject, object, action}。 - 具有访问控制模型model和策略policy两个核心概念。
- 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
- 支持内置的超级用户 例如:
root或administrator。超级用户可以执行任何操作而无需显式的权限声明。 - 支持多种内置的操作符,如
keyMatch,方便对路径式的资源进行管理,如/foo/bar可以映射到/foo*
1、Casbin的工作原理
在 Casbin 中, 访问控制模型被抽象为基于 **PERM **(Policy, Effect, Request, Matcher) [策略,效果,请求,匹配器]的一个文件。
- Policy:定义权限的规则
- Effect:定义组合了多个Policy之后的结果
- Request:访问请求
- Matcher:判断Request是否满足Policy
首先会定义一堆Policy,让后通过Matcher来判断Request和Policy是否匹配,然后通过Effect来判断匹配结果是Allow还是Deny。
2、核心概念
Model
Model是Casbin的具体访问模型,其主要以文件的形式出现,该文件常常以.conf最为后缀。
- Model CONF 至少应包含四个部分:
[request_definition],[policy_definition],[policy_effect],[matchers]。 - 如果 model 使用 RBAC, 还需要添加
[role_definition]部分。 - Model CONF 文件可以包含注释。注释以 # 开头, # 会注释该行剩余部分。
# Request定义
[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
- request_definition:用于request的定义,它明确了e.Enforce(…)函数中参数的定义,
sub, obj, act表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。 - policy_definition:用于policy的定义,每条规则通常以形如
p的policy type开头,比如p,joker,data1,read就是一条joker具有data1读权限的规则。 - role_definition:是RBAC角色继承关系的定义。
g是一个 RBAC系统,_, _表示角色继承关系的前项和后项,即前项继承后项角色的权限。 - policy_effect:是对policy生效范围的定义,它对request的决策结果进行统一的决策,比如
e = some(where (p.eft == allow))就表示如果存在任意一个决策结果为allow的匹配规则,则最终决策结果为allow。p.eft表示策略规则的决策结果,可以为allow或者deny,当不指定规则的决策结果时,取默认值allow。 - matchers:定义了策略匹配者。匹配者是一组表达式,它定义了如何根据请求来匹配策略规则
Policy
Policy主要表示访问控制关于角色、资源、行为的具体映射关系。
p, alice, data1, read
p, bob, data2, write
p, data2_admin, data2, read
p, data2_admin, data2, write
g, alice, data2_admin
它的关系规则很简单,主要是选择什么方式来存储规则,目前官方提供csv文件存储和通过adapter适配器从其他存储系统中加载配置文件,比如MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等。
3、使用
postgresql 建表语句
CREATE TABLE casbin_rule ( id serial4 NOT NULL, ptype varchar(100) NOT NULL, v0 varchar(100) NULL, v1 varchar(100) NULL, v2 varchar(100) NULL, v3 varchar(100) NULL, v4 varchar(100) NULL, v5 varchar(100) NULL, CONSTRAINT casbin_rule_pkey PRIMARY KEY (id));
CREATE INDEX idx_casbin_rule_composite ON public.casbin_rule USING btree (v0, v1, v2);
CREATE INDEX idx_casbin_rule_ptype ON public.casbin_rule USING btree (ptype);
CREATE INDEX idx_casbin_rule_v0 ON public.casbin_rule USING btree (v0);
CREATE INDEX idx_casbin_rule_v1 ON public.casbin_rule USING btree (v1);
CREATE INDEX idx_casbin_rule_v2 ON public.casbin_rule USING btree (v2);
测试数据
INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES
('p', 'admin', '*', '*', NULL, NULL, NULL),
('p', 'user', '/api/v1/user/list-option', 'POST', NULL, NULL, NULL);
('g', 'alice', 'admin', NULL, NULL, NULL, NULL);
('g', 'bob', 'user', NULL, NULL, NULL, NULL);
说明:
声明了两个角色,admin和user,admin可以对任意接口和方法请求,user只能post请求一个,这两个角色分别绑定了一个用户
接入中间间
package middleware
import (
"log/slog"
"net/http"
"strings"
"fox/internal/pkg/utils"
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
)
// AuthMiddleware jwt验证中间件
func AuthMiddleware(e *casbin.Enforcer, whitelistMap map[string]string) gin.HandlerFunc {
return func(c *gin.Context) {
if _, ok := whitelistMap[c.Request.URL.Path]; ok {
c.Next()
return
}
// 从 Header 取出 token
jwtToken := ""
header := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
if len(header) != 2 || !strings.EqualFold(header[0], "Bearer") {
jwtToken = c.Query("token")
if jwtToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": "7000", "error": "invalid token"})
c.Abort()
return
}
} else {
jwtToken = header[1]
}
claims, err := utils.ParseToken(jwtToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": "7000", "error": "invalid token"})
c.Abort()
return
}
// casbin 鉴权
sub := claims.Username
obj := c.Request.URL.Path
act := c.Request.Method
allowed, err := e.Enforce(sub, obj, act)
if err != nil {
slog.Error("casbin enforce error", "err", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"code": "6000", "error": "authorization error"})
return
}
if !allowed {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": "5000", "error": "no permissions"})
return
}
// 把解析后的用户信息存入 Context
c.Set("username", claims.Username)
c.Next()
}
}
func RegisterMiddleware(app *gin.Engine, cfg *config.Config, e *casbin.Enforcer, whitelistMap map[string]string) {
// app.Use(
// ginzap.Ginzap(pkg.Log, time.RFC3339, true),
// ginzap.RecoveryWithZap(pkg.Log, true),
// cors.Default())
app.Use(
timeoutMiddleware(),
AuthMiddleware(e, whitelistMap),
cors.Default(),
slog.SetLogger(),
gin.Recovery(),
)
if cfg.ENV != "local" {
// app.Use(CasbinMiddleware(whitelist))
}
}