基于Casbin的RBAC入门

基于Casbin的RBAC

最近有点松懈,有段时间没输入了......感觉自己对权限相关的东西还不是很熟,就打算学一波Casbin,后面应该还会有关于oAuth2相关的学习(就是一个第三方登录协议,比如QQ,WeChat登录......)

一、 Casbin简介

先给三个比较重要的东西吧,一个是Casbin的官网,一个是用Go写的demo,一个是Casbin的教学视频(之前都不太喜欢教学视频,但最近一个在家久了,感觉有个人在那边说话还不错哈哈哈)

Casbin官方网站

Casbin Demo

Casbin教学视频

下面是官网的介绍,由此可知,Casbin是一个访问控制框架,用户权限管理。

image-20220218221247663

介绍完Casbin,再来介绍一个什么事RBAC,它的全称就是Role-Based Access Control,即基于角色的权限控制,后面设计的库表也会基于这个理念,有兴趣的可以再去了解一下(我了解的也不是很多)。

二、 开始使用

1. 添加依赖

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

2. 添加配置文件

# basis_model.conf
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[role_definition]
g = _, _

[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (regexMatch(r.act, p.act) || methodMatch(r.act, p.act)) || isSuperAdmin(r.sub)

basis_model.conf主要是用来配置访问模型,Casbin会加载该配置文件,将我们的[request_definition][policy_definition]进行匹配,如果还是不懂,可以接着往下看。

# basis_policy.csv
p,member,/depts,GET
p,member,/depts/:id,GET


p,admin,/depts,POST
p,admin,/depts/:id,PUT
p,admin,/depts/:id,DELETE

g,admin,member,
g,ch,admin,
g,lisi,member,

basis_policy.csv用来存储用户权限配置,后续会用持久化替代掉。

这里解释一下:

p,member,/depts,GET,p是权限模型名,member是角色名,/depts是具体路由,GET是请求方式

g,admin,member,g还是模型名,这段的语意是admin继承member的权限

我们可以看到这个文件中配置的模型可以是多样的,继承|角色细化到路由的权限

3. Demo

import "github.com/casbin/casbin/v2"

e, err := casbin.NewEnforcer("resource/basis_model.conf", "resource/basis_policy.csv")

sub := "ch" // 想要访问资源的用户。
obj: = "/depts" // 将被访问的资源。
act := "POST" // 用户对资源执行的操作。

ok, err := e.Enforce(sub, obj, act)

if err != nil {
    // 处理err
}

if ok == true {
    // 允许ch读取/depts
} else {
    // 拒绝请求,抛出异常
}

以上就是最简单的demo了,我们通过配置权限规则,再通过表格存储用户权限,Casbin根据二则决定请求是否通过。

三、 持久化

1. 接入Gorm

var Gorm *gorm.DB

func InitGormDB() *gorm.DB {
	dsn := "root:123456@tcp(localhost:3306)/casbin_learning?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	mysqlDB, err := db.DB()
	if err != nil {
		panic(err)
	}
	mysqlDB.SetMaxIdleConns(5)
	mysqlDB.SetMaxOpenConns(5)
	Gorm = db
	return db
}

func InitAccess() {
	InitGormDB()
	adapter, err := gormadapter.NewAdapterByDB(Gorm)
	if err != nil {
		log.Fatalln(err)
	}
	e, err := casbin.NewEnforcer("resource/basis_model.conf", adapter)
	if err != nil {
		log.Fatalln(err)
	}
	err = e.LoadPolicy()
	if err != nil {
		log.Fatalln(err)
	}
	E = e
}

这里有个很有意思的点,SetMaxIdleConns设置的如果比SetMaxOpenConns大的话,MaxIdleConns会被减少到和MaxOpenConns一样,但是对这两个参数有点疑问,看了官方注释才知道,MaxIdleConns是连接池中最大连接数量,MaxOpenConns是可连接到该数据库的最大连接。

image-20220218223841201

接入Gorm后,我们先来看看Casbin默认提供的表吧

create table casbin_rule
(
    id    bigint unsigned auto_increment
        primary key,
    ptype varchar(100) 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 idx_casbin_rule
        unique (ptype, v0, v1, v2, v3, v4, v5)
);

字段没有明确的意思,由Casbin自己定义了一套规则去解释这些数据,我们要做的是根据它的规则设计表结构,然后填入其中,这里的规则也不是由Casbin自己瞎定义的,他也是基于多套成熟的理论,我们今天使用的是RBAC这套理论设计的库表:

CREATE TABLE IF NOT EXISTS t_user
(
    user_id     int         NOT NULL AUTO_INCREMENT comment '用户id',
    user_name   VARCHAR(32) NOT NULL DEFAULT '' comment '用户名',
    create_time TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP comment '创建时间',
    update_time TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新时间',
    primary key (user_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = UTF8MB4 comment '用户表';

CREATE TABLE IF NOT EXISTS t_role
(
    role_id      int         NOT NULL AUTO_INCREMENT comment '权限id',
    role_name    VARCHAR(32) NOT NULL DEFAULT '' comment '权限名',
    role_pid     int         NOT NULL comment '父权限id',
    role_comment VARCHAR(32) NOT NULL DEFAULT '' comment '权限备注',
    create_time  TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP comment '创建时间',
    update_time  TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新时间',
    primary key (role_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = UTF8MB4 comment '权限表';

CREATE TABLE IF NOT EXISTS t_user_role
(
    id          int       NOT NULL AUTO_INCREMENT comment 'id',
    user_id     int       NOT NULL comment '用户id',
    role_id     int       NOT NULL comment '权限id',
    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP comment '创建时间',
    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新时间',
    primary key (id)
) ENGINE = InnoDB
  DEFAULT CHARSET = UTF8MB4 comment '用户权限关系表';


CREATE TABLE IF NOT EXISTS t_route
(
    route_id     int          NOT NULL AUTO_INCREMENT comment '路由id',
    route_name   VARCHAR(32)  NOT NULL DEFAULT '' comment '路由名',
    route_uri    VARCHAR(255) NOT NULL DEFAULT '' comment '路由uri',
    route_method VARCHAR(16)  NOT NULL DEFAULT '' comment '方法',
    route_state  VARCHAR(16)  NOT NULL DEFAULT '' comment '状态',
    create_time  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP comment '创建时间',
    update_time  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新时间',
    primary key (route_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = UTF8MB4 comment '路由表';


CREATE TABLE IF NOT EXISTS t_role_route
(
    id          int       NOT NULL AUTO_INCREMENT comment 'id',
    role_id     int       NOT NULL comment '权限id',
    route_id    int       NOT NULL comment '路由id',
    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP comment '创建时间',
    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新时间',
    primary key (id)
) ENGINE = InnoDB
  DEFAULT CHARSET = UTF8MB4 comment '权限路由关系表';

总共有5张表,其中两张是关系表,用来维护多对多的关系,我们主要来看角色表,里面通过role_pid表示继承关系。

2. 初始化权限数据

各位大佬可以自己根据刚才的basis_policy.csv自己构造一下数据库中的数据,这里只提供需要插入的3种数据,具体的代码大家可以参照我的github哈。

var E *casbin.Enforcer // 已经在其他地方完成了初始化

func initPolicy() {

	// 初始化继承关系
	m := make([]*RoleRel, 0)
	GetRoles(0, &m)
	for _, r := range m {
		_, err := E.AddRoleForUser(r.PRole, r.Role)
		if err != nil {
			log.Fatalln("初始化继承关系失败!")
		}
	}

	// 初始化用户角色
	userRoles := GetUserRoles()
	for _, userRole := range userRoles {
		_, err := E.AddRoleForUser(userRole.UserName, userRole.RoleName)
		if err != nil {
			log.Fatalln("初始化用户角色失败!")
		}
	}

	// 初始化角色路由
	roleRoutes := GetRoleRoutes()
	for _, roleRoute := range roleRoutes {
		_, err := E.AddPolicy(roleRoute.RoleName, roleRoute.RouteUri, roleRoute.RouteMethod)
		if err != nil {
			log.Fatalln("初始化角色路由!")
		}
	}
}

至此我们完成了权限数据的持久化,并将这些数据动态添加到了Casbin框架中,由他负责权限的校验。

四、 其他

1. 函数配置

1.1. 内置

在Casbin的配置文件中我们可以使用框架内置的函数也可以使用自定义的函数:

image-20220218230108010

在通配符这块Go可以使用keyMatch2(r.obj, p.obj)去适配。

1.2. 自定义

func MethodMatch(key1, key2 string) bool {
	ks := strings.Split(key2, ",")
	for _, v := range ks {
		if v == key1 {
			return true
		}
	}
	return false
}

func InitFunctions() {
	E.AddFunction("methodMatch", func(arguments ...interface{}) (interface{}, error) {
		if len(arguments) == 2 {
			key1, key2 := arguments[0].(string), arguments[1].(string)
			return MethodMatch(key1, key2), nil
		}
		return nil, fmt.Errorf("methodMatch err")
	})
}

// 在配置文件中的使用方式:methodMatch(r.act, p.act));该字符串和AddFunction的第一个参数一致

2. 超级管理员

怎么设置超级管理员呢?

2.1. 静态

image-20220218225931838

2.2. 动态

用自定义函数可以动态添加

func IsSuperAdmin(key string) bool {
	for _, v := range admin { // admin可以从DB中查出来
		if v == key {
			return true
		}
	}
	return false
}

func InitFunctions() {
	E.AddFunction("isSuperAdmin", func(arguments ...interface{}) (interface{}, error) {
		if len(arguments) == 1 {
			key := arguments[0].(string)
			return IsSuperAdmin(key), nil
		}
		return nil, fmt.Errorf("isSuperAdmin err")
	})
}

// isSuperAdmin(r.sub)

3. 注意点

// 1. 项目启动后删除casbin_rule表中数据,权限系统依然能够运行,由此判断casbin_rule的数据会被载入内存中,并没实时查表
// 2. 配置文件的内容也不会热更新
// 3. cas_bin先加载配置文件和数据源,之后可以通过代码动态添加

五、 总结

可以看出来入门Casbin还是很简单的,也了解了一下Go的权限框架大概怎么玩,算是小补了一下这方面的知识,不过真的不算深入,如果有什么问题可以指出或一起讨论。

(hai,一个晚上就这么过去了......晚安哈~)

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×