Contents

casbin

Contents

Casbin

一、介绍

Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型,Casbin只负责访问控制。

其功能有:

  • 支持自定义请求的格式,默认的请求格式为{subject, object, action}
  • 具有访问控制模型model和策略policy两个核心概念。
  • 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
  • 支持内置的超级用户 例如:rootadministrator。超级用户可以执行任何操作而无需显式的权限声明。
  • 支持多种内置的操作符,如 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的定义,每条规则通常以形如ppolicy type开头,比如p,joker,data1,read就是一条joker具有data1读权限的规则。
  • role_definition:是RBAC角色继承关系的定义。g 是一个 RBAC系统,_, _表示角色继承关系的前项和后项,即前项继承后项角色的权限。
  • policy_effect:是对policy生效范围的定义,它对request的决策结果进行统一的决策,比如e = some(where (p.eft == allow))就表示如果存在任意一个决策结果为allow的匹配规则,则最终决策结果为allowp.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))
	}

}