34

集成go和casbin

 3 years ago
source link: http://www.hi-roy.com/2020/09/28/集成gin和casbin/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

原文 ,省略了一些无关的内容。

如果你搜到这篇文章,那么什么是gin以及casbin应该不用过多解释了。

项目结构

root/
    main.go              # entry point of application
    handler/             # Gin handler functions
    middleware/          # Gin middlewares
    config/              # some configuration files like Casbin's rbac_model.conf
    component/           # global components like GORM DB instance

初始化数据库和缓存

component 目录下创建 persistence.go 用于初始化,这里使用 GORM 来处理数据库, BigCache 处理缓存:

import (
    "fmt"
    "github.com/allegro/bigcache"
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "time"
)

var (
    DB           *gorm.DB
    GlobalCache  *bigcache.BigCache
)

func init() {
    // Connect to DB
    var err error
    DB, err = gorm.Open("mysql", "your_db_url")
    if err != nil {
        panic(fmt.Sprintf("failed to connect to DB: %v", err))
    }

    // Initialize cache
    GlobalCache, err = bigcache.NewBigCache(bigcache.DefaultConfig(30 * time.Minute)) // Set expire time to 30 mins
    if err != nil {
        panic(fmt.Sprintf("failed to initialize cahce: %v", err))
    }
}

在这个示例中,我们使用数据库来存储casbin的polices,使用缓存存储登录用户信息。

配置Casbin

Model Configuration File

首先,你也许会发现casbin中有些概念让你很困惑,比如 Model Configuration File 。这里我不想讨论太多原理(因为我也不熟),直接举个例子,使用基于角色的权限控制(RBAC,Role-based access control)。所以首先在 config 目录创建 rbac_model.conf

[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

上面的文件定义了casbin如何判断用户拥有什么权限,例子中我们定义了5个字段:

  1. r = sub, obj, act 定义了一个请求需要由3部分组成:sub=用户,obj=URL或资源,act=操作。
  2. p = sub, obj, act 定义了策略的格式,比如 admin,dada,write 表示admin有data的写权限。
  3. e = some(where (p.eft == allow)) 定义了用户可以做那些策略中定义准许他做的事。
  4. g = _, _ 定义了角色的格式,例如 bob,admin 表示用户bob是admin这个角色。
  5. m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 定义了鉴权时的流程,先检查用户角色,再检查用户访问的资源,最后检查用户行为。

上面几个部分,仅1、2、3、5是必须的,如果不使用RBAC可以忽略4。

Roy注:下面的这个更常用。

[matchers]

m = g(r.sub, p.sub) == true \

&& keyMatch2(r.obj, p.obj) == true \

&& regexMatch(r.act, p.act) == true \

|| r.sub == “admin” \

|| keyMatch2(r.obj, “/auth”) == true

Polices

举个例子:

p, user, data, read
p, admin, data, read
p, admin, data, write
g, Alice, admin
g, Bob, user

首先我们定义了3个策略:

  1. user可以读取data
  2. admin可以写data
  3. admin可以读data

以及2个用户角色:

  1. Alice属于admin
  2. Bob属于user

所以Alice有数据的所有权限而Bob只能读取数据。官网教程中casbin使用csv来简单的存储策略,这里我们使用数据库。casbin通常把表名命名为 casbin_rule ,结构语句如下:

CREATE TABLE casbin_rule (
    p_type VARCHAR(100),
    v0 VARCHAR(100),
    v1 VARCHAR(100),
    v2 VARCHAR(100)
);

INSERT INTO casbin_rule VALUES('p', 'user', 'data', 'read');
INSERT INTO casbin_rule(p_type, v0, v1) VALUES('g', 'Bob', 'user');

实现Gin的Handler

首先实现登录逻辑

// handler/user_handler.go

func Login(c *gin.Context) {
    username, password := c.PostForm("username"), c.PostForm("password")
        // Authentication
        // blahblah...

    // Generate random session id
    u, err := uuid.NewRandom()
    if err != nil {
        log.Fatal(err)
    }
    sessionId := fmt.Sprintf("%s-%s", u.String(), username)
    // Store current subject in cache
    component.GlobalCache.Set(sessionId, []byte(username))
    // Send cache key back to client in cookie
    c.SetCookie("current_subject", sessionId, 30*60, "/resource", "", false, true)
    c.JSON(200, component.RestResponse{Code: 1, Message:username + " logged in successfully"})
}

如果登录成功,我们存储用户(或者叫sub)信息到缓存中,这里不要忘记将sessionId写回cookie中。casbin只负责鉴权不负责认证,所以我们要自己实现认证逻辑。接下来实现读、写逻辑:

// handler/resource_handler.go

func ReadResource(c *gin.Context) {
        // some stuff
        // blahblah...

    c.JSON(200, component.RestResponse{Code: 1, Message: "read resource successfully", Data: "resource"})
}

func WriteResource(c *gin.Context) {
        // some stuff
        // blahblah...

    c.JSON(200, component.RestResponse{Code: 1, Message: "write resource successfully", Data: "resource"})
}

然后实现 main.go

// main.go

var (
    router *gin.Engine
)

func init() {
    // Initialize gin router
    router = gin.Default()
    corsConfig := cors.DefaultConfig()
    corsConfig.AllowAllOrigins = true
    corsConfig.AllowCredentials = true
    router.Use(cors.New(corsConfig)) // CORS configuraion
    router.POST("/user/login", handler.Login)
    router.GET("/resource", handler.ReadResource)
    router.POST("/resource", handler.WriteResource)
}

func main() {
    defer component.DB.Close()

    // Start our application
    err := router.Run(":8081")
    if err != nil {
        panic(fmt.Sprintf("failed to start gin engin: %v", err))
    }
    log.Println("application is now running...")
}

一切就绪,接下来开始集成。

启用casbin策略

从数据库加载polices

第一个问题就是,我们如何从数据库动态加载策略?我们可以使用 Casbin Adapters ,更精确的说我们使用的是 Gorm Adapter 。首先进行初始化:

// main.go

func init() {
    // Initialize  casbin adapter
    adapter, err := gormadapter.NewAdapterByDB(component.DB)
    if err != nil {
        panic(fmt.Sprintf("failed to initialize casbin adapter: %v", err))
    }

    // Initialize gin router
    router = gin.Default()
    corsConfig := cors.DefaultConfig()
    corsConfig.AllowAllOrigins = true
    corsConfig.AllowCredentials = true
    router.Use(cors.New(corsConfig)) // CORS configuraion
        router.POST("/user/login", handler.Login)
        router.GET("/resource", handler.ReadResource)
        router.POST("/resource", handler.WriteResource)
}

显然的,在进行任何操作前都需要经过鉴权,所以更优雅的方式是使用gin提供的 middlewaresgrouping routes

// middleware/access_control.go

// Authorize determines if current subject has been authorized to take an action on an object.
func Authorize(obj string, act string, adapter *gormadapter.Adapter) gin.HandlerFunc {
    return func(c *gin.Context) {
		// Get current user/subject
        val, existed := c.Get("current_subject")
        if !existed {
            c.AbortWithStatusJSON(401, component.RestResponse{Message: "user hasn't logged in yet"})
            return
        }
        // Casbin enforces policy
        ok, err := enforce(val.(string), obj, act, adapter)
        if err != nil {
            log.Println(err)
            c.AbortWithStatusJSON(500, component.RestResponse{Message: "error occurred when authorizing user"})
            return
        }
        if !ok {
            c.AbortWithStatusJSON(403, component.RestResponse{Message: "forbidden"})
            return
        }
        c.Next()
    }
}

func enforce(sub string, obj string, act string, adapter *gormadapter.Adapter) (bool, error) {
	// Load model configuration file and policy store adapter
    enforcer, err := casbin.NewEnforcer("config/rbac_model.conf", adapter)
    if err != nil {
        return false, fmt.Errorf("failed to create casbin enforcer: %w", err)
    }
    // Load policies from DB dynamically
    err = enforcer.LoadPolicy()
    if err != nil {
        return false, fmt.Errorf("failed to load policy from DB: %w", err)
    }
	// Verify
    ok, err := enforcer.Enforce(sub, obj, act)
    return ok, err
}

最后进行一些修改:

// main.go

func init() {
    // Initialize  casbin adapter
    adapter, err := gormadapter.NewAdapterByDB(component.DB)
    if err != nil {
        panic(fmt.Sprintf("failed to initialize casbin adapter: %v", err))
    }

    // Initialize Gin router
    router = gin.Default()
    corsConfig := cors.DefaultConfig()
    corsConfig.AllowAllOrigins = true
    corsConfig.AllowCredentials = true
    router.Use(cors.New(corsConfig)) // CORS configuraion
        router.POST("/user/login", handler.Login)
        // Secure our API
    resource := router.Group("/api")
    {
        resource.GET("/resource", middleware.Authorize("resource", "read", adapter), handler.ReadResource)
        resource.POST("/resource", middleware.Authorize("resource", "write", adapter), handler.WriteResource)
    }
}

大功告成。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK