概念
權限管理幾乎是每個系統或者服務都會直接或者間接涉及的部分. 權限管理保障了資源(大部分時候就是數據)的安全, 權限管理一般都是和業務強關聯, 每當有新的業務或者業務變化時, 不能將精力完全放在業務實現上, 權限的調整往往耗費大量的精力. 其實, 權限的本質沒有那么復雜, 只是對訪問的控制而已, 有一套完善的訪問控制接口, 再加上簡單的權限模型. 權限模型之所以能夠簡單, 就是因為權限管理本身並不復雜, 只是在和具體業務結合時, 出現了各種各樣的訪問控制場景, 才顯得復雜.
PERM 模型
PERM(Policy, Effect, Request, Matchers)模型很簡單, 但是反映了權限的本質 – 訪問控制
- Policy: 定義權限的規則
- Effect: 定義組合了多個 Policy 之后的結果, allow/deny
- Request: 訪問請求, 也就是誰想操作什么
- Matcher: 判斷 Request 是否滿足 Policy
casbin的作用
- 以經典{subject, object, action}形式或您定義的自定義形式實施策略,同時支持允許和拒絕授權。
- 處理訪問控制模型及其策略的存儲。
- 管理角色用戶映射和角色角色映射(RBAC中的角色層次結構)。
- 支持內置的超級用戶,例如root或administrator。超級用戶可以在沒有顯式權限的情況下執行任何操作。
- 多個內置運算符支持規則匹配。例如,keyMatch可以將資源鍵映射/foo/bar到模式/foo*。
casbin不執行的操作
- 身份驗證(又名驗證username以及password用戶登錄時)
- 管理用戶或角色列表。我相信項目本身管理這些實體會更方便。用戶通常具有其密碼,而Casbin並非設計為密碼容器。但是,Casbin存儲RBAC方案的用戶角色映射。
在使用Casbin 控制后台接口時使用以下模型
[request_definition]
r = sub, obj, act
# 請求的規則
# r 是規則的名稱,sub 為請求的實體,obj 為資源的名稱, act 為請求的實際操作動作
[policy_definition]
p = sub, obj, act
# 策略的規則
# 同請求
[role_definition]
g = _, _
# 角色的定義
# g 角色的名稱,第一個位置為用戶,第二個位置為角色,第三個位置為域(在多租戶場景下使用)
[policy_effect]
e = some(where (p.eft == allow))
# 任意一條 policy rule 滿足, 則最終結果為 allow
[matchers]
m = g(r.sub, p.sub) == true \
&& keyMatch2(r.obj, p.obj) == true \
&& regexMatch(r.act, p.act) == true \
|| r.sub == "root"
# [matchers] 也可以這樣寫
# m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "root"
# 前三個用來匹配上面定義的請求的規則, 最后一個或條件為:如果實體是root 直接通過, 不驗證權限
RBAC模型的示例策略如下:
p, cityAdmin, /city, GET
p, cityAdmin, /city, POST
p, countyAdmin, /county, GET
g, mayanan, superAdmin
在理解了Casbin 的工作原理后,實際寫代碼測試一下
需要使用的外部包
go get -u github.com/casbin/casbin Casbin 官方庫
go get -u github.com/casbin/gorm-adapter Casbin 插件,用來將規則和策略保存到數據庫中
go get -u github.com/gin-gonic/gin Go Web 框架
go get -u github.com/go-sql-driver/mysql Go MySQL 驅動
方案一
點擊查看代碼
package main
import (
"fmt"
"github.com/casbin/casbin"
"github.com/casbin/gorm-adapter"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"net/http"
)
func main() {
// 要使用自己定義的數據庫rbac_db,最后的true很重要.默認為false,使用缺省的數據庫名casbin,不存在則創建
a := gormadapter.NewAdapter("mysql", "root:pwdZ@tcp(rm-xxxx.mysql.rds.aliyuncs.com:33016)/my_casbin?charset=utf8mb4&parseTime=True&loc=Local", true)
e := casbin.NewEnforcer("./test/model.conf", a)
// 從DB加載策略
e.LoadPolicy()
//獲取router路由對象
r := gin.New()
r.POST("/api/v1/add", func(c *gin.Context) {
fmt.Println("增加Policy")
// AddPolicy 向當前策略添加授權規則。如果規則已經存在,函數返回false,不會添加規則。否則,該函數通過添加新規則返回 true
if ok := e.AddPolicy("admin", "/api/v1/hello", "GET"); !ok {
fmt.Println("Policy已經存在")
} else {
fmt.Println("增加成功")
}
})
//刪除policy
r.DELETE("/api/v1/delete", func(c *gin.Context) {
fmt.Println("刪除Policy")
// RemovePolicy 從當前策略中刪除授權規則。
if ok := e.RemovePolicy("admin", "/api/v1/hello", "GET"); !ok {
fmt.Println("Policy不存在")
} else {
fmt.Println("刪除成功")
}
})
//獲取policy
r.GET("/api/v1/get", func(c *gin.Context) {
fmt.Println("查看policy")
// GetPolicy 獲取策略中的所有授權規則。
list := e.GetPolicy()
for _, vlist := range list {
for _, v := range vlist {
fmt.Printf("value: %s, ", v)
}
}
})
//使用自定義攔截器中間件
r.Use(Authorize(e))
//創建請求
r.GET("/api/v1/hello", func(c *gin.Context) {
fmt.Println("Hello 接收到GET請求..")
})
r.Run(":9000") //參數為空 默認監聽8080端口
}
//攔截器
func Authorize(e *casbin.Enforcer) gin.HandlerFunc {
return func(c *gin.Context) {
//獲取請求的URI
obj := c.Request.URL.RequestURI()
//獲取請求方法
act := c.Request.Method
//獲取用戶的角色
sub := "admin"
//判斷策略中是否存在
if ok := e.Enforce(sub, obj, act); ok {
fmt.Println("恭喜您,權限驗證通過")
c.Next()
} else {
fmt.Println("很遺憾,權限驗證沒有通過")
c.Abort()
c.String(http.StatusUnauthorized, "無權訪問")
}
}
}
方案二:
點擊查看代碼
package main
import (
"fmt"
"github.com/casbin/casbin"
"github.com/casbin/gorm-adapter"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"net/http"
//"gorm.io/gorm"
)
// 統一響應結構體
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
var O *gorm.DB
var PO *gormadapter.Adapter
var Enforcer *casbin.Enforcer
func ping(c *gin.Context) {
var response Response
response.Code = 0
response.Message = "success"
response.Data = ""
c.JSON(200, response)
return
}
// 數據庫連接及角色規則的初始化
func connect() {
dsn := "root:xxx@tcp(rm-xxx.mysql.rds.aliyuncs.com:33016)/my_casbin?charset=utf8mb4&parseTime=True&loc=Local"
var err error
O, err = gorm.Open("mysql", dsn)
if err != nil {
fmt.Println("connect DB error")
panic(err)
}
// 將數據庫連接同步給插件, 插件用來操作數據庫
PO = gormadapter.NewAdapterByDB(O)
// 這里也可以使用原生字符串方式
Enforcer = casbin.NewEnforcer("./test/model.conf", PO)
// 開啟權限認證日志
Enforcer.EnableLog(true)
// 加載數據庫中的策略
err = Enforcer.LoadPolicy()
if err != nil {
fmt.Println("loadPolicy error")
panic(err)
}
// 創建一個角色,並賦於權限
// admin 這個角色可以用 GET 方式訪問 /api/v2/ping
res := Enforcer.AddPolicy("admin", "/api/v2/ping", "GET")
if !res {
fmt.Println("policy is exist")
} else {
fmt.Println("policy is not exist, adding")
}
// 將 test 用戶加入一個角色中
Enforcer.AddRoleForUser("test", "root")
Enforcer.AddRoleForUser("tom", "admin")
// 請看規則中如果用戶名為 root 則不受限制
}
func main() {
defer O.Close()
connect()
g := gin.Default()
// 這里的接口沒有使用權限認證中間件
version1 := g.Group("/api/v1")
{
version1.GET("/ping", ping) // 這個是通用的接口
}
// 接口使用權限認證中間件
version2 := g.Group("/api/v2", CasbinMiddleWare)
{
version2.GET("/ping", ping)
}
_ = g.Run(":8099")
}
// casbin middleware 權限認證中間件
func CasbinMiddleWare(c *gin.Context) {
var userName string
userName = c.GetHeader("userName")
if userName == "" {
fmt.Println("headers invalid")
c.JSON(200, gin.H{
"code": http.StatusUnauthorized,
"message": "Unauthorized",
"data": "",
})
c.Abort()
return
}
// 請求的path
p := c.Request.URL.Path
// 請求的方法
m := c.Request.Method
// 這里認證
res, err := Enforcer.EnforceSafe(userName, p, m)
// 這個 HasPermissionForUser 跟上面的有什么區別
// EnforceSafe 會驗證角色的相關的權限
// 而 HasPermissionForUser 只驗證用戶是否有權限
//res = Enforcer.HasPermissionForUser(userName,p,m)
if err != nil {
fmt.Println("no permission ")
fmt.Println(err)
c.JSON(200, gin.H{
"code": 401,
"message": "Unauthorized",
"data": "",
})
c.Abort()
return
}
if !res {
fmt.Println("permission check failed")
c.JSON(200, gin.H{
"code": 401,
"message": "Unauthorized",
"data": "",
})
c.Abort()
return
}
c.Next()
}
- 結果
p 代表的是策略 admin 角色可以使用GET 訪問 /api/v2/ping
g 代表的是角色 test 用戶在root 角色中
tom 在admin 角色中
所以在測試時請求頭
userName = root 有正常響應 (這里不會到數據庫驗證,策略最后一條)
userName = tom 正常響應 (tom 有admin 角色 , 所以驗證通過)
userName = role_admin 正常響應 (參考這里:https://casbin.org/docs/zh-CN/rbac , 正常情況下用戶名和角色名稱不應該一樣)
userName = *** 都無法通過認證