go学习笔记——casbin权限管理

1.casbin简介

casbin是一个可用于Golang, Java, C/C++, Node.js, Javascript, PHP, Laravel, Python, .NET (C#), Delphi, Rust, Ruby, Lua (OpenResty), Dart (Flutter)和Elixir的授权库。

在golang web中可以使用casbin实现RBAC权限管理,类似java spring security。

官网:https://casbin.org/zh/

casbin支持多种访问模型,如ACL, RBAC, ABAC等,访问控制模型是基于PERM元模型 (Policy, Effect, Request, Matchers) 压缩而成的一个CONF文件。

casbin还支持多种策略存储方式,比如文件,MySQL、Postgres、Oracle到MongoDB、Redis、Cassandra、AWS S3等数十种数据库。参考:https://casbin.org/zh/docs/adapters/

casbin还可以通过middleware和多种语言的web框架进行集成,比如golang的Gin,Kratos等。参考:https://casbin.org/zh/docs/middlewares

2.casbin和kratos集成

可以参考kratos的example中的casbin项目:https://github.com/go-kratos/examples/tree/main/casbin

在这个例子中,作者定义了3种角色,分别具有不同的权限

  • admin:管理员角色,具有所有权限,显示所有标签页
  • moderator:普通用户角色,具有部分权限,无权限访问User标签页,无法看到Admin标签页
  • user:普通用户角色,具有一些权限,无权限访问User标签页,无法看到Admin和Moderator标签页

admin用户

moderator用户

user用户

在authz_model.conf文件中定义了访问控制模型,在authz_policy.csv中存储了权限策略

authz_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]

[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 || p.act == "*")
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
  • [request_definition]

    • 定义了请求的结构,即请求包含的参数。
    • 这里的 r = sub, obj, act 表示请求由三个要素组成:sub(主体,例如用户或角色)、obj(访问的对象,例如资源或 URL)、和 act(操作,例如读、写)。
  • [policy_definition]

    • 定义了权限策略的结构,表示策略包含的参数。
    • p = sub, obj, act 表示策略同样包含主体、对象和操作三部分。每条策略通过匹配这些属性来决定权限。
  • [role_definition]

    • 定义了角色的继承关系,用于角色访问控制(RBAC)。
    • g = _, _ 表示使用角色继承的关系。例如,g(alice, admin) 表示用户 aliceadmin 角色的成员,继承该角色的权限。
  • [policy_effect]

    • 定义策略的效果,说明在多条策略匹配时如何处理。
    • e = some(where (p.eft == allow)) 表示当某条策略生效(即 p.eft 等于 allow)时,允许请求通过。some(where (...)) 的语法意味着只要有一条策略允许访问即可。
  • [matchers]

    • 定义了请求和策略如何匹配,以生成访问决策。
    • m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") 规则的含义:
      • g(r.sub, p.sub):检查请求的主体 r.sub 是否具有策略中的角色 p.sub
      • keyMatch(r.obj, p.obj):支持 URL 模式匹配(如路径前缀匹配),判断请求对象 r.obj 是否匹配策略中的对象 p.obj
      • (r.act == p.act || p.act == "*"):检查请求的操作 r.act 是否与策略中的操作 p.act 相符,或者策略允许任意操作(`p.act == "*")。

authz_policy.csv文件如下

p, moderator, /admin.v1.AdminService/GetModeratorBoard, *
p, api_admin, /admin.v1.AdminService/*, *
g, admin, api_admin
  1. p, moderator, /admin.v1.AdminService/GetModeratorBoard, *

    • 这是一个权限策略 (p)。
    • moderator 角色可以访问 /admin.v1.AdminService/GetModeratorBoard 路径。
    • 最后一个参数是 *,表示对该路径上的任意操作(如 GET、POST 等)都允许。
  2. p, api_admin, /admin.v1.AdminService/*, *

    • 另一个权限策略 (p)。
    • api_admin 角色可以访问 /admin.v1.AdminService/ 下的所有路径(使用了通配符 *)。
    • 同样,* 表示允许对这些路径上的所有操作。
  3. g, admin, api_admin

    • 这是一个角色继承关系 (g)。
    • admin 角色继承了 api_admin 角色的权限,因此拥有 api_admin 角色所允许的所有权限。

参考:Go 每日一库之 casbin 以及 golang微服务框架Kratos实现鉴权 - Casbin

可以尝试启动这个casbin项目,体会一下

项目需要依赖Consul和Jaeger

docker pull bitnami/consul:latest

docker run -itd \
    --name consul-server-standalone \
    -p 8300:8300 \
    -p 8500:8500 \
    -p 8600:8600/udp \
    -e CONSUL_BIND_INTERFACE='eth0' \
    -e CONSUL_AGENT_MODE=server \
    -e CONSUL_ENABLE_UI=true \
    -e CONSUL_BOOTSTRAP_EXPECT=1 \
    -e CONSUL_CLIENT_LAN_ADDRESS=0.0.0.0 \
    bitnami/consul:latest

docker pull jaegertracing/all-in-one:latest

docker run -d \
    --name jaeger \
    -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
    -p 5775:5775/udp \
    -p 6831:6831/udp \
    -p 6832:6832/udp \
    -p 5778:5778 \
    -p 16686:16686 \
    -p 14268:14268 \
    -p 14250:14250 \
    -p 9411:9411 \
    jaegertracing/all-in-one:latest

启动后端,配置如下

访问swagger-ui:http://localhost:8800/q/swagger-ui

启动前端

yarn start

访问前端界面:http://localhost:8081

casbin提供多种不同的适配器,支持从配置文件,MySQL存储加载和存储策略规则。

casbin在很多语言中都有支持的middleware中间件用于提供casbin的鉴权功能。

比如 https://github.com/go-kratos/examples/blob/main/casbin/app/admin/internal/server/http.go 中使用的就是 tx7do/kratos-casbin 中间件来实现的casbin鉴权,代码如下

// NewMiddleware 创建中间件
func NewMiddleware(ac *conf.Auth, logger log.Logger) http.ServerOption {
	m, _ := model.NewModelFromFile("/Users/lintong/coding/go/examples/casbin/app/admin/configs/authz/authz_model.conf")
	a := fileAdapter.NewAdapter("/Users/lintong/coding/go/examples/casbin/app/admin/configs/authz/authz_policy.csv")

	return http.Middleware(
		recovery.Recovery(),
		tracing.Server(),
		logging.Server(logger),
		selector.Server(
			jwt.Server(
				func(token *jwtV4.Token) (interface{}, error) {
					return []byte(ac.ApiKey), nil
				},
				jwt.WithSigningMethod(jwtV4.SigningMethodHS256),
			),
			casbinM.Server(
				casbinM.WithCasbinModel(m),
				casbinM.WithCasbinPolicy(a),
				casbinM.WithSecurityUserCreator(myAuthz.NewSecurityUser),
			),
		).
			Match(NewWhiteListMatcher()).Build(),
	)
}

而 SecurityUser 于创建Jwt的令牌,以及后面Casbin解析和存取权鉴相关的数据,需要实现其如下方法

type SecurityUser interface {
	// ParseFromContext parses the user from the context.
	ParseFromContext(ctx context.Context) error

	// GetSubject returns the subject of the token.
	GetSubject() string

	// GetObject returns the object of the token.
	GetObject() string

	// GetAction returns the action of the token.
	GetAction() string
	// GetDomain returns the domain of the token.
	GetDomain() string
}

代码如下

package authz

import (
	"context"
	"errors"
	"fmt"

	"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
	"github.com/go-kratos/kratos/v2/transport"
	jwtV4 "github.com/golang-jwt/jwt/v4"
	authzM "github.com/tx7do/kratos-casbin/authz"
)

const (
	ClaimAuthorityId = "authorityId"
)

type SecurityUser struct {
	Path        string
	Method      string
	AuthorityId string
	Domain      string
}

func NewSecurityUser() authzM.SecurityUser {
	return &SecurityUser{}
}

func (su *SecurityUser) ParseFromContext(ctx context.Context) error {
	if claims, ok := jwt.FromContext(ctx); ok {
		su.AuthorityId = claims.(jwtV4.MapClaims)[ClaimAuthorityId].(string)
	} else {
		return errors.New("jwt claim missing")
	}

	if header, ok := transport.FromServerContext(ctx); ok {
		su.Path = header.Operation()
		su.Method = "*"
	} else {
		return errors.New("jwt claim missing")
	}

	return nil
}

func (su *SecurityUser) GetSubject() string {
	return su.AuthorityId
}

func (su *SecurityUser) GetObject() string {
	return su.Path
}

func (su *SecurityUser) GetAction() string {
	return su.Method
}

func (su *SecurityUser) GetDomain() string {
	return su.Domain
}

参考:https://tx7do.github.io/docs/kratos_auth_authz.html

3.casbin和gin集成

1.basic auth+casbin

下面举一个gin框架集成basic auth和casbin框架的例子

basic auth集成参考:https://gin-gonic.com/docs/examples/using-basicauth-middleware/

首先添加一个basic auth的middleware

// basic auth
authMiddleware := gin.BasicAuth(gin.Accounts{
	"foo":    "bar", //用户名:密码
	"austin": "1234",
	"lena":   "hello2",
	"admin":   "admin",
})
authGroup := r.Group("/api/v1", authMiddleware)

再基于basic auth添加casbin,参考:https://github.com/gin-contrib/authz

go get github.com/gin-contrib/authz

注意这个库需要基于basic auth

func NewEnforcer() (*casbin.Enforcer, error) {
	enforcer, err := casbin.NewEnforcer("./conf/authz_model.conf", "./conf/authz_policy.csv")
	if err != nil {
		panic(err)
	}
	return enforcer, nil
}

// casbin
enforcer, err := auth.NewEnforcer()
if err != nil {
	panic(err)
}
authGroup.Use(authz.NewAuthorizer(enforcer))

// handler
UserRouters(userHandler, authGroup)

其中userHandler中有如下接口用于获取user

/api/v1/user/{id}

authz_model.conf访问控制模型配置文件,其中keyMatch用于匹配/api/v1/user/*的path,参考:https://casbin.org/docs/function/

[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) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")

authz_policy.csv权限策略文件

p, admin, /api/v1/user/*, *
p, admin, /api/v1/user, POST
p, foo, /api/v1/user/1, GET
p, austin, /api/v1/user, POST
p, lena, /api/v1/user, PUT

admin用户拥有创建user的权限

curl -i 'http://localhost:18080/api/v1/user' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic YWRtaW46YWRtaW4=' \
--data-raw '{
    "username": "test",
    "email":"test@test"
}'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 23 Nov 2024 15:48:38 GMT
Content-Length: 98

{"code":200,"msg":"create user success","data":{"id":10025,"username":"test","email":"test@test"}}%

foo用户只有访问id=1的用户的权限,访问id=2会返回403

curl -I -X GET 'http://localhost:18080/api/v1/user/2' \
--header 'Authorization: Basic Zm9vOmJhcg==' \
--header 'Content-Type: application/json'
HTTP/1.1 403 Forbidden
Date: Sun, 17 Nov 2024 16:21:01 GMT
Content-Length: 0

2.使用GORM adapter

如果使用MySQL存储policy的话,可以使用gorm-adapter,参考:https://casbin.org/zh/docs/adapters/ 以及 https://dev.to/maxwellhertz/tutorial-integrate-gin-with-cabsin-56m0

go get github.com/casbin/gorm-adapter/v3

并在enforce中从csv文件的权限策略适配器,修改成gorm的适配器

func NewEnforcer(adapter *gormadapter.Adapter) (*casbin.Enforcer, error) {
	//enforcer, err := casbin.NewEnforcer("./conf/authz_model.conf", "./conf/authz_policy.csv")
	enforcer, err := casbin.NewEnforcer("./conf/authz_model.conf", adapter)
	if err != nil {
		panic(err)
	}
	return enforcer, nil
}

func NewGormAdapter(conf *viper.Viper) (*gormadapter.Adapter, error) {
	dsn := conf.GetString("db.mysql.dsn")
	print(dsn)
	adapter, err := gormadapter.NewAdapter("mysql", dsn, true)
	if err != nil {
		panic(err)
	}
	return adapter, nil
}

运行代码后,会自动在MySQL数据库中创建一张名为casbin_rule的表,如下

表定义应为(id int primary key, ptype varchar, v0 varchar, v1 varchar, v2 varchar, v3 varchar, v4 varchar, v5 varchar)。

唯一键索引应在ptype,v0,v1,v2,v3,v4,v5列上建立。

使用如下代码可以添加policy

package auth

import (
	"fmt"
	"gin-template/internal/config"
	zaplog "gin-template/internal/logger"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"testing"
)

var (
	conf       *viper.Viper
	zap_logger *zap.Logger
)

func init() {
	conf = config.InitConfig()
	zap_logger = zaplog.InitLogger(conf)
}

func TestNewGormAdapter(t *testing.T) {
	adapter, err := NewGormAdapter(conf)
	if err != nil {
		fmt.Println(err)
		return
	}
	enforcer, err := NewEnforcer(adapter)
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = enforcer.AddPolicy("jack", "data1", "write")
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = enforcer.AddPolicy("jack", "data2", "write")
	if err != nil {
		fmt.Println(err)
		return
	}
}

参考:https://github.com/casbin/gorm-adapter

如果要移除policy

// 删除policy
_, err = enforcer.RemovePolicy("jack", "data3", "write")
if err != nil {
	fmt.Println(err)
	return
}
// 删除index=0位置的值等于jack的policy
_, err = enforcer.RemoveFilteredPolicy(0, "jack")
if err != nil {
	fmt.Println(err)
	return
}

3.刷新权限policy

当权限policy变更了之后,为了保证鉴权结果的正确,需要即使加载最新的权限policy,方法有如下几种

1.定时任务调用 LoadPolicy 方法

// 开启定时自动加载策略
go func() {
	ticker := time.NewTicker(10 * time.Second) // 每隔 10 秒加载一次
	defer ticker.Stop()
	for range ticker.C {
		err := enforcer.LoadPolicy()
		if err != nil {
			// 记录错误日志
			fmt.Println("Failed to load policy:", err.Error())
		} else {
			fmt.Println("Policy reloaded successfully.")
		}
	}
}()

2.手动调用 LoadPolicy 方法

在要鉴权之前,调用如下代码

enforcer.LoadPolicy()

3.使用 Watcher 机制

可以使用watcher来通知每个实例的enforcer更新权限策略,参考:https://casbin.org/docs/watchers/

这里以 https://github.com/casbin/redis-watcher 为例,使用这个redis-watcher需要依赖golang redis v9

// casbin watcher
watcher, _ := rediswatcher.NewWatcher("localhost:3306", rediswatcher.WatcherOptions{
	Options: redis.Options{
		Network:  "tcp",
		Password: "",
	},
	Channel:    "/casbin",
	// Only exists in test, generally be true
	IgnoreSelf: false,
})
err := enforcer.SetWatcher(watcher)
if err != nil {
	zap.L().Error(err.Error())
}
err = watcher.SetUpdateCallback(func(msg string) {
    println(msg)
    enforcer.LoadPolicy()
})
if err != nil {
	zap.L().Error(err.Error())
}

其中updateCallback是回调函数,这里打印一下权限变更的msg

写一个测试用例添加一下policy,这里也设置了watcher,会将权限变更的信息写到redis中

func TestNewGormAdapter(t *testing.T) {
	adapter, err := NewGormAdapter(conf)
	if err != nil {
		fmt.Println(err)
		return
	}
	enforcer, err := NewEnforcer(adapter)
	if err != nil {
		fmt.Println(err)
		return
	}
	watcher, _ := rediswatcher.NewWatcher("localhost:3306", rediswatcher.WatcherOptions{
		Options: redis.Options{
			Network:  "tcp",
			Password: "",
		},
		Channel:    "/casbin",
		// Only exists in test, generally be true
		IgnoreSelf: false,
	})
	err = enforcer.SetWatcher(watcher)

	if err != nil {
		zap.L().Error(err.Error())
	}
	_, err = enforcer.AddPolicy("jack", "data1", "write")
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = enforcer.AddPolicy("jack", "data2", "write")
	if err != nil {
		fmt.Println(err)
		return
	}
}

查看原先的日志,获取到2次policy变更的消息

{"Method":"UpdateForAddPolicy","ID":"a14b5075-bd4c-43fa-b987-8e500d9fcf46","Sec":"p","Ptype":"p","OldRule":null,"OldRules":null,"NewRule":["jack","data1","write"],"NewRules":null,"FieldIndex":0,"FieldValues":null}
{"Method":"UpdateForAddPolicy","ID":"a14b5075-bd4c-43fa-b987-8e500d9fcf46","Sec":"p","Ptype":"p","OldRule":null,"OldRules":null,"NewRule":["jack","data2","write"],"NewRules":null,"FieldIndex":0,"FieldValues":null}

 

4.casbin管理界面

casbin生态提供了admin-portal用于模型管理和策略管理,参考:https://casbin.org/zh/docs/admin-portal

casdoor就是其中之一(别用casbin-hub这个项目,已经过时),官网:https://casdoor.org/

clone项目

git clone https://github.com/casdoor/casdoor

修改配置文件app.conf中的数据库地址:dataSourceName

启动后端

go run main.go

后端会自动在MySQL中创建一个casdoor的数据库

启动前端

cd web
yarn install
yarn start

参考:https://casdoor.org/docs/basic/server-installation/

界面如下,默认的账号密码是admin 123

在casbin管理中可以添加casbin的model,adapter和enforcer

添加casbin model模型,即模型控制文件,比如上面的 authz_model.conf

添加casbin adapter适配器,需要指定MySQL表的名字,这里指定table的名字为casbin_rule,和GORM adapter默认创建的table名字保持一致

这个当然也可以选择和casdoor不同的MySQL database

添加casbin enforcer执行器,需要指定model和adapter,同时在enforcer中添加policy,如下

对应MySQL表中已经添加了该policy

 

posted @ 2018-06-07 23:58  tonglin0325  阅读(3286)  评论(0编辑  收藏  举报