集成gin和casbin

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

如果你搜到这篇文章,那么什么是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注:下面的这个更常用。程序中判断如果角色是admin则直接传’admin’而非用户名,这样直接有所有权限了。 [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, “/login”) == 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', 'resource', '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)
    }
}

大功告成。