1. Casbin是什么?
Casbin是一個強(qiáng)大的、高效的開源訪問控制框架瓤的,其權(quán)限管理機(jī)制支持多種訪問控制模型圈膏。因此Casbin不能做身份驗證, 最佳的實踐是只負(fù)責(zé)訪問控制
1.1 Casbin的model
Casbin 中, 訪問控制模型被抽象為基于 PERM (Policy, Effect, Request, Matcher) 的一個文件,這個文件的具體呈現(xiàn)是一個以 .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 : 定策略的格式 , 參數(shù)的基本意思和定義請求的相同 ,定義好了策略格式,那么對于策略(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角色繼承關(guān)系的定義 ,此處的 _, _
表示 前項繼承后項角色的權(quán)限
1.1.4 [policy_effect]
e = some(where (p.eft == allow)) : 表示任意一條Policy策略滿足那么結(jié)果就為allow
1.1.5 [matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act : 定義了策略匹配者睦擂。匹配者是一組表達(dá)式。它定義了如何根據(jù)請求來匹配策略規(guī)則,匹配表達(dá)式的寫法比較靈活根據(jù)具體需求來編寫即可.
而此處的表達(dá)式意思是 ,檢測用戶角色 && 檢測用戶訪問的資源 &&檢測用戶的動作 (&&
表示并且關(guān)系,當(dāng)然也有其他邏輯運算符 ||
,!
等)
1.2 Casbin的Policy
Policy 主要表示訪問控制關(guān)于角色,資源,行為的具體映射關(guān)系這比較好處理,但是這種映射關(guān)系怎么存儲就值得考慮了
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 |
拒絕優(yōu)先 | 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 可以從存儲中加載策略規(guī)則,也可將策略規(guī)則保存到不同的存儲系統(tǒng)中
支持如: MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等等存儲系統(tǒng)
2. gin集成Casbin實現(xiàn)RESTful接口訪問控制
2.1 go mod 構(gòu)建項目
# 新建個叫做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驅(qū)動依賴
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 目錄規(guī)劃說明
├─app # 業(yè)務(wù)目錄
│ ├─api ## 存放api的目錄(暫時不用)
│ ├─model ## 存放實體的目錄(暫時不用)
│ └─service ## 存放業(yè)務(wù)代碼的目錄(暫時不用)
├─config # 存放配置文件的目錄
├─middleware # 存放中間件的目錄
├─routers # 存放路由的目錄
└─utils # 常用工具組件目錄
├─ACS ## 存放訪問控制執(zhí)行器目錄
├─APIResponse ## 存放API統(tǒng)一響應(yīng)函數(shù)目錄
├─Cache ## 緩存工具目錄
└─DB ## 數(shù)據(jù)連接文件目錄
├─go.mod
├─go.sum
├─main.go # 項目入口文件
2.4 項目代碼開發(fā)
2.4.1 工具組件開發(fā)
# 進(jìn)入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 配置文件
常規(guī)項目中配置文件目錄中會存放各種配置文件,在這個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權(quán)限控制的中間件
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 {
// 從數(shù)據(jù)庫中讀取&判斷
//記錄日志
ACS.Enforcer.EnableLog(true)
// 加載策略規(guī)則
err := ACS.Enforcer.LoadPolicy()
if err != nil {
log.Println("loadPolicy error")
panic(err)
}
// 驗證策略規(guī)則
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訪問控制
新開啟一個命令行終端
# 訪問接口
# 參數(shù)缺失
curl -X POST http://127.0.0.1:8080/api/routers
{"code":-1,"message":"header miss userName","data":null}
# 無訪問權(quán)限
curl -X POST -H "userName:tom" http://127.0.0.1:8080/api/routers
{"code":-1,"message":"access denied","data":null}
# 添加一條規(guī)則(代碼中是模擬數(shù)據(jù))
curl -X POST http://127.0.0.1:8080/api/acs
{"code":200,"message":"success","data":"add success"}
# 再次訪問(有訪問權(quán)限,可以訪問)
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"
}
]
}
# 直接向數(shù)據(jù)庫添加幾條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"}
## 刪除接口(沒有分配訪問權(quán)限)
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的一些適配器有自動保存功能而另外一些則沒有,有自動保存功能的適配器會在連接數(shù)據(jù)的時候自動創(chuàng)建一張表用來保存Policy策略數(shù)據(jù)(替代存儲Policy的csv文件)
上述 Demo 的SQL文件如下(該表是gorm適配器自動創(chuàng)建的)
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