1. Casbin是什么?
Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。因此Casbin不能做身份验证, 最佳的实践是只负责访问控制
1.1 Casbin的model
Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件,这个文件的具体呈现是一个以 .conf 作为后缀的文件
example :
rbac_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
对于上面配置文件简单的理解是:
1.1.1 [request_definition]
r = sub, obj, act :定义请求由三部分组成 访问用户的用户 Subject , 访问的资源 Object 访问的动作 Action
1.1.2 [policy_definition]
p = sub, obj, act : 定策略的格式 , 参数的基本意思和定义请求的相同 ,定义好了策略格式,那么对于策略(Policy)的具体描述可以存放在一个以 .csv 作为后缀的文件中
example :
rbac_Policy_example.csv
g, coder, root
g, zhangsan coder
p, root,api/v1/ping,GET
p, coder,api/v1/pong,GET
g, lisi, manager
p, manager, api/v1/user,POST
上面的rbac策略中我们定义了三条策略和三个用户组,我们来看一下这些策略都有啥作用
- coder是root的角色
- zhangsan是coder的角色
- root 可以访问 api/v1/ping 资源 通过GET动作,那么coder , zhangsan也可以访问
- coder可以访问 api/v1/pong 资源 通过GET动作,zhangsan也能访问
- lisi是manager的角色
- manager可以访问 api/v1/user资源通过POST动作,lisi也可以访问
1.1.3 [role_definition]
**g = _, _ ** : 是RBAC角色继承关系的定义 ,此处的 _, _
表示 前项继承后项角色的权限
1.1.4 [policy_effect]
e = some(where (p.eft == allow)) : 表示任意一条Policy策略满足那么结果就为allow
1.1.5 [matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act : 定义了策略匹配者。匹配者是一组表达式。它定义了如何根据请求来匹配策略规则,匹配表达式的写法比较灵活根据具体需求来编写即可.
而此处的表达式意思是 ,检测用户角色 && 检测用户访问的资源 &&检测用户的动作 (&&
表示并且关系,当然也有其他逻辑运算符 ||
,!
等)
1.2 Casbin的Policy
Policy 主要表示访问控制关于角色,资源,行为的具体映射关系这比较好处理,但是这种映射关系怎么存储就值得考虑了
1.2.1 csv 文件存储
访问控制模型 | Model 文件 | Policy 文件 |
---|---|---|
ACL | basic_model.conf | basic_policy.csv |
具有超级用户的ACL | basic_with_root_model.conf | basic_policy.csv |
没有用户的ACL | basic_without_users_model.conf | basic_without_users_policy.csv |
没有资源的ACL | basic_without_resources_model.conf | basic_without_resources_policy.csv |
RBAC | rbac_model.conf | rbac_policy.csv |
支持资源角色的RBAC | rbac_with_resource_roles_model.conf | rbac_with_resource_roles_policy.csv |
支持域/租户的RBAC | rbac_with_domains_model.conf | rbac_with_domains_policy.csv |
ABAC | abac_model.conf | 无 |
RESTful | keymatch_model.conf | keymatch_policy.csv |
拒绝优先 | rbac_with_not_deny_model.conf | rbac_with_deny_policy.csv |
Allow-and-deny | rbac_with_deny_model.conf | rbac_with_deny_policy.csv |
Priority | priority_model.conf | priority_policy.csv |
1.2.2 适配器存储
casbin的适配器 adapter 可以从存储中加载策略规则,也可将策略规则保存到不同的存储系统中
支持如: MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等等存储系统
2. gin集成Casbin实现RESTful接口访问控制
2.1 go mod 构建项目
# 新建个叫做ginCasbin的gomod项目(项目名自定义)
go mod init GinCasbin
2.2 安装依赖包
# 安装依赖包
# 安装gin框架
go get -u github.com/gin-gonic/gin
# Go语言casbin的依赖包
go get github.com/casbin/casbin
# gorm 适配器依赖包
go get github.com/casbin/gorm-adapter
# mysql驱动依赖
go get github.com/go-sql-driver/mysql
# gorm 包
go get github.com/jinzhu/gorm
# 高性能缓存BigCache
go get github.com/allegro/bigcache/v2
2.3 目录规划说明
├─app # 业务目录
│ ├─api ## 存放api的目录(暂时不用)
│ ├─model ## 存放实体的目录(暂时不用)
│ └─service ## 存放业务代码的目录(暂时不用)
├─config # 存放配置文件的目录
├─middleware # 存放中间件的目录
├─routers # 存放路由的目录
└─utils # 常用工具组件目录
├─ACS ## 存放访问控制执行器目录
├─APIResponse ## 存放API统一响应函数目录
├─Cache ## 缓存工具目录
└─DB ## 数据连接文件目录
├─go.mod
├─go.sum
├─main.go # 项目入口文件
2.4 项目代码开发
2.4.1 工具组件开发
# 进入utils目录
cd utils
DB/mysql.go
package DB
import (
"fmt"
"github.com/jinzhu/gorm"
)
import _ "github.com/go-sql-driver/mysql"
var (
Mysql *gorm.DB
)
func init() {
var err error
dsn := "root:root@(127.0.0.1:3306)/xz_boss?charset=utf8&parseTime=True&loc=Local"
Mysql, err = gorm.Open("mysql", dsn)
if err != nil {
fmt.Println("connect DB error")
panic(err)
}
}
ACS/enforcer.go
package ACS
import (
"GinCasbin/utils/DB"
"github.com/casbin/casbin"
"github.com/casbin/gorm-adapter"
)
var Enforcer *casbin.Enforcer
func init() {
// mysql 适配器
adapter := gormadapter.NewAdapterByDB(DB.Mysql)
// 通过mysql适配器新建一个enforcer
Enforcer = casbin.NewEnforcer("config/keymatch2_model.conf", adapter)
// 日志记录
Enforcer.EnableLog(true)
}
APIResponse/response.go
package APIResponse
import "github.com/gin-gonic/gin"
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
var C *gin.Context
func Error(message string) {
if len(message) == 0 {
message = "fail"
}
C.JSON(200, Response{
Code: -1,
Message: message,
Data: nil,
})
}
func Success(data interface{}) {
C.JSON(200, Response{
Code: 200,
Message: "success",
Data: data,
})
}
Cache/big.go
package Cache
import (
"github.com/allegro/bigcache/v2"
"time"
)
var GlobalCache *bigcache.BigCache
func init() {
// 初始化BigCache实例
GlobalCache, _ = bigcache.NewBigCache(bigcache.DefaultConfig(30 * time.Minute))
}
2.4.2 配置文件
常规项目中配置文件目录中会存放各种配置文件,在这个Demo中仅将casbin的模型文件放在这里
cd ../config
config/keymatch2_model.conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
2.4.3 中间件
此处我们编写的一个基于casbin权限控制的中间件
cd ../middleware
middleware/privilege.go
package middleware
import (
"GinCasbin/utils/ACS"
"GinCasbin/utils/APIResponse"
"GinCasbin/utils/Cache"
"github.com/gin-gonic/gin"
"log"
)
func Privilege() gin.HandlerFunc {
return func(c *gin.Context) {
APIResponse.C = c
var userName = c.GetHeader("userName")
if userName == "" {
APIResponse.Error("header miss userName")
c.Abort()
return
}
path := c.Request.URL.Path
method := c.Request.Method
cacheName := userName + path + method
// 从缓存中读取&判断
entry, err := Cache.GlobalCache.Get(cacheName)
if err == nil && entry != nil {
if string(entry) == "true" {
c.Next()
} else {
APIResponse.Error("access denied")
c.Abort()
return
}
} else {
// 从数据库中读取&判断
//记录日志
ACS.Enforcer.EnableLog(true)
// 加载策略规则
err := ACS.Enforcer.LoadPolicy()
if err != nil {
log.Println("loadPolicy error")
panic(err)
}
// 验证策略规则
result, err := ACS.Enforcer.EnforceSafe(userName, path, method)
if err != nil {
APIResponse.Error("No permission found")
c.Abort()
return
}
if !result {
// 添加到缓存中
Cache.GlobalCache.Set(cacheName, []byte("false"))
APIResponse.Error("access denied")
c.Abort()
return
} else {
Cache.GlobalCache.Set(cacheName, []byte("true"))
}
c.Next()
}
}
}
2.4.4 路由文件
cd ../routers
routers/route.go
package routers
import (
"GinCasbin/middleware"
"GinCasbin/utils/ACS"
"GinCasbin/utils/APIResponse"
"GinCasbin/utils/Cache"
"github.com/gin-gonic/gin"
)
var (
R *gin.Engine
)
func init() {
R = gin.Default()
R.NoRoute(func(c *gin.Context) {
c.JSON(400, gin.H{"code": 400, "message": "Bad Request"})
})
api()
}
func api() {
auth := R.Group("/api")
{
// 模拟添加一条Policy策略
auth.POST("acs", func(c *gin.Context) {
APIResponse.C = c
subject := "tom"
object := "/api/routers"
action := "POST"
cacheName := subject + object + action
result := ACS.Enforcer.AddPolicy(subject, object, action)
if result {
// 清除缓存
_ = Cache.GlobalCache.Delete(cacheName)
APIResponse.Success("add success")
} else {
APIResponse.Error("add fail")
}
})
// 模拟删除一条Policy策略
auth.DELETE("acs/:id", func(context *gin.Context) {
APIResponse.C = context
result := ACS.Enforcer.RemovePolicy("tom", "/api/routers", "POST")
if result {
// 清除缓存 代码省略
APIResponse.Success("delete Policy success")
} else {
APIResponse.Error("delete Policy fail")
}
})
// 获取路由列表
auth.POST("/routers", middleware.Privilege(), func(c *gin.Context) {
type data struct {
Method string `json:"method"`
Path string `json:"path"`
}
var datas []data
routers := R.Routes()
for _, v := range routers {
var temp data
temp.Method = v.Method
temp.Path = v.Path
datas = append(datas, temp)
}
APIResponse.C = c
APIResponse.Success(datas)
return
})
}
// 定义路由组
user := R.Group("/api/v1")
// 使用访问控制中间件
user.Use(middleware.Privilege())
{
user.POST("user", func(c *gin.Context) {
c.JSON(200, gin.H{"code": 200, "message": "user add success"})
})
user.DELETE("user/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"code": 200, "message": "user delete success " + id})
})
user.PUT("user/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"code": 200, "message": "user update success " + id})
})
user.GET("user/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"code": 200, "message": "user Get success " + id})
})
}
}
2.4.5 项目入口文件
cd ..
main.go
package main
import (
. "GinCasbin/routers"
)
func main() {
R.Run()
}
2.5 测试访问策略
2.5.1 启动项目
# 运行项目
go run main.go
# gin框架在debug模式下的输出
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /api/acs --> GinCasbin/routers.api.func1 (3 handlers)
[GIN-debug] DELETE /api/acs/:id --> GinCasbin/routers.api.func2 (3 handlers)
[GIN-debug] POST /api/routers --> GinCasbin/routers.api.func3 (4 handlers)
[GIN-debug] POST /api/v1/user --> GinCasbin/routers.api.func4 (4 handlers)
[GIN-debug] DELETE /api/v1/user/:id --> GinCasbin/routers.api.func5 (4 handlers)
[GIN-debug] PUT /api/v1/user/:id --> GinCasbin/routers.api.func6 (4 handlers)
[GIN-debug] GET /api/v1/user/:id --> GinCasbin/routers.api.func7 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
2.5.2 测试casbin访问控制
新开启一个命令行终端
# 访问接口
# 参数缺失
curl -X POST http://127.0.0.1:8080/api/routers
{"code":-1,"message":"header miss userName","data":null}
# 无访问权限
curl -X POST -H "userName:tom" http://127.0.0.1:8080/api/routers
{"code":-1,"message":"access denied","data":null}
# 添加一条规则(代码中是模拟数据)
curl -X POST http://127.0.0.1:8080/api/acs
{"code":200,"message":"success","data":"add success"}
# 再次访问(有访问权限,可以访问)
curl -X POST -H "userName:tom" http://127.0.0.1:8080/api/routers
{
"code":200,
"message":"success",
"data":[
{
"method":"POST",
"path":"/api/acs"
},
{
"method":"POST",
"path":"/api/routers"
},
{
"method":"POST",
"path":"/api/v1/user"
},
{
"method":"DELETE",
"path":"/api/acs/:id"
},
{
"method":"DELETE",
"path":"/api/v1/user/:id"
},
{
"method":"PUT",
"path":"/api/v1/user/:id"
},
{
"method":"GET",
"path":"/api/v1/user/:id"
}
]
}
# 直接向数据库添加几条Policy策略
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user', 'POST', NULL, NULL, NULL);
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user/:id', 'GET', NULL, NULL, NULL);
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user/:id', 'PUT', NULL, NULL, NULL);
#再测试
## 添加接口
curl -X POST -H "userName:admin" http://127.0.0.1:8080/api/v1/user
{"code":200,"message":"user add success"}
## 查询接口
curl -X GET -H "userName:admin" http://127.0.0.1:8080/api/v1/user/99
{"code":200,"message":"user Get success 99"}
## 更新接口
curl -X PUT -H "userName:admin" http://127.0.0.1:8080/api/v1/user/199
{"code":200,"message":"user update success 199"}
## 删除接口(没有分配访问权限)
curl -X DELETE -H "userName:admin" http://127.0.0.1:8080/api/v1/user/299
{"code":-1,"message":"access denied","data":null}
2.6 其他
casbin的一些适配器有自动保存功能而另外一些则没有,有自动保存功能的适配器会在连接数据的时候自动创建一张表用来保存Policy策略数据(替代存储Policy的csv文件)
上述 Demo 的SQL文件如下(该表是gorm适配器自动创建的)
casbin_rule.sql
-- ----------------------------
-- Table structure for casbin_rule
-- ----------------------------
DROP TABLE IF EXISTS `casbin_rule`;
CREATE TABLE `casbin_rule` (
`p_type` varchar(100) DEFAULT NULL,
`v0` varchar(100) DEFAULT NULL,
`v1` varchar(100) DEFAULT NULL,
`v2` varchar(100) DEFAULT NULL,
`v3` varchar(100) DEFAULT NULL,
`v4` varchar(100) DEFAULT NULL,
`v5` varchar(100) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of casbin_rule
-- ----------------------------
INSERT INTO `casbin_rule` VALUES ('p', 'zhangsan', '/api/v1/ping', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'coder', '/api/v2/user/:id', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'coder', '/api/v2/routers', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user', 'POST', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user/:id', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user/:id', 'PUT', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'tom', '/api/routers', 'POST', '', '', '');
参考资料
- [1] casbin