Casbin 是一个授权库,在我们希望特定用户访问特定的 对象 或实体的流程中可以使用 主题 访问类型,例如 动作 可以是 读取, 写入, 删除 或开发者设置的任何其他动作。 这是Casbin最广泛的使用,它叫做"标准" 或经典 { subject, object, action } 流程。
Casbin能够处理除标准流量以外的许多复杂的许可使用者。 可以添加 角色 (RBAC), 属性 (ABAC) 等。
**PERM(Policy, Effect, Request, Matchers)模型很简单, 但是反映了权限的本质 – 访问控制。**在 Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件。 因此,切换或升级项目的授权机制与修改配置一样简单。 您可以通过组合可用的模型来定制您自己的访问控制模型。 例如,您可以在一个model中结合RBAC角色和ABAC属性,并共享一组policy规则。
policy: 定义权限的规则
Effect: 定义组合了多个 Policy 之后的结果, allow/deny
Request: 访问请求, 也就是谁想操作什么
Matcher: 判断 Request 是否满足 Policy.
适配器作用
适配器对应版本
自定义的策略
settings.go
模拟一些数据
// 模拟用户
var Users = map[string]User{
"1":{
Name: "zhangsan",
Age: "20",
RoleKey: admin,
},
"2":{
Name: "lisi",
Age: "21",
RoleKey: formalMember,
},
"3":{
Name: "wangwu",
Age: "23",
RoleKey: businessAdmin,
},
"4":{
Name: "zhaoliu",
Age: "24",
RoleKey: normalUser,
},
"5":{
Name: "tianqi",
Age: "25",
RoleKey: formalMember,
},
}
//模拟认证
func GetIdentity(c *gin.Context) *User {
auth := c.Request.Header.Get("Authentication")
user,ok := Users[auth]
if ok {
return &user
}
return nil
}
mycasbin.go
定义casbin restful权限模型,将策略存在数据库
/**
采用模型 RESTFUL 支持路径, 如 /res/*, /res/: id 和 HTTP 方法, 如 GET, POST, PUT, DELETE。
keyMatch2和keyMatch区别:
keyMatch : 一个URL 路径或 * 模式下,例如 /alice_data/*
keyMatch2:一个URL 路径或 : 模式下,例如 /alice_data/:resource
r : 请求。 sub主题(访问实体)、obj对象(访问资源) 和 act动作(访问方式
p : 策略。 sub主题(访问实体)、obj对象(访问资源) 和 act动作(访问方式
m: 匹配器。请求访问实体 == 策略定义的访问实体 并且 (url匹配到请求对应中访问资源 || url匹配到请求和策略中对应的访问资源 )
并且 (请求中的行为与策略中的行为一致 || 策略的行为*)
e : 效果。 满足匹配器即为true,否则为false
*/
var text = `
[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) || keyMatch(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
`
// 设置casbin
func Setup(db *gorm.DB) (*casbin.SyncedEnforcer, error) {
//建立数据库连接
//apter, err := gormadapter.NewAdapter("mysql", "root:root1234@tcp(127.0.0.1:31234)/") // Your driver and data source.
apter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
return nil, err
}
//将casbin model转化为字符串格式
modelFromString, err := model.NewModelFromString(text)
if err != nil {
return nil, err
}
//NewSyncedEnforcer通过文件或数据库创建同步执行器。
nef, err := casbin.NewSyncedEnforcer(modelFromString, apter)
if err != nil {
return nil, err
}
//LoadPolicy从文件/数据库重新加载策略。
err = nef.LoadPolicy()
if err != nil {
return nil, err
}
return nef, err
}
```
此时会在数据库中初始化一张casbin_rule的表,表是空的。长这个样子
id | ptype | v0 | v1 | v2 | v3 | v4 | v5 |
---|---|---|---|---|---|---|---|
定义完api,然后手动创建数据
application.go
抽象配置
import (
"net/http"
"sort"
"strings"
"sync"
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
var Runtime = NewApplication()
type Application struct {
mux sync.RWMutex
db *gorm.DB
casbin *casbin.SyncedEnforcer
}
// SetDb 设置对应key的db
func (e *Application) SetDb(db *gorm.DB) {
e.mux.Lock()
defer e.mux.Unlock()
e.db = db
}
// GetDb 获取所有map里的db数据
func (e *Application) GetDb() *gorm.DB {
e.mux.Lock()
defer e.mux.Unlock()
return e.db
}
func (e *Application) SetCasbin(enforcer *casbin.SyncedEnforcer) {
e.mux.Lock()
defer e.mux.Unlock()
e.casbin = enforcer
}
func (e *Application) GetCasbin() *casbin.SyncedEnforcer {
e.mux.Lock()
defer e.mux.Unlock()
return e.casbin
}
func NewApplication() Application {
return Application{}
}
middleware.go
模拟认证授权中间件
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
// 定义一些角色 TODO 未来数据库存储
const (
admin = "admin"
businessAdmin = "businessAdmin"
formalMember = "formalMember"
normalUser = "normalUser"
)
// 检查角色,鉴权
func AuthCheckRole() gin.HandlerFunc {
return func(c *gin.Context) {
// TODO 认证 jwt
user := GetIdentity(c)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "token过期,不存在",
})
c.Abort()
return
}
// 鉴权 casbin
if user.RoleKey == admin {
fmt.Printf("用户:%s, 是 %s, 直接通过! \n",user.Name,user.RoleKey)
c.Next()
return
}
// 数据库匹配
res, err := Runtime.casbin.Enforce(user.RoleKey, c.Request.URL.Path, c.Request.Method)
if err != nil {
fmt.Printf("AuthCheckRole error: %s method:%s path:%s\n", err, c.Request.Method, c.Request.URL.Path)
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
})
return
}
// 匹配成功
if res {
fmt.Printf("username :%s, isTrue: %v, role: %s method: %s path: %s \n", user.Name,res, user.RoleKey, c.Request.Method, c.Request.URL.Path)
c.Next()
} else {
fmt.Printf("username :%s,isTrue: %v, role: %s method: %s path: %s message: %s \n", user.Name,res, user.RoleKey, c.Request.Method, c.Request.URL.Path, "当前request无权限,请管理员确认!")
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"msg": "对不起,您没有该接口访问权限,请联系管理员",
})
c.Abort()
return
}
}
}
mycasbin_test.go
写一些测试case
package casbin
import (
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"net/http"
"net/http/httptest"
"testing"
)
type header struct {
Key string
Value string
}
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, nil)
for _, h := range headers {
req.Header.Add(h.Key, h.Value)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func init() {
// 初始化db
db, err := gorm.Open(mysql.Open(fmt.Sprintf("root:root1234@tcp(localhost:%v)/learnk2?charset=utf8&parseTime=True&loc=Local", 31234)), &gorm.Config{})
if err != nil {
panic(fmt.Sprintf("failed to connect db, got error: %v, port: %v", err, 31234))
}
Runtime.db = db
// 初始化 casbin
casbin, err := Setup(db)
if err != nil {
panic(fmt.Sprintf("failed to init casbin, got error: %v", err))
}
Runtime.casbin = casbin
}
// 1,2,3,4,5就当是token了
var headers = []header{
{
Key: "Authentication",
Value: "1",
},
{
Key: "Authentication",
Value: "2",
},
{
Key: "Authentication",
Value: "3",
},
{
Key: "Authentication",
Value: "4",
},
{
Key: "Authentication",
Value: "5",
},
{
Key: "Authentication",
Value: "6",
},
}
func TestAuth(t *testing.T) {
router := gin.New()
router.Use(AuthCheckRole())
router.GET("/api/v1/test", func(c *gin.Context) {
fmt.Println("hello")
}) //api资源为 /api/v1/test
//做不同角色的case测试
for _,h := range headers {
w := performRequest(router, "GET", "/api/v1/test",h)
fmt.Println(w.Body.String())
}
}
手动配置数据库资源
当角色为业务管理员businessAdmin,放过
当角色为正式成员formalMember, 放过
数据库casbin_rule表
id | ptype | v0 | v1 | v2 | v3 | v4 | v5 |
---|---|---|---|---|---|---|---|
1 | p | businessAdmin | /api/v1/test | GET | |||
1 | p | formalMember | /api/v1/test | GET |
测试预期
执行结果
=== RUN TestAuth
[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] GET /api/v1/test --> learnk2/common/casbin.TestAuth.func1 (2 handlers)
用户:zhangsan, 是 admin, 直接通过!
hello
username :lisi, isTrue: true, role: formalMember method: GET path: /api/v1/test
hello
username :wangwu, isTrue: true, role: businessAdmin method: GET path: /api/v1/test
hello
username :zhaoliu,isTrue: false, role: normalUser method: GET path: /api/v1/test message: 当前request无权限,请管理员确认!
{"code":403,"msg":"对不起,您没有该接口访问权限,请联系管理员"}
username :tianqi, isTrue: true, role: formalMember method: GET path: /api/v1/test
hello
{"code":401,"msg":"token过期,不存在"}
--- PASS: TestAuth (0.00s)
PASS
结论